26

集成 Flutter 到现有项目,并实现使用单个 FlutterEngine 管理多个入口

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ%3D%3D&%3Bmid=2247491271&%3Bidx=1&%3Bsn=01fc532805ccb6eeb6905789d846b78f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

code小生 一个专注大前端领域的技术平台 公众号回复 Android 加入安卓技术群

作者: DoctorCat2020

链接: https://www.jianshu.com/p/0b6674c3a3ad

声明:本文已获 DoctorCat2020 授权发表,转发等请联系原作者授权

文章示例代码已上传 GitHub,地址: https://github.com/chenglei1986/reuse_a_flutter_engine_across_screens

1 准备工作

集成 Flutter 到现有项目具体步骤请参考官方文档。

如果你的项目是基于 Flutter stable 分支(在这篇文章完成当时的版本为 1.17.1-stable),那么 Android 项目 Application 模块的文件夹名称必须为 app,否则编译报错。

如果有一个工程下有多个 Application 模块,那么请将 Flutter 切换到 dev 或 master 分支,并在 gradle.properties 文件中加入

# 指定 app 模块的目录名称为 xxx
flutter.hostAppProjectName=xxx

然后再参考 Adding a Flutter screen to an Android app,编写 Android 端代码。

完成 Step 1 和 Step 2 就能运行 Flutter 代码了,只是每次启动都很慢,而且 Activity 切换动画会有肉眼可见的黑色背景,纯 Flutter 项目可以将启动页的 windowBackground 设置成启动图来提升用户体验,对于混合项目没有帮助。

此时再看 Step 3,翻一下文档相应部分如下:

默认情况下,每个 FlutterActivity 都会创建一个自己的 FlutterEngine。而每个 FlutterEngine 都会有一个明显的 “预热” 时间。这就意味着启动标准的 FlutterActivity ,在 Flutter 界面显示之前,将会有一个短暂的延迟。为了将这个延迟最小化,你可以在启动 FlutterActivity 之前对 FlutterEngine 进行预加载,之后使用这个预加载的 FlutterEngine 即可。

框架提供了方法,可以将 FlutterEngine 缓存起来,这样每次启动 FlutterActivity 都是“热启动”,用户体验上与原生基本没有差别。

至此,对于文档中提及的混合开发方案中的所有步骤都已经完成。此时我们还面临一个最大的问题就是,缓存了 FlutterEngine 之后,无法指定入口,这一点文档也有提到,大意就是:

FlutterEngine 是独立于 FlutterActivity 的,FlutterEngine 会在预热的时候就执行一部分 Dart 代码,如果等到 FlutterActivity 启动的时候再指定入口就晚了。

2 解决多个 Flutter 页面的入口问题

Flutter 混合开发,一般都是使用 Flutter 来开发部分模块,所以原生界面启动在前。除非所有使用 Flutter 开发的模块都从同一个入口进入,否则多入口是无法逃避的问题。

上面提到 FlutterEngine 会在预热的时候就开始执行 Dart 代码,这就意味着,同一个 FlutterEngine 只能指定一个入口,所以我自己在做项目的时候最先想到的是,我可以缓存多个 FlutterEngine 啊。

2.1 一次预热多个 FlutterEngine

核心代码如下:

/**
* 初始化 FlutterEngine
*
* @param context 上下文
* @param routes 路由名称列表
*/

void initFlutterEngine(Context context, List<String> routes) {
for (String route : routes) {
FlutterEngine flutterEngine = new FlutterEngine(context, null, false);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
// 使用路由名称作为 engineId
FlutterEngineCache.getInstance().put(route, flutterEngine);
}
}

这个方案可以解决问题,但是面临最大的风险与挑战就是,每初始化一个 FlutterEngine 都要消耗相应的内存。

根据文档,在 Flutter v1.10.3 版本上使用 2015 年的低端手机测试,Android 系统中每加载一个 FlutterEngine 需要 42 MB 内存,渲染首页需要约 12MB 内存。随着版本的升级,预计消耗内存会更多。

随着项目的迭代升级,功能模块越来越多,这个方案的风险就越大。

2.2 复用 FlutterEngine

2.2.1 改造 Flutter 入口代码

一个典型的 Flutter 程序如下

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Default Home Page'),
),
body: Container(),
);
}
}

其中 MyApp 是继承自 StatelessWidget,所以是无法改变状态的,我们要进行如下改造:

void main() => runApp(MyApp());

/// 继承自 StatefulWidget
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

/// 实例化一个 MethodChannel
MethodChannel _methodChannel = MethodChannel('com.example/method_channel');
/// Flutter 页面入口
Widget _initRoute = DefaultHomePage();

