Mobile Development 18 min read

Unlocking Flutter 2 Router: Deep Dive into Implementation and Source Code

This article guides developers through compiling, debugging, and analyzing the Flutter 2 Router source code, covering engine setup, architecture layers, key initialization classes, and practical steps to run a custom Flutter engine on Android and iOS.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Unlocking Flutter 2 Router: Deep Dive into Implementation and Source Code

Flutter 2 Router: From Basics to Deep Source Analysis (Part 1)

Zhou Jianhua: Mobile medical diagnosis team, Android developer who loves reading and sports.

Preface

In the previous article we covered the basic usage of multi‑engine mixed development and the differences between multi‑engine and single‑engine approaches. This article focuses on how multi‑engine reuse is implemented by examining the source code.

1. Flutter 2 Source Compilation and Debugging

Source Compilation

First install depot_tools and add it to the PATH.

<code>git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
</code>

Create an empty engine directory and a .gclient configuration file that points to a fork of the flutter/engine repository.

<code>solutions = [
  {
    "managed": False,
    "name": "src/flutter",
    "url": "https://github.com/Alex0605/engine.git",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]
</code>

Run gclient sync inside the engine directory.

Switch the source to the engine version that matches the local Flutter SDK commit:

<code># View the engine version used by the local Flutter SDK
vim src/flutter/bin/internal/engine.version

# Adjust the code
cd engine/src/flutter
git reset --hard <commit id>
gclient sync -D --with_branch_heads --with_tags
</code>

Generate build configurations for Android and iOS, then compile:

<code># Android
./flutter/tools/gn --unoptimized
./flutter/tools/gn --android --unoptimized
./flutter/tools/gn --android --unoptimized --runtime-mode=debug --android-cpu=arm64
ninja -C out/host_debug_unopt -j 16
ninja -C out/android_debug_unopt -j 16
ninja -C out/android_debug_unopt_arm64 -j 16

# iOS
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64
ninja -C out/ios_debug_unopt_arm
ninja -C out/ios_debug_unopt
ninja -C out/host_debug_unopt_arm
ninja -C out/host_debug_unopt
</code>

After compilation the output directories look like this:

Source Runtime Debugging

Create a new Flutter project:

flutter create --org com.wedotor.flutter source_code

Open the generated Android project in Android Studio.

Add the localEngineOut property to gradle.properties :

<code>org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJitfier=true
localEngineOut=/Users/zhoujh/myproj/3-proj/flutter/engine/src/out/android_debug_unopt_arm64
</code>

Import the engine source directory

engine/src/flutter/shell/platform/android

(the *Flutter Engine project*) into Android Studio.

Run a Flutter App using the custom engine (the *Flutter App project*) and set breakpoints in the engine project to debug the running process.

For C++ debugging, copy the generated

compile_commands.json

into

src/flutter

and open the project with CLion.

2. Flutter 2 Source Reading

The official architecture diagram (shown below) represents the three layers of the Flutter architecture:

Flutter architecture diagram
Flutter architecture diagram

Framework : Implemented in Dart, includes Material and Cupertino widgets, basic widgets, rendering, animation, and gesture handling. Core code lives in the

flutter

package and the

sky_engine

package (e.g.,

dart:ui

bindings to Skia).

Engine : Implemented in C++, comprises Skia, the Dart runtime, and text handling. Skia provides a cross‑platform 2D graphics API; the Dart VM can run in JIT, JIT‑snapshot, or AOT mode.

Embedder : The platform‑specific layer that embeds Flutter into Android, iOS, etc., handling surface setup, threading, and plugins. This low‑level layer gives Flutter its cross‑platform consistency.

App Startup and FlutterEngineGroup

When the app starts,

FlutterEngineGroup

is created in

Application.onCreate

:

<code>public void onCreate() {
    super.onCreate();
    // Create FlutterEngineGroup object
    engineGroup = new FlutterEngineGroup(this);
}
</code>
FlutterEngineGroup

allows sub‑engines to share resources, resulting in faster creation and lower memory usage compared with creating separate

FlutterEngine

instances.

<code>public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {
    FlutterLoader loader = FlutterInjector.instance().flutterLoader();
    if (!loader.initialized()) {
        loader.startInitialization(context.getApplicationContext());
        loader.ensureInitializationComplete(context, dartVmArgs);
    }
}
</code>

FlutterLoader.startInitialization

This method loads

flutter.so

, extracts Dart assets, and prepares VSync handling. Key steps include:

Check whether settings have been assigned.

Must run on the main thread.

Obtain the application context.

Initialize

VsyncWaiter

.

Record initialization time.

Run heavy I/O (resource extraction, library loading) on a background thread.

<code>public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
    if (this.settings != null) return;
    if (Looper.myLooper() != Looper.getMainLooper()) {
        throw new IllegalStateException("startInitialization must be called on the main thread");
    }
    final Context appContext = applicationContext.getApplicationContext();
    this.settings = settings;
    initStartTimestampMillis = SystemClock.uptimeMillis();
    flutterApplicationInfo = ApplicationInfoLoader.load(appContext);
    VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)).init();
    Callable<InitResult> initTask = new Callable<InitResult>() {
        @Override
        public InitResult call() {
            ResourceExtractor resourceExtractor = initResources(appContext);
            flutterJNI.loadLibrary();
            Executors.newSingleThreadExecutor().execute(new Runnable() {
                @Override
                public void run() {
                    flutterJNI.prefetchDefaultFontManager();
                }
            });
            if (resourceExtractor != null) {
                resourceExtractor.waitForCompletion();
            }
            return new InitResult(PathUtils.getFilesDir(appContext),
                                  PathUtils.getCacheDirectory(appContext),
                                  PathUtils.getDataDirectory(appContext));
        }
    };
    initResultFuture = Executors.newSingleThreadExecutor().submit(initTask);
}
</code>

initResources

Copies assets from the APK to the app’s local storage (e.g.,

vm_snapshot_data

,

isolate_snapshot_data

,

kernel_blob.bin

) when in DEBUG or JIT_RELEASE mode.

<code>private ResourceExtractor initResources(@NonNull Context applicationContext) {
    ResourceExtractor resourceExtractor = null;
    if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
        final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
        final String packageName = applicationContext.getPackageName();
        final PackageManager packageManager = applicationContext.getPackageManager();
        final AssetManager assetManager = applicationContext.getResources().getAssets();
        resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);
        resourceExtractor
            .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData))
            .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData))
            .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));
        resourceExtractor.start();
    }
    return resourceExtractor;
}
</code>

ensureInitializationComplete

Verifies that initialization has finished and passes the library path to

FlutterJNI

via

shellArgs

. This method must also run on the main thread.

<code>public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
    if (initialized) return;
    if (Looper.myLooper() != Looper.getMainLooper()) {
        throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
    }
    if (settings == null) {
        throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
    }
    try {
        InitResult result = initResultFuture.get();
        List<String> shellArgs = new ArrayList<>();
        // ... configure shellArgs ...
        long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
        flutterJNI.init(applicationContext, shellArgs.toArray(new String[0]), kernelPath,
                      result.appStoragePath, result.engineCachesPath, initTimeMillis);
        initialized = true;
    } catch (Exception e) {
        Log.e(TAG, "Flutter initialization failed.", e);
        throw new RuntimeException(e);
    }
}
</code>

FlutterJNI.init

<code>public void init(@NonNull Context context,
                @NonNull String[] args,
                @Nullable String bundlePath,
                @NonNull String appStoragePath,
                @NonNull String engineCachesPath,
                long initTimeMillis) {
    if (FlutterJNI.initCalled) {
        Log.w(TAG, "FlutterJNI.init called more than once");
    }
    FlutterJNI.nativeInit(context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis);
    FlutterJNI.initCalled = true;
}
</code>

Loading flutter.so and JNI_OnLoad

After resources are ready,

flutter.so

is loaded by the Android VM. The first native entry point is

JNI_OnLoad

, which registers

FlutterMain

,

PlatformView

, and

VSyncWaiter

.

<code>JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    fml::jni::InitJavaVM(vm);
    JNIEnv* env = fml::jni::AttachCurrentThread();
    bool result = false;
    result = flutter::FlutterMain::Register(env);
    FML_CHECK(result);
    result = flutter::PlatformViewAndroid::Register(env);
    FML_CHECK(result);
    result = flutter::VsyncWaiterAndroid::Register(env);
    FML_CHECK(result);
    return JNI_VERSION_1_4;
}
</code>

FlutterMain::Init

Parses the

args

array into a

Settings

object, sets up paths for snapshots, caches, and registers callbacks for task observation and logging.

<code>void FlutterMain::Init(JNIEnv* env,
                       jclass clazz,
                       jobject context,
                       jobjectArray jargs,
                       jstring kernelPath,
                       jstring appStoragePath,
                       jstring engineCachesPath,
                       jlong initTimeMillis) {
    std::vector<std::string> args;
    args.push_back("flutter");
    for (auto& arg : fml::jni::StringArrayToVector(env, jargs)) {
        args.push_back(std::move(arg));
    }
    auto command_line = fml::CommandLineFromIterators(args.begin(), args.end());
    auto settings = SettingsFromCommandLine(command_line);
    // ... set timestamps, cache paths, load snapshots, etc. ...
    g_flutter_main.reset(new FlutterMain(std::move(settings)));
    g_flutter_main->SetupObservatoryUriCallback(env);
}
</code>

Postscript

The above mainly covers the initialization process of FlutterEngineGroup in Flutter 2. In the next section we will explore how to create FlutterEngine instances from the group and bind UI pages.

fluttermobile developmentiOSAndroidRouterEngineSource Code
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.