28 How to Integrate Flutter Into Native Applications

添加Flutter依赖 #

首先,你需要在原生应用的项目中添加Flutter的依赖。Flutter提供了一个工具 flutter_module 来帮助你在已有的原生应用中引入和管理Flutter工程。

你可以通过在原生应用的 build.gradle 文件中添加以下配置来引入Flutter依赖:

dependencies {
    // ... 其他依赖
    implementation project(':flutter')
}

flutter 是Flutter工程的模块,你需要在原生应用的项目中添加它作为一个依赖。

创建Flutter模块 #

接下来,你需要在原生应用的项目中创建一个Flutter模块。这个模块将用于构建和管理Flutter工程。你可以使用下面的命令创建一个Flutter模块:

flutter create --org com.example --template=module flutter_module

其中,flutter_module 是你指定的Flutter模块名称。

这将在原生应用的项目目录下创建一个名为 flutter_module 的文件夹,用于存放Flutter工程的代码。

配置原生应用 #

接下来,你需要配置原生应用来引入Flutter模块。

首先,你需要在原生应用的 settings.gradle 文件中添加以下内容:

include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')

这会告诉原生应用项目去包含Flutter模块,并指定Flutter模块的路径。

然后,你需要在原生应用的 build.gradle 文件中添加以下配置:

android {
    // ... 其他配置

    sourceSets {
        main {
            // ... 其他源文件

            java.srcDirs += [
                'src/main/java', // 原生应用的java源文件目录
                '../flutter_module/android/src/main/java' // Flutter模块的java源文件目录
            ]
        }
    }
}

这将告诉原生应用项目去包含Flutter模块的java源文件。

启动Flutter引擎 #

现在,你可以在原生应用的代码中启动Flutter引擎,并跳转到Flutter界面了。

在原生应用的代码中,你可以使用以下代码启动Flutter引擎并跳转到Flutter界面:

FlutterEngine flutterEngine = new FlutterEngine(context);
flutterEngine.getDartExecutor().executeDartEntrypoint(
    DartExecutor.DartEntrypoint.createDefault()
);
FlutterActivity.launch(context, flutterEngine);

这将创建一个新的Flutter引擎,并启动Flutter界面。

通过这种方式,你可以在已有的原生应用中实现混编,使用Flutter来扩展原生应用的功能。

总结 #

通过以上步骤,你可以在原生应用中混编Flutter工程,并使用Flutter来扩展原生应用的能力。

尽管在接入过程中可能会遇到一些挑战,但通过逐步试验和有序推进,你将能够提升终端开发效率并享受Flutter的强大功能。

希望本文能对你在原生应用中接入Flutter有所帮助!如果你有任何问题,请随时向我提问。谢谢!

Preparation #

Since we are going to mix Flutter into a native application, I believe you have already prepared the native application project for today’s transformation. If you haven’t prepared yet, it’s okay. I will demonstrate the transformation process with a minimal example.

First, let’s quickly create a basic project with only a home page using Xcode and Android Studio. The project names will be iOSDemo and AndroidDemo respectively.

At this point, the Android project is already prepared. However, for the iOS project, since the basic project does not support project management in a modularized way, we need to do an extra step to transform it into a project managed by CocoaPods. This means we need to create a Podfile file with only basic information in the iOSDemo root directory:

   use_frameworks!
   platform :ios, '8.0'
   target 'iOSDemo' do
   #todo
   end

After that, enter pod install in the terminal. This will generate an iOSDemo.xcworkspace file, and we will have completed the transformation of the iOS project.

Introduction to Flutter Mixed Coding Solutions #

If you want to embed some Flutter pages in an existing native app, there are two ways to do it:

  • Treat the native project as a subproject of the Flutter project, managed by Flutter. This is called the unified management mode.
  • Treat the Flutter project as a shared submodule of the native project, maintaining the original native project management. This is called the three-end separation mode.

Figure 1 Flutter Mixed Coding Project Management Mode

Due to the limited capabilities and related information provided by Flutter in its early stages, most teams in China that started using Flutter hybrid development adopted the unified management mode. However, as the functionality evolved, the drawbacks of this approach became evident. Not only did it lead to tight coupling of code between the three ends (Android, iOS, and Flutter), but it also increased the time-consuming nature of the relevant toolchains, resulting in reduced development efficiency.

Therefore, teams that later adopted Flutter hybrid development have gradually transitioned to the three-end separation mode to manage dependencies more effectively, achieving lightweight integration of the Flutter project.

In addition to lightweight integration, the three-end separation mode treats the Flutter module as a sub-module of the native project, enabling rapid implementation of “hot swapping” of Flutter functionality and reducing the cost of modifying the native project. The Flutter project is managed via Android Studio, eliminating the need to open the native project and allowing direct development and debugging of Dart code and native code.

The key to the three-end separation mode is to abstract the Flutter project and manage the build artifacts of different platforms in a standardized component form, i.e., using aar for Android and pod for iOS. In other words, the mixed coding solution we will introduce next will package the Flutter module into aar and pod formats, enabling the native project to quickly integrate Flutter, just like referencing other third-party native component libraries.

Doesn’t that sound exciting? Next, let’s get started with the three-end separation mode to integrate the Flutter module formally.

Integrating Flutter #

I mentioned in earlier articles that the project structure of Flutter is quite special, including the directories for the Flutter project and the native project (i.e., the iOS and Android directories). In this case, the native project will depend on the Flutter-related libraries and resources, making it impossible to build and run independently without the parent directory.

The native project’s dependence on Flutter is divided into two main parts:

  • Flutter libraries and engine, which include the Flutter framework library and engine library;
  • Flutter project, which is the implementation of our own Flutter module functionality. This mainly includes the Dart code implementation in the lib directory of the Flutter project.

In the case where a native project already exists, we need to create a Flutter module at the same level directory to build the Flutter dependency libraries for both iOS and Android. This is easily accomplished as Flutter provides us with a command for it. We just need to execute the following Flutter command in the same level directory as the native project to create a module named flutter_library:

Flutter create -t module flutter_library

The Flutter module, which is the Flutter project, can be opened using Android Studio. Its directory structure is shown in the following image:

Figure 2 Flutter module project structure

As you can see, compared to a traditional Flutter project, the Flutter module project also contains embedded Android and iOS projects. Therefore, we can use Android Studio for development and debugging just like a regular project.

If you look closely, you will notice a subtle change in the Flutter module: there is a Flutter directory under the Android project, and the build.gradle configuration in this directory is used to build the aar package. This is the secret behind the module project being able to use Android Studio for development and debugging like a regular Flutter project while also being able to build the aar package and pod.

In fact, the directory structure of the iOS project also has subtle changes, but these differences do not affect the packaging and building process, so I won’t go into further detail.

Next, let’s open the main.dart file and update its logic with the following code, which creates a full-screen red Flutter widget with the text “Hello from Flutter”:

import 'package:flutter/material.dart';
import 'dart:ui';

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    default:
      return MaterialApp(
        home: Scaffold(
          backgroundColor: const Color(0xFFD63031),
          body: Center(
            child: Text(
              'Hello from Flutter',
              textDirection: TextDirection.ltr,
              style: TextStyle(
                fontSize: 20.0,
                color: Colors.blue,
              ),
            ),
          ),
        ),
      );
  }
}

Note: The widget we create is actually wrapped in a switch-case statement. This is because a packaged Flutter module usually has multiple page-level widgets, and the native app code will provide a route identifier to tell Flutter which widget to return. In this example, we ignore the route identifier and always return a MaterialApp to simplify the case.

Next, we need to compile and package this code, build the corresponding Android and iOS dependency libraries, and integrate them into the native project.

Now, let’s first see how to integrate the Android module.

Integrating the Android module #

As mentioned earlier, the native project’s dependency on Flutter on the Android platform can be divided into two parts:

  • Flutter libraries and engine, including icudtl.dat, libFlutter.so, and some class files. These files are packaged in Flutter.jar.
  • Flutter project artifacts, mainly including the isolate_snapshot_data, isolate_snapshot_instr, vm_snapshot_data, vm_snapshot_instr, and Flutter_assets resource files.

After understanding the Android build artifacts of the Flutter project, the steps to extract the Android Flutter dependency are as follows:

First, in the root directory of the flutter_library, execute the aar packaging and building command:

Flutter build apk --debug

This command compiles the project artifacts and packages Flutter.jar and the project artifacts into an aar file. You might quickly realize that for building release artifacts, you just need to replace debug with release.

Next, the packaged flutter-debug.aar is located under the .android/Flutter/build/outputs/aar/ directory. We copy it to the libs directory of the native Android project AndroidDemo and add its dependency in the build.gradle file of the app:

...
repositories {
    flatDir {
        dirs 'libs'
    }
}
android {
    ...
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
}
...

}

dependencies { … implementation(name: ‘flutter-debug’, ext: ‘aar’) // Flutter Module aar … }

After syncing, the Flutter module will be added to the Android project.

Next, let’s try modifying the code in MainActivity.java to change its contentView to a Flutter widget:

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); View FlutterView = Flutter.createView(this, getLifecycle(), “defaultRoute”); // Pass in the route identifier setContentView(FlutterView); // Replace the Activity’s ContentView with FlutterView }