@override
void initState() {
super.initState();
// 设置 MethodCallHandler 接收来自 Android 的消息
_methodChannel.setMethodCallHandler((call) async {
switch (call.method) {
case 'setInitRoute':
_handleInitRouteMethodCall(call);
break;
}
});
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// home 设置成变量,当接收到 Android 发过来的路由时,
// 将 home 修改以实现入口的切换
home: _initRoute,
);
}
/// 处理来自 setInitRoute 的消息
void _handleInitRouteMethodCall(MethodCall call) async {
switch (call.arguments) {
case '/page_a':
_initRoute = PageA();
break;
case '/page_b':
_initRoute = PageB();
break;
default:
_initRoute = DefaultHomePage();
break;
}
/// 更新界面
setState(() {});
}
}
/// 默认入口,空白即可
class DefaultHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 因为是路由栈的最底层,所以不会自动处理返回,我们要自己处理
leading: GestureDetector(
child: Icon(Icons.arrow_back),
// 退出 FlutterActivity,iOS 不支持,要单独处理
onTap: () => SystemNavigator.pop(),
),
title: Text('Default Home Page'),
),
body: Container(),
);
}
}
/// page_a.dart
class PageA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: GestureDetector(
child: Icon(Icons.arrow_back),
onTap: () => onBackPressed(context),
),
title: Text('Page A'),
),
body: Container(),
);
}

void onBackPressed(BuildContext context) {
NavigatorState navigatorState = Navigator.of(context);
if (navigatorState.canPop()) {
navigatorState.pop();
} else {
SystemNavigator.pop();
}
}
}
/// page_b.dart
class PageB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: GestureDetector(
child: Icon(Icons.arrow_back),
onTap: () => onBackPressed(context),
),
title: Text('Page B'),
),
body: Container(),
);
}

void onBackPressed(BuildContext context) {
NavigatorState navigatorState = Navigator.of(context);
if (navigatorState.canPop()) {
navigatorState.pop();
} else {
SystemNavigator.pop();
}
}
}

主要思路是把 MyApp 改成 StatefulWidget,同时创建一个 MethodChannel 用于接收原生 App 发送的消息,当启动 FlutterActivity 时,使用该 MethodChannel 将要进入的界面路由发送过来,然后将 DefaultHomePage 替换掉。

为此我们还要自定义一个 FlutterActivity。

2.2.2 改造 Android 端代码

public class AndroidFlutterActivity extends FlutterActivity {

static final String EXTRA_CACHED_ENGINE_ID = "cached_engine_id";
static final String EXTRA_ROUTE = "extra_route";
static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity";

public static void open(Context context, String route) {
Intent intent = new Intent(context, AndroidFlutterActivity.class)
.putExtra(EXTRA_CACHED_ENGINE_ID, "default_engine_id")
.putExtra(EXTRA_ROUTE, route)
// Activity 销毁时保留 FlutterEngine
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false);
context.startActivity(intent);
}

@Override
public void onFlutterUiDisplayed() {
super.onFlutterUiDisplayed();

// 设置 Flutter 界面入口,注意不要在 onCreate 方法中调用,否则
// Flutter 入口不会更新。
String route = getIntent().getStringExtra(EXTRA_ROUTE);
FlutterTools.setInitRoute(route);
}
}

需要注意得是,AndroidFlutterActivity 向 Flutter 发送入口路由的时机。因为 FlutterEngine 在预加载的时候并不会执行 Flutter 首页的全部代码,即在界面展示出来之前,Widget 的 build 方法不会执行,所以如果在 AndroidFlutterActivity onCreate 方法中向 Flutter 发送消息,虽然能够收到,但是时机过早,当 Flutter 页面真正展示之后,还是会展示 DefaultHomePage。

public class FlutterTools {

public static final String ENGINE_ID = "default_engine_id";

public static final String ROUTE_PAGE_A = "/page_a";
public static final String ROUTE_PAGE_B = "/page_b";

private static final String METHOD_CHANNEL = "com.example/method_channel";

private static FlutterEngine sFlutterEngine;
private static MethodChannel sMethodChannel;

public static void preWarmFlutterEngine(Context context) {
if (null == sFlutterEngine) {
sFlutterEngine = new FlutterEngine(context);
sFlutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
sMethodChannel = new MethodChannel(sFlutterEngine.getDartExecutor(), METHOD_CHANNEL);
FlutterEngineCache.getInstance().put(ENGINE_ID, sFlutterEngine);
}
}

public static void setInitRoute(String route) {
sMethodChannel.invokeMethod("setInitRoute", route);
}

public static void destroyEngine() {
if (sFlutterEngine != null) {
sFlutterEngine.destroy();
}
}

}

原生 App 入口

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(findViewById(R.id.tool_bar));
initViews();

// 预加载 FlutterEngine
FlutterTools.preWarmFlutterEngine(this);
}

private void initViews() {
findViewById(R.id.button_page_a).setOnClickListener(this);
findViewById(R.id.button_page_b).setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button_page_a:
AndroidFlutterActivity.open(this, FlutterTools.ROUTE_PAGE_A);
break;

case R.id.button_page_b:
AndroidFlutterActivity.open(this, FlutterTools.ROUTE_PAGE_B);
break;

default:
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();

// 释放资源
FlutterTools.destroyEngine();
}
}

最后看效果。模拟器上会有短暂的白屏,真机上基本看不出来。

https://upload-images.jianshu.io/upload_images/23473819-8ade76a3172440ff.gif

相关阅读

1 Android Flutter 混合开发高仿大厂 App

那些初学者实践 Flutter 最常出现的错误

Flutter 概述

Android 敏感数据泄露引发的思考

Flutter 添加到现有项目

B3Q7faa.jpg!mobile

如果你有写博客的好习惯

欢迎投稿

赞+在看,小生感恩 :heart:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK