Flutter与Android混编
一.Flutter工程模式
flutter有四种工程模式:application、plugin、package、module
1.Flutter Application: Flutter应用
(1.)创建命令
1 | flutter create xxapp |
(2.)目录结构
1 | │ pubspec.lock |
- android:Android原生代码目录。
- ios:iOS原生代码目录。
- lib:这个是Flutter项目的核心目录,写的Flutter代码放在这个目录,也可以在这个目录创建子目录。
- test:测试代码目录。
- pubspec.yaml:这个是Flutter项目的依赖配置文件,类似于Android build.gradle文件,这里面包含了Flutter SDK 版本、依赖等。
(3.)说明
Flutter Application是一个标准的Flutter App项目工程,包含标准的Dart层与Native平台层代码,项目主体是Flutter, 其内部包含 Android 和 iOS、 Web 项目。
2.Flutter Plugin:Flutter插件
(1.)创建命令
1 | flutter create --template=plugin xxapp_plugin |
(2.)目录结构
1 | │ pubspec.lock |
(3.)说明
FlutterPlugin是Flutter 插件,包含Dart层与Native平台层的实现。Flutter Plugin提供Android或者iOS的底层封装,在Flutter层提供组件功能, 使Flutter可以方便的调取Native的模块。如果要开发一个 Plugin 且此 Plugin 涉及到原生支持,比如蓝牙功能、网络功能等,这些功能纯 Flutter 是无法实现得。 很多平台相关性或者对于Flutter实现起来比较复杂的部分,也都可以封装成Plugin。
3.Flutter Package:纯Dart组件
(1.)创建命令
1 | flutter create --template=package xxapp_package |
(2.)目录结构
1 | │ pubspec.lock |
(3.)说明
Flutter Package是纯 Flutter 模块,不需要原生开发,没有Android 和 iOS 项目,仅包含Dart层的实现,往往定义一些公共Widget。
4.Flutter Module :Flutter模块
(1.)创建命令
1 | flutter create --template module my_flutter |
(2.)目录结构
1 | │ pubspec.lock |
.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。
通过查看app的build.gradle文件可以知道是将Flutter变成moudle的形式依赖到Android项目中的:
1 | ... |
这种集成方式虽然比较方便, 但是在多人协作的app中, 尤其是对非flutter开发的同学来说, 是强依赖本地安装flutter环境的, 所以也是有缺点的。
二.Flutter和Native工程混编方案
1.混编方案
一般来说混编方案有以下两种:
(1.)统一管理方案(不推荐)
也就是Flutter Application创建工程的方案,目的是将iOS工程和Android工程作为Flutter工程的子工程,由Flutter统一管理。
优点:
- 代码集中,可以很方便的进行项目开发,每个开发都可以进行iOS、Android和Flutter的工程开发。
缺点:
- 对原有项目的侵入性太大,项目对外部环境的依赖程度增加。
- 每个开发人员本地都要装有自己端的开发环境(iOS/Android)和Flutter的开发环境,并且Flutter SDK版本要保持一致。
- 耦合度会越来越高。当项目越来越复杂后,整个项目的代码耦合度会越来越高,相关工具链耗时也会越来越长,导致开发效率降低。
(2.)各端分离方案(推荐)
各端分离方案是iOS、Android和Flutter分别作为三个独立项目存在,将Flutter工程的编译产物作为iOS工程和Android工程的依赖模块,原有工程的管理模式不变,对原生工程没有侵入性,无需额外配置工作。 这种方案需要单独创建Flutter项目,然后通过iOS(CocoaPods)和安卓的依赖管理工具将Flutter项目build出来的framework、资源包等放入Native工程以供使用。 这种方式可以将iOS、Android和Flutter项目放在一个目录下面作为一个项目来管理,也可以不在同一目录下,关键是设置Flutter模块依赖时相对路径一定要设置正确。
通过Flutter Module创建工程的方案就可以完成这种混编。
2.混编方案总结
官方也比较推荐各端分离方案这种混合开发解决方案,并且推出了Flutter Module工程方便,可将flutter作为module让原生native工程直接依赖。
三.Android项目引入Flutter模块
在上文中得出Flutter和Native工程混编采用各端分离方案比较合理,下面来研究一下如果将Flutter作用一个单独的模块如何引入Android项目工程。
Flutter 可以作为 Gradle 子项目源码或者 AAR 嵌入到现有的 Android 应用程序中。因此Android 原生项目中接入Flutter项目一般有两种方式:
- 将 Flutter 打包成 AAR 包,然后在现有项目引入。
- 将 Flutter 源码视为 Gradle Module 引入现有项目。
目前现有的 Android 项目可能支持 mips 或 x86 之类的架构,然而,Flutter 当前仅支持 为 x86_64,armeabi-v7a 和 arm64-v8a 构建预编(AOT)的库。 可以考虑使用 abiFilters 这个 Android Gradle 插件 API 来指定 APK 中支持的架构,从而避免 libflutter.so 无法生成而导致应用运行时崩溃
四.通过AAR的方式引入Flutter
1.创建Flutter module工程
这种方式需要配置好Flutter的开发环境. 有两种方式:
- 通过命令行生成Flutter module
- 可以通过Android Studio或者Vscode安装对应的插件创建工程。
(1.)命令行生成Flutter module集成项目
在Android工程中集成flutter,要保证 将要使用的Flutter Module工程与原生Android工程 在同一根目录下 , Terminal进入到项目根目录, 执行以下命令:
1 | flutter create -t module --org {package_name} {module_name} |
执行完成后会在Android的同一级目录下生成一个新的 flutter_module文件夹。这里在项目根目录创建子 module 只是为了把代码放在一个仓库,方便维护,理论上可以放在硬盘的任何位置。
(2.)通过Android studio创建Flutter module
- 通过Android studio创建module
- new->new module择flutter module
- 根据提示指定创建路径后,指定目录就可以看见刚刚创建的module
Flutter module模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/。 .android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。 注意:
- 将自己的 Android 代码添加到现有应用程序的项目或插件中,而不是添加到 .android/ 中的模块。在模块的 .android/ 目录中所做的任何更改并不会显示在使用该模块的现有 Android 项目中。
- 由于 .android/ 目录是自动生成的,因此不需要对它的代码进行版本控制,在新机器上构建模块之前,可以先在 my_flutter 目录中运行 flutter pub get 来重新生成 .android/ 目录,然后再使用 Flutter 模块构建 Android 项目。
2.编译打包AAR
可以将Flutter模块打成Android AAR 作为依赖媒介引入到Android应用程序中,这样Android应用开发不用安装Flutter SDK也可以.缺点是如果Flutter需要经常修改就需要每次都需要重新编译一次打包AAR进行引入。这种方式会将 Flutter 库打包成由 AAR 和 POM artifacts 组成的本地 Maven 存储库。这种方案可以使你的团队不需要安装 Flutter SDK 即可编译宿主应用。之后,也可以从本地或远程存储库中分发更新AAR。
假设你在 some/path/my_flutter 下构建 Flutter 模块,执行如下命令:
1 | content_copy |
执行打包命令flutter build aar
,这个打包AAR方式会包含多种abi,如果想要指定abi可以使用 flutter build aar --target-platform xxx
平台。
执行成功以后会输出以下信息。也就是说build以后会生成本地的maven仓库,仓库位于build->host->outputs->repo内,将这个仓库配置到需要混编的Android项目的gradle就可以完成混编。
1 | 1. Open <host>\app\build.gradle |
flutter build aar
该命令主要用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件:
build/host/outputs/repo
└── com
└── example
└── my_flutter
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── …
└── flutter_debug
└── …
3.Android工程引入AAR
可以发现,使用上面的命令编译的AAR包主要分为debug、profile和release三个版本,使用哪个版本的AAR需要根据原生的环境进行选择。找到AAR包,然后再Android宿主应用程序中修改 app/build.gradle 文件, 使其包含本地存储库和上述依赖项。
1 | android { |
注意,不要将编译好的aar直接copy到项目中去,必须要通过maven坐标依赖才能将完整的Flutter模块引入。
如果需要打不同版本的AAR在构建命令后面加上 --build-number [版本号]
;
例如:
1 | flutter build aar --build-number 1.1 |
4.AAR上传Maven仓库
如果团队开发时,这种本地Maven仓库引用aar的方式就不方便了,毕竟本地仓库路径都不一样。可以先分析通过flutter build aar是如何生成本地的maven仓库的:
//执行-v命令,查看命令输出信息
1 | flutter build aar -v |
可以发现,整个flutter build aar命令中,其实就执行了这三个Gradle任务assembleAarDebug、assembleAarProfile、assembleAarRelease
而这三个任务都执行了flutter\packages\flutter_tools\gradle\aar_init_script.gradle
这个gradle脚本。
查看aar_init_script.gradle文件:
1 | // This script is used to initialize the build in a module or plugin project. |
它在指定的目录下,生成了一个maven本地仓库,并将aar上传到了这里
。 因此如果需要将aar上传到自己的私有maven仓库中,只需要把repository中的地址改成自己的maven私服地址即可。
例如
1 | ... |
也可以通过shell脚本来实现动态的修改:
5.AAR的方式的优缺点
优点:
- 不开发flutter的同学无需安装环境与工程,方便分离开发。
- 更有利于组件化开展。
缺点:
- 调试包与正式包都需要单独编译,测试较为麻烦。
- 上线前需要主动更新版本号。
五.依赖模块的源码引入Flutter
1.创建Flutter module工程
和上文中一致,参考上文。
2.settings.gradle配置
在settings.gradle里面配置如下代码:
// 加入下面配置
1 | setBinding(new Binding([gradle: this])) |
如果主module名称不是app,而是改成了其他的名称:,则还需要在project级别的gradle.properties中添加如下代码:
1 | flutter.hostAppProjectName = app_module_name |
3.依赖flutter模块
在app主模块的build.gradle的dependencies中加入依赖库
1 | implementation project(':flutter') |
现在的原生项目就包含 Flutter SDK 的所有依赖了。 Flutter UI 相关的内容,还是用 dart 在 main.dart 中写, 然后可以把这个 dart 渲染出来的内容按照 Activity 、Fragment 或 View 的形式添加到已有的项目中了。
4.Flutter Module方式的优缺点
优点
方便快捷,所见即所得,不用频繁发布。
缺点
- 需要Android开发者配置flutter环境,且会增加构建时间。
- Android Studio 项目结构上会出现很多flutter依赖的模块,使项目视图变得杂乱。
- 需要团队每个人都要安装flutter 环境,下载sdk等,否则无法编译项目。
六.Android启动Flutter页面
1.Android启动Flutter页面
有两种方式:
- 直接启动一个FlutterActivity
- 启动复写后的FlutterActivity(推荐)
(1.)直接启动一个FlutterActivity
//在清单配置文件中注册activity
1 | <activity |
//跳转到flutter页面
1 | startActivity( |
(2.)启动复写后的FlutterActivity(推荐)
自己创建一个FlutterAppActivity继承自FlutterActivity.
1 | public class MainActivity extends FlutterActivity { |
(3.)两种启动方式的区别
如果单纯只是想打开一个Flutter页面,两种方式实际上基本没有太大区别,第一种方式也许还会更简单一点。 但是,在Flutter开发中,往往还需要开发一些Native插件供Flutter调用,如果使用复写FlutterActivity的方式更有利于在FlutterActivity中注册的Native插件, 所以实际开发中一般推荐使用第二种方式.
2.Android跳转到Flutter指定页面
上文中的跳转是假定 Dart 代码入口是调用 main(),并且Flutter 初始路由是 ‘/’。 Dart 代码入口不能通过 Intent 改变,但是初始路由可以通过 Intent 来修改。Android跳转到Flutter指定页面是通过路由来跳转的,需要先声明路由;
在Flutter项目工程的MeterialApp下声明路由;并创建ImagePage()和TextPage()页面:
1 | MaterialApp( |
Android部分通过指定路由跳转:
1 | /** |
3.缓存FlutterEngine(可选)
默认情况下,每一个FlutterActivity都会创建自己的FlutterEngine对象,每一个FlutterEngine都需要加载耗时,也就是说打开每一个标准的FlutterActivity时都会有一个延迟。 为了减少这个延迟,可以在打开FlutterActivity前预加载FlutterEngine,需要使用的时候直接使用预加载好的FlutterEngine。
需要找一个合适的地方来预加载FlutterEngine,下面的示例在Application代码中进行预加载:
1 | public class MyApplication extends Application { |
传递给FlutterEngineCache的id可以是任意值,只要确保和在FlutterActivity和FlutterFragment中取出FlutterEngine时使用的一致即可。要使用预热且缓存的 FlutterEngine 时,让你的 FlutterActivity 从缓存中获取 FlutterEngine,而不是创建一个新的。可以使用 FlutterActivity 的 withCachedEngine() 方法来实现:
1 | myButton.addOnClickListener(new OnClickListener() { |
当使用withCachedEngine方法时,注意id要和预加载FlutterEngine时保持一致。现在再打开FlutterActivity时,延迟问题就好很多了。
当使用一个缓存的 FlutterEngine 时, FlutterEngine 会比展示它的 FlutterActivity 或 FlutterFragment 存活得更久。切记,Dart 代码会在你预热 FlutterEngine 时就开始执行,并且在你的 FlutterActivity 或 FlutterFragment 销毁后继续运行。要停止代码运行和清理相关资源,可以从 FlutterEngineCache 中获取你的 FlutterEngine,然后使用 FlutterEngine.destroy() 来销毁 FlutterEngine。
七.通过FlutterFragment添加Flutter界面
1.FlutterFragment启动Flutter页面
1 | class MainActivity : AppCompatActivity() { |
2.FlutterFragment生命周期
FlutterFragment它不支持生命周期管理,需要借助Activity:
1 | public class MyActivity extends FragmentActivity { |
3.FlutterFragment使用预热的 FlutterEngine
默认情况下,FlutterFragment 会创建它自己的 FlutterEngine 实例,同时也需要不少的启动时间。这就意味着您的用户会看到短暂的白屏。通过使用已存在的、预热的 FlutterEngine 就可以大幅度减少启动的耗时。 要在 FlutterFragment 中使用预热 FlutterEngine,可以使用工厂方法 withCachedEngine() 实例化 FlutterFragment。
1 | // Somewhere in your app, before your FlutterFragment is needed, |
1 | FlutterFragment.withCachedEngine("my_engine_id").build(); |
FlutterFragment 内部可访问 FlutterEngineCache,并且可以根据传递给 withCachedEngine() 的 ID 获取预热的 FlutterEngine。 如上所示,通过提供预热的 FlutterEngine,您的应用将以最快速度渲染出第一帧。
4.FlutterFragment缓存引擎中的初始路由
当配置一个使用新 FlutterEngine 的 FlutterActivity 或者 FlutterFragment 时,会使用到初始路由的概念。但是,使用缓存中的 Flutter 引擎时, FlutterActivity 或者 FlutterFragment 则没有涉及初始路由的概念。这是因为被缓存的引擎理论上已经执行了 Dart 代码,在这时配置初始路由已经太迟了。
开发者如果想要让缓存中的引擎从自定义的初始路由开始运行,那么可以执行 Dart 入口前,为缓存的 FlutterEngine 配置自定义的初始路由。如下面这个例子:
1 | public class MyApplication extends Application { |
通过设置导航通道中的初始路由,会让关联的 FlutterEngine 在 runApp() 方法首次执行后,展示已配置的路由页面。在 runApp() 的首次执行之后,修改导航通道中的初始路由属性是不会生效的。想要在不同的 Activity 和 Fragment 之间使用同一个 FlutterEngine,并且在其展示时切换不同的路由,开发者需要设置一个方法通道,来显式地通知他们的 Dart 代码切换 Navigator 路由。
八.Flutter与Android原生交互
1.在Flutter中调用原生方法,并获取返回值
(1.)Flutter端代码
首先创建一个MethodChannel对象,它接受一个String作为参数,String可以为任意字符串,这个String的作用是用来和原生中注册的MethodChannel进行匹配,只有双方的值一致时,这个通道才起作用。
1 | static const platform = const MethodChannel('samples.flutter.dev/battery'); |
然后通过MethodChannel的invokeMethod()方法调用原生,这个方法接受两个参数,第一个参数为标识,也就是原生内边的方法名字,第二个参数就是需要向原生传递的参数,可以是一个map或者jsonString,我这没有需要向原生传递的参数,所以就没填。
1 | Future<void> _getBatteryLevel() async { |
(2.)Android端代码
Fluttrt的代码写完后,接着实现原生端的代码,首先需要在原生注册MethodChannel,如果你用的Activity,那么就继承FlutterActivity,如果用的Fragment,那么就继承FlutterFragment, 然后需要在Activity或者Fragment中实例化一下这个通道。常用的做法是在 configureFlutterEngine 这个方法中实例化通道就行了,有多少个通道,就在这里实例化多少个通道。
值得注意:不管执行结果如何都要执行result.success或者result.error,否则Flutter端可能会一直等待响应
1 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { |
注意,第二个参数的值要和Flutter中MethodChannel的值一致。
在Android实例化了一个MethodChannel对象,并调用了setMethodCallHandler方法进行注册。这个setMethodCallHandler方法需要实现一个接口参数,这个接口为MethodChannel.MethodCallHandler,看一下这个接口。
1 | public interface MethodCallHandler { |
这个接口只有一个onMethodCall方法,并且有两个参数,MethodCall和Result,其实最终就是通过这两个参数来与Flutter进行通信。Result,为一个接口,当Android处理完Flutter调用的方法逻辑后需要向Flutter回传一些数据时,就通过Result来实现。
2.在Android中调用Flutter方法
Android调用Flutter,更简单,只是需要注意线程问题,UI相关就放主线程执行,耗时操作放子线程然后event更新即可.
例如
- Android端调用location方法并传参给Flutter,当然不传参数不加参数,也不取参数就行了,这里就写个带参数的
(1.)Flutter端代码
例如用的StatefulWidget,在initState方法中给它加个接收消息
1 | @override |
(2.)Android端代码
1 | class MainActivity: FlutterActivity() { |
九.Flutter混合栈管理
Flutter 整个项目都是绘制在一个 Surface 画布上,而fluttet_boost 将栈统一到了原生层,通过一个单例的 flutter engine 进行绘制。每个 FlutterFragment 和 FlutterActivity 都是一个 Surface承载容器, 切换页面时就是切换 Surface 渲染显示,而对于不渲染的页面通过 Surface 截图缓存画面显示。 这样整个 Flutter 的路由就被映射到原生栈中,统一由原生页面栈管理,Flutter 内每 push 一个页面就是打开一个 Activity 。 一般情况下都会避免混合栈的相互调用 ,但是面对不得不如此为之的情况下,还是会出现问题。
1.Google官方(多引擎方案)
即每次使用一个新的FlutterEngine来渲染Widget树。虽然Flutter 2.0之后的创建FlutterEngine的开销大大降低,但是依然没有解决每个FlutterEngine是一个单独isolate。
2.flutter_boost(单引擎方案)
flutter_boost阿里巴巴闲鱼团队出品的一款解决Flutter栈管理的框架
参考资料:
Flutter 开发文档
Flutter 开发文档(英文版)
恋猫小郭Flutter合集
android调用flutter aar_在原生项目中集成Flutter
Flutter文章中文版
Android 项目集成 Flutter
在 Flutter 中使用原生控件
详解Flutter工程创建的几种模式
Flutter混合开发组件化与工程化架构
Flutter: 以 aar 形式嵌入 android 中的方案和原理
Flutter打包AAR插件之fat-aar使用教程
优雅地将Flutter项目打包aar上传maven
flutter打包aar并上传Maven私服,flutter模块与原生工程解耦
flutter工程化踩坑实录
Android Native 工程集成 Flutter 的两种方式
Flutter 工程化搭建(Android端)
给现有 App 引入 Flutter Module
开门见山——Flutter 自动打包aars 并上传Maven仓库
Flutter 2.0 下混合开发浅析