Finally, click on run, and you will see a full-screen red Flutter widget that says “Hello from Flutter” displayed. With this, we have completed the integration of the Android project.

图2 Android工程接入示例

Figure 3: Example of Android project integration

Integration of iOS Module #

The integration of an iOS project is a bit more complicated. In an iOS project, the dependencies on Flutter are:

  • Flutter library and engine, i.e. Flutter.framework
  • The output of the Flutter project, i.e. App.framework

The extraction of the Flutter module on the iOS platform is essentially generating these two artifacts using the build command and packaging them into a pod for the native project to reference.

Similarly, first, we execute the iOS build command in the root directory of Flutter_library:

Flutter build ios –debug

This command compiles the Flutter project to generate the two artifacts: Flutter.framework and App.framework. By substituting “debug” with “release”, you can build the release artifacts (of course, you also need to address signing issues).

Then, in the root directory of iOSDemo, create a directory called FlutterEngine and copy these two framework files into it. The modularization of the iOS artifacts requires an additional step, as we need to manually package these two artifacts into a pod. Hence, we also need to create FlutterEngine.podspec in this directory, which defines the Flutter module component:

Pod::Spec.new do |s| s.name = ‘FlutterEngine’ s.version = ‘0.1.0’ s.summary = ‘XXXXXXX’ s.description = «-DESC TODO: Add long description of the pod here. DESC s.homepage = ‘https://github.com/xx/FlutterEngine' s.license = { :type => ‘MIT’, :file => ‘LICENSE’ } s.author = { ‘chenhang’ => ‘[email protected]’ } s.source = { :git => “”, :tag => “#{s.version}” } s.ios.deployment_target = ‘8.0’ s.ios.vendored_frameworks = ‘App.framework’, ‘Flutter.framework’ end

After running pod lib lint, the Flutter module component is ready. While we’re at it, let’s modify the Podfile file to integrate it into the iOSDemo project:

… target ‘iOSDemo’ do pod ‘FlutterEngine’, :path => ‘./’ end

Running pod install will then integrate the Flutter module into the iOS native project.

Once again, we try modifying the code in AppDelegate.m to change the window’s rootViewController to a FlutterViewController:

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; FlutterViewController *vc = [[FlutterViewController alloc]init]; [vc setInitialRoute:@“defaultRoute”]; // Route identifier self.window.rootViewController = vc; [self.window makeKeyAndVisible]; return YES; }

Finally, click on run, and you will see a full-screen red Flutter widget that says “Hello from Flutter” displayed. With this, we have successfully completed the integration of the iOS project.

图4 iOS工程接入示例

Figure 4: Example of iOS project integration

Summary #

By separating the Android, iOS, and Flutter projects, extracting the Flutter library, engine, and project code into a component library, and integrating them into the native project in the common aar and pod formats for Android and iOS platforms, we can easily integrate Flutter modules at a low cost and use Flutter to expand the boundaries of native apps happily.

However, we can do even better.

If we manually transfer the Flutter build artifacts every time we build the Flutter module project, it is easy for the Flutter component library to be overwritten due to messy project management, which can cause difficult-to-troubleshoot bugs. To solve this problem, we can introduce a CI automatic build framework to automate the build artifacts of Flutter, and the native project can integrate different versions of the build artifacts to achieve a more elegant three-platform separation mode.

As for automated build, I will provide a detailed introduction in later articles, so I won’t repeat it here.

Next, let’s briefly review today’s content.

There are two ways to mix Flutter into native projects. One is to embed the Flutter project into Android and iOS projects, using a centralized mode managed by Flutter; the other is to treat the Flutter project as a shared submodule of the native projects, using a three-platform separation mode managed separately by the native projects. Currently, the industry mostly adopts the second approach.

The main point of the three-platform separation mode is to extract the Flutter project, manage the build artifacts of different platforms in a standardized componentized manner, namely: generate aar for Android platform through build.gradle for dependency management and generate a framework for iOS platform, encapsulate it into an independent pod, and manage dependencies through the podfile.

I have packaged the knowledge points involved in today’s sharing into GitHub (flutter_module_page, iOS_demo, Android_Demo), you can download them, run them repeatedly, and deepen your understanding and memory.

Thought-provoking question #

Finally, I’ll leave you with a thought-provoking question.

What are the differences in the packaging and extraction process of a Flutter module project with resource dependencies and the process of extracting the Flutter component library?

Feel free to leave your thoughts in the comments section, and I’ll be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends for reading.