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
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).
Download cmdline-tools
Go to Android Studio website and download "Command line tools only". Unpack them with unzip
command in a desired 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 August 2022, 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
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 two packages to be installed: build-tools
and platform-tools
.
sdkmanager "build-tools;33.0.0" "platform-tools"
33.0.0
is the latest version of build-tools
as of August 2022. Learn what the latest version of build-tools
is available by running:
sdkmanager --list | grep build-tools
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.
- Starts ADB and launches
code-server
.
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
# 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; code-server"
Optional
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="\$(ss -tulpn | grep ":\$port" | grep -Po 'pid=\\d+,' | grep -Po '\\d+' | uniq)"
if [ ! -z \$pid ]; then
kill "\$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.