For the past few months I've been building a Flutter IDE that runs on an Android phone. The Flutter SDK, the Dart VM, a JDK and Gradle all run on the phone itself, and the APK comes out of the phone too.
It's called Utakata Studio Mobile IDE, and it just went into open testing on Google Play. This post is about the part I found most interesting to solve: getting a toolchain that was written for desktop Linux, macOS and Windows to run on Android.
A Flutter SDK is mostly data
The first thing that made this feel possible was looking at what a Flutter SDK actually contains. Most of it doesn't care which OS it runs on:
- the framework itself is Dart source,
-
flutter_toolsships as a kernel snapshot, - the VM snapshots, ICU data, the patched SDK and
sky_engineare plain files.
What does care are the host binaries. For a phone, that list is short:
- the Dart SDK (
dart,dartaotruntime, the frontend server), -
gen_snapshotfor release and profile builds, -
impellerc(every app needs it, because the framework's own shaders are compiled with it), -
font-subset, -
flutter_testerforflutter test.
So the plan became: build those for Android arm64, and reuse everything else from the official Linux SDK as-is. The Dart SDK builds for Android with its own build script (--os android --arch arm64), and the engine's host tools build with gn --android --android-cpu arm64 using Android's default toolchain. Everything else is copied over from the official linux-x64 SDK, minus the ELF files.
Telling flutter_tools that Android is Linux
Run on a phone, the Dart VM reports Platform.operatingSystem == "android". flutter_tools has never expected to be hosted on Android, so it gets confused quickly.
The fix turned out to be a small patch: report android as linux, map the host platform to linux_arm64, and point the gen_snapshot lookup at the arm64 build. After that, flutter_tools mostly behaves as if it were running on an arm64 Linux machine.
You can't execute files you download (or create)
This was the part that's specific to Android. Since Android 10, an app targeting a recent SDK can't exec files in its own data directory. It's part of Android's W^X policy, and adding a directory to PATH doesn't help.
The only place an app can run native code from is its native library directory. So every executable is packaged inside the APK as lib<name>.so, the APK is built with legacy packaging so the files are extracted to disk, and the SDK's bin/dart and friends are links to those files.
That raised two more problems:
- Shell scripts:
bin/flutterandgradleware scripts, and they can't be exec'd either. Instead of shipping them as fake.sofiles, the IDE starts them through/system/bin/sh. That needed small patches in two places: whereflutter_toolslaunchesgradlew, and where Flutter's Gradle plugin calls back intobin/flutter. - Finding the SDK from the VM: the Dart VM uses
/proc/self/exeto work out where it lives, and that points at…/lib/arm64/libmide_dart.so, not at the SDK. Tools likedart runandbuild_runnerthen can't find the SDK. A patch to the VM makes it return the path it was invoked with (taken fromAT_EXECFN), but only when that path is the same file as/proc/self/exe. It touches nothing underruntime/vm/, so the snapshot hash doesn't change.
A JDK for Gradle
Android builds still go through Gradle and the Android Gradle Plugin, so the phone needs a JDK. I cross-compile OpenJDK 21 for aarch64-linux-android (API 28) with the NDK, using patches from the termux-packages project for the Bionic-specific parts. aapt2 gets the same treatment.
It's a full server JVM (C1 and C2), around 270 MB unpacked. On a recent phone it runs Gradle comfortably.
Building without the internet
A first Gradle build normally downloads a lot. On a phone, on mobile data, that's not great, so the IDE ships the Maven artifacts and Gradle distribution it needs for a release build, plus the pub packages used by the templates, and points Gradle at that local repository first. Packages that aren't bundled are still fetched from pub.dev and Maven when you're online.
The bundled parts add up: what a release build needs (the Maven artifacts, Gradle and lint) is around 440 MB compressed, and it passes 700 MB once you add the debug engine, common plugins and code generation. So only what a build needs is installed with the app, as Play asset packs at install time: the Flutter SDK, those release build essentials and the NDK. The debug engine, common plugins and code-generation packages are downloaded the first time you need them, straight from where they're published (Google's Maven repository and pub.dev).
Hot reload inside the IDE
The preview is the part I like most. Your app runs inside the IDE, in a separate process, on the official Flutter debug engine, which ships inside the app itself. Saving a Dart file triggers a normal hot reload, and the app keeps its state.
On a Galaxy Z Fold7, a neon shooter demo built in about 6 seconds when I pressed Run, showed up in the preview in 0.8 seconds, and hot reloaded in 0.72 seconds after I changed its bullet color. The score stayed where it was.
A bug you only hit when Dart runs on Android
One more that I found recently. In a project that uses Drift (which depends on sqlite3 and its build hook), dart run stopped before running anything:
Build input for sqlite3 contains errors - No value was provided for 'config.extensions.code_assets.android'
The cause wasn't in the hook. When dart run builds native assets for the host, dartdev passes a macOS-specific config but nothing for Android, because nobody runs the Dart SDK on Android. With the host being Android, the hook runner's input check fails before the hook even starts.
Passing an Android code config for the host fixed it, and sqlite3 loads on the phone now. I patched the bundled SDK, and the fix is rolling out through the test builds.
Limits
A few things to know before trying it:
- it needs a 64-bit Android device on Android 9 or later, with at least 4 GB of RAM (8 GB is nicer),
- the toolchain takes several GB of storage,
- some native plugins (camera, location and so on) don't work in the in-IDE preview, so you install the APK to try them,
- it's an open test, and things will break.
Try it
If you're curious, the open test is on Google Play and anyone can join:
https://play.google.com/store/apps/details?id=code.utakata.mide
Bug reports and "this didn't work on my phone" messages help the most right now. If you've run into any of these problems in your own projects, I'm curious how you dealt with them.
Flutter and Dart are trademarks of Google LLC. This project is not affiliated with or endorsed by Google.
Top comments (0)