VS Code can indeed be used for remote development. I was able to achieve the described workflow using code-server and a bunch of SSH forwarding.
Here I describe how to set up code-server for Android development on Flutter with a remote machine used for build and debug tasks, and with a local machine used to access the editor GUI and to connect a physical device.
Verified on Ubuntu 22.04 (server), Fedora 36 (local), and Flutter 3.0.5.
Install software and SDKs on remote machine
1. Install code-server from the official repo
Important: After installing code-server tells you to make systemd start your server automatically by running a systemctl command. Avoid doing that because that way ADB under VS Code won't detect devices. I haven't come up with any workaround for that yet to make it work under a systemd-managed instance.
2. Install Flutter SDK and update your PATH
Your system might also need additional dependencies for the Flutter SDK to run. I recommend learning about it from the official manual. Prefer manual ways of installation described there.
After installation is done, update PATH variable in ~/.bashrc file to include /bin folder of the Flutter SDK, for example, add a line like this:
export PATH="$PATH:$HOME/path/to/flutter/bin"
after which, apply the changes:
source ~/.bashrc
3. Install Android toolchain
I assume your server does not have any Desktop Environment, so we will install Android toolchain without Android Studio (since Studio requires a DE to run).
Install Java
As of September 2023, Android Command line tools require minimal class file version 61.0 (Java 17).
You can check what version of Java you have installed by running java --version, you can also check where it is installed by running which java. If you already have Java installed, make sure it is compatible with cmdline-tools and your projects. You can install multiple Java versions and control your preferred version with sudo update-alternatives --config java command or JAVA_HOME environment variable.
Here we'll be installing OpenJDK from Ubuntu's repositories for simplicity. If you prefer so, you can install commercial Oracle Java from the official website.
To install OpenJDK 17, run:
sudo apt install openjdk-17-jdk
You can also install latest available OpenJDK version, to list available versions for install run:
sudo apt list openjdk-*-jdk
If you are trying to build an existing project
Older Flutter projects that did not target Android 14 might require older JDK version to build out of the box, like openjdk-11-jdk. Newer cmdline-tools won't work with openjdk-11, so you will either have to use older cmdline-tools and JDK, or migrate the project to newer version. Google doesn't provide links to older releases of cmdline-tools, but you can find them on the Internet Archive, for example, this archived copy of v9.
Download cmdline-tools
Go to Android Studio website and download "Command line tools only".
To download the archive directly onto your server, click the link on the Android Studio website, scroll down and agree to the terms, but instead of clicking the "Download Android Command Line Tools for Linux" button, right click it and copy the URL. Then download it from your server, for example, using wget:
wget https://dl.google.com/android/repository/commandlinetools-linux-XXX_latest.zip
Unpack them with unzip commandlinetools-linux-XXX_latest.zip command in a desired location. Calling unzip will create a cmdline-tools folder in your current location. I recommend creating this folder structure when unpacking the archive:
~/path/to/android-sdk/cmdline-tools
This way, when sdkmanager downloads its packages, new folders will be created inside the android-sdk folder.
As of September 2023, sdkmanager inside Android command line tools requires a special folder hierarchy, which can be achieved by putting content of the cmdline-tools folder inside a latest folder under it with this command:
mv ./cmdline-tools/ ./latest && mkdir cmdline-tools && mv ./latest/ ./cmdline-tools/latest/
Add the tools to your PATH in ~/.bashrc file and specify ANDROID_SDK_ROOT by adding new lines:
export ANDROID_SDK_ROOT="$HOME/path/to/android-sdk" export PATH="$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools"
Don't forget to run source ~/.bashrc
Install SDK
Flutter SDK requires three packages to be installed: build-tools, platforms and platform-tools.
sdkmanager "build-tools;34.0.0" "platforms;android-34" "platform-tools"
34.0.0 is the latest version of build-tools, and android-34 is the latest version of platforms, as of September 2023, targeting Android 14.
Learn what the latest version of build-tools is available by running:
sdkmanager --list | grep build-tools
And for platforms:
sdkmanager --list | grep "platforms;android"
For existing projects, Flutter will automatically download required SDKs to build them.
Accept licenses
Run sdkmanager --licenses and accept all licenses by pressing the y key
VS Code setup
After installing code-server, you can now access the editor from your browser.
It is also recommended to install it as a PWA, so you will have more screen space, more options for keyboard shortcuts and ability to launch the editor from the system launcher.
- Install the Flutter extension
- Add these settings to VS Code user settings:
{ "dart.flutterRunAdditionalArgs": [ // Dart Developer Service port (debugger) "--dds-port=10388", // Dart VM Service instance port (device) "--host-vmservice-port=10389" ], "dart.devToolsPort": 9100, }
By default, Dart chooses random ports for connection between the debugger and the device. By setting these settings we make the ports static, so we can forward them easily.
You might want to add dart.devToolsLocation: external to your config if your browser or adblocker prevents "local network intrusion", aka preventing websites from accessing localhost ports. Though, I'd recommend adding your code-server instance into exceptions instead.
Port forwarding with SSH on local machine
For debugging an Android app with Flutter, we'll have to forward 4 ports. The table lists the port numbers according to the VS Code settings above. You can use your own port numbers, but then you must change the configs accordingly.
| Port | Description | Forwarding |
| 5037 | ADB | To remote |
| 10388 | Dart Developer Service | To local |
| 10389 | VM Service on device | To remote |
| 9100 | Dart DevTools | To local |
Commands
SSH can be used to forward ports.
To forward a port from localhost to remote host, run on the local machine:
ssh -R XXXX:localhost:XXXX user@host
To forward a port from remote host to localhost, run on the local machine:
ssh -L XXXX:localhost:XXXX user@host
You can chain options to ssh command, for example:
ssh -R 5037:localhost:5037 -L 10388:localhost:10388 -R 10389:localhost:10389 -L 9100:localhost:9100 user@host
Port forwarding will be active until you close the SSH connection.
Make sure your firewall is set up to allow port forwarding.
Automation script
I made a script that automates possible quirks around the process:
- Kills local ADB and restarts it to release used ports
- Sets up port forwarding for specified ports to a remote host. The script expects you to use keys for SSH authentication.
- Kills all running instances of
code-server, node, and adb. You will have to customize this if this doesn't work for you. - Sets
VSCODE_PROXY_URI to be empty to turn off port forwarding URL overrides, which break Dart DevTools. Starts ADB and launches code-server.
It is intended to be run on the local machine.
#!/bin/bash # Remote machine CE_MACHINE="user@host" # Local machine, no need to change CE_LOCALHOST="localhost" # Default port for ADB daemon is 5037 CE_ADB_PORT="5037" # You might need to specify exact path to adb on your # remote system since .bashrc is not sourced for # non-interactive sessions (see https://stackoverflow.com/a/6212684) CE_ADB_EXECUTABLE="~/dev/tools/android-sdk/platform-tools/adb" # "Dart Developer Service port CE_DDS_PORT="10388" # Dart VM Service instance port CE_HOST_VMSERVICE_PORT="10389" # Flutter DevTools port CE_DEVTOOLS_PORT="9100" #### VSCode Settings #### # "dart.flutterRunAdditionalArgs": [ # "--dds-port=10388", # "--host-vmservice-port=10389", # ], # "dart.devToolsPort": 9100 #### VSCode Settings #### # Reset ADB on local machine # so it releases used ports killall adb adb devices ## INSERT ADDITIONAL LINES HERE # When `adb devices` is called, ADB checks the daemon port. # If it detects there's no response on the port, it # launches a new daemon. # # After killing ADB and forwarding the ADB port to local machine, # a newly launched ADB client will bind to the existing connection # (which is our physical device) instead of launching a daemon on # the remote machine. # # Restart code-server # ADB doesn't detect devices if code-server is managed by systemctl # WARNING, killing all codee-server, Node, Dart, and ADB processes here. Customize if needed. # # 1. ADB forwarding Local -> Remote # 2. Dart Dev Server forwarding Remote -> Local # 3. Dart on-device debugger client forwarding Local -> Remote # 4. Flutter DevTools Remote -> Local forwarding ssh -R $CE_ADB_PORT:$CE_LOCALHOST:$CE_ADB_PORT \ -L $CE_DDS_PORT:$CE_LOCALHOST:$CE_DDS_PORT \ -R $CE_HOST_VMSERVICE_PORT:$CE_LOCALHOST:$CE_HOST_VMSERVICE_PORT \ -L $CE_DEVTOOLS_PORT:$CE_LOCALHOST:$CE_DEVTOOLS_PORT \ $CE_MACHINE "killall code-server; killall node; killall dart; killall adb; $CE_ADB_EXECUTABLE devices; VSCODE_PROXY_URI= code-server"
Additional port management
Sometimes connection can be broken unexpectedly, so ports will be busy and forwarding won't work. You can add these lines after the adb devices line to the script above:
payload() { cat <<EOF for port in $CE_ADB_PORT $CE_DDS_PORT $CE_HOST_VMSERVICE_PORT $CE_DEVTOOLS_PORT; do pid="\$(sudo ss -tulpn | grep ":\$port" | grep -Po 'pid=\\d+,' | grep -Po '\\d+' | uniq)" if [ ! -z \$pid ]; then kill "\$pid" echo Freed :\$port from PID \$pid fi done EOF } if [ ! -z "$1" ]; then payload | ssh $CE_MACHINE /bin/bash fi
With the code added, if you pass any argument to the script, it will attempt to free the used ports on the remote machine (e.g. script.sh kill).
This code enumerates processes that keep the used ports busy, and then kills them.
For sudo ss to work in the script for non-superuser, you'll need to add the following line to /etc/sudoers by opening the file with sudo visudo command. Doing this will allow your user run sudo ss without entering password:
myusername ALL=(root) NOPASSWD: /usr/bin/ss
Alternatively, you can store your password in the script and change the script line the following way:
echo YOURPASSWORD | sudo -S