DEV Community

Cover image for How I got the Flutter toolchain building apps on an Android phone
Utakata Code
Utakata Code

Posted on AI-assisted

How I got the Flutter toolchain building apps on an Android phone

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_tools ships as a kernel snapshot,
  • the VM snapshots, ICU data, the patched SDK and sky_engine are 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_snapshot for release and profile builds,
  • impellerc (every app needs it, because the framework's own shaders are compiled with it),
  • font-subset,
  • flutter_tester for flutter 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:

  1. Shell scripts: bin/flutter and gradlew are scripts, and they can't be exec'd either. Instead of shipping them as fake .so files, the IDE starts them through /system/bin/sh. That needed small patches in two places: where flutter_tools launches gradlew, and where Flutter's Gradle plugin calls back into bin/flutter.
  2. Finding the SDK from the VM: the Dart VM uses /proc/self/exe to work out where it lives, and that points at …/lib/arm64/libmide_dart.so, not at the SDK. Tools like dart run and build_runner then can't find the SDK. A patch to the VM makes it return the path it was invoked with (taken from AT_EXECFN), but only when that path is the same file as /proc/self/exe. It touches nothing under runtime/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'
Enter fullscreen mode Exit fullscreen mode

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)