技术基石
最初我们是FlutterBoost
的使用者,在1.12.x版本前后因为版本更新和使用问题上遇到了很大瓶颈,于是我们基于Flutter单engine开发了一款适合自己的混合栈管理方案,实现的功能比较单一。随着Flutter2.0发布,支持多Engine,Navigator2.0等更优秀的API出现,我们着手开发一款新的混合栈管理方案。
多Engine
虽然现在多Engine还在实验阶段,但对比1.x版本已经有很大提升。在第一个Engine创建之后其他的Engine都是基于第一个spawn
(fork)的,他们之间共享GPU上下文、字体映射、图形缓冲区,真正需要新开辟的资源只有DartVM
隔离内存,托管 Dart/UI 线程的新 pthread
线程。官方介绍新增一个Engine在AOT模式下大约会增加180K内存。
https://flutter.dev/docs/development/add-to-app/multiple-flutters
https://flutter.dev/go/multiple-flutters
Navigator2.0
这也是Flutter2.0的一个重大更新,为什么需要新的API?
- 路由栈切换不灵活。只提供了
push pop
等api,比如从【home => mine => settings】到【home => list => detail】这样的路由栈变化,在1.0版本将会是一个噩梦。 - 无法对路由进行参数解析。从类似
/details/:id
路由配置中传递参数。 - 嵌套路由的情况下子路由无法监听系统返回键,实现复杂。
Navigator2.0
- 支持web。浏览器本身比手机终端更难把控,因为浏览器可以随便更改地址,导致路由的变化,怎么办。那么接收到根路由的变化,将会重新获取所有页面的配置信息。
- 开发者完全把控路由,只需改变app状态就可以刷新当前路由栈。
提供这个特性,我们可以随意定义各种push pop popUntil
等操作,而且还能配套混合模式调用原生的方法。
https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade
https://flutter.cn/community/tutorials/understanding-navigator-v2
要做些什么?
考虑框架要支持大部分的APP技术架构,实现以下特性
- 支持 基本的 push、pop ,并携带参数
- 支持
Uri
类型参数 push(兼容ARouter),提供Schema链接解析入口 - 支持 自定义
MethodChannel
实现 从 FlutterBoost 的迁移 - 数据同步 实现 一些 类似Cookie、用户信息在原生和Flutter之间的同步
- 支持 Mock 原生和Flutter交互
- 支持
Android Fragment
,因为可能存在一些业务必须继承原生VC - 不修改 flutter-embed 任何代码,减少维护成本。
TODO
- 真正意义支持
popUntil
。其实现在的 pop 是通过 popUntil API 实现的 - 支持多级路由配置,比如
/home/detail
框架设计
NavigatorStack队列
- 只有当从Native打开一个Flutter页面的时候,创建新的engine
- 监听所有的原生页面生命周期,当打开一个原生页面时,将页面信息添加到StackManager中
- 当打开Flutter页面时,即调用了push,将Flutter页面信息保存在Flutter侧的StackManager中并同步到原生的StackManager。即所有的页面信息栈维护在原生,Flutter只维护当前纯Flutter的路由栈。
- Flutter pop 时检查当前栈内的路由表,当个数大于1时正常pop,否则退出当前Flutter Activity
一个完整的push流程
可以看到
NavigatorStackManager
中 队列在不断填充
数据共享
之所以做这个功能,是因为在开发的过程中,一般需要从原生同步一些数据,比如cookie,一般需要借助methodchannel实现,并且为了确保页面在网络请求之前能拿到数据,在网络请求之前都会 waite cookie获取,十分麻烦。于是找到一种机制,依赖java对象字段改变监听PropertyChangeListener
,当数据发生改变是通知所有的Flutter Activity,当然对应的Flutter侧对象也是可监听的,可以实现在原生数据变化时Flutter页面同步变化,大大减少了开发时间。
页面数据传递
为了支持通过Uri的方式打开Flutter页面,将Uri参数通过提供的接口进行转化为 path 和 arguments,再通过 navigate-channel 传递到Flutter侧实现页面跳转。在数据传递中,一直保持着path和arguments参数。
实现pushForResult时,因为StackManager中保存着将要退出Activity的弱引用,在finish之前setResult。打开Flutter的native页面只需重写onActivityResult
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null) {
Log.e("AAA", "requestCode : " + requestCode + "; resultCode : " + resultCode);
Log.e("AAA", (data.getSerializableExtra("result")).toString());
}
}
Push Handler
当Flutter打开一个Native页面时(通过MuffinNavigator.of(context).pushNamed('/native_second', {'data': "data from Home Screen"});
),原生在初始化时添加PushNativeHandler
可自定义push。
public interface PushNativeHandler {
void pushNamed(Activity activity, String pageName, @NonNull HashMap<String, Object> data);
}
当Native页面通过Uri打开Flutter页面时(通过MuffinNavigator.push(Uri.parse("meijianclient://meijian.io?url=first&name=uri_test"));
),原生在初始化时添加PushFlutterHandler
实现Uri参数的解析。
public interface PushFlutterHandler {
String getPath(Uri uri);
HashMap<String, Object> getArguments(Uri uri);
}
上面的例子我的现实可能是
//meijianclient://meijian.io?url=first&name=uri_test
public class DefaultPushFlutterHandler implements PushFlutterHandler {
@Override public String getPath(Uri uri) {
return "/" + uri.getQueryParameter("url");
}
@Override public HashMap<String, Object> getArguments(Uri uri) {
HashMap<String, Object> arguments = new HashMap<>();
for (String queryParameterName : uri.getQueryParameterNames()) {
if (!TextUtils.equals("url", queryParameterName)) {
arguments.put(queryParameterName, uri.getQueryParameter(queryParameterName));
}
}
return arguments;
}
}
接入Muffin
1.在Flutter项目中添加依赖
muffin: ^0.0.1
2.路由配置&&数据共享配置&&各种配置
void main() async {
///确保channel初始化
WidgetsFlutterBinding.ensureInitialized();
///如果需要数据同步,则添加下面的代码,将原生的数据同步到Flutter侧
await Share.instance.init([BasicInfo.instance]);
///添加 channel method mock
Muffin.instance.addMock(MockConfig('someMethod', (key, value) => {}));
///get Navigator Widget
runApp(await getApp());
}
Future<Widget> getApp() async {
///初始化 Navigator,配置页面路由信息
///initRoute参数:在单独运行时可以配置打开默认的页面
///initArguments参数:在单独运行时可以配置打开默认的页面参数
///emptyWidget参数:在跳转时没有找对应的页面,则显示定义的空页面
final navigator = MuffinNavigator(routes: {
'/home': (arguments) => MuffinRoutePage(child: HomeScreen()),
'/first': (arguments) => MuffinRoutePage(
child: FirstScreen(
arguments: arguments,
))
},
initRoute:'/',
initArguments:{},
emptyWidget: CustomEmptyView()
);
return MaterialApp.router(
///路由解析
routeInformationParser: MuffinInformationParser(navigator: navigator),
routerDelegate: navigator,
///系统返回键监听
backButtonDispatcher: MuffinBackButtonDispatcher(navigator: navigator),
);
}
3.原生,在Applocation中初始化 Muffin
//普通初始化,第二个参数为 各种提供给上层的接口实现
Muffin.init(this, options());
private Muffin.Options options() {
//数据同步对象
List<DataModelChangeListener> models = new ArrayList<>();
models.add(BasicInfo.getInstance());
return new Muffin.Options()
//Flutter 跳转 Native 时提供给上层的接口
.setPushNativeHandler((activity, pageName, data) -> {
//根据 pageName 和 data 拼接成 schema 跳转
if (TextUtils.equals("/main", pageName)) {
Intent intent = new Intent(activity, MainActivity.class);
activity.startActivity(intent);
}
})
//Native Uri 类型跳转到 Flutter 接口,可参考默认实现
.setPushFlutterHandler(new DefaultPushFlutterHandler())
//带有数据同步能力
.setModels(models)
//新增自定义VC,使用【MuffinFlutterFragment】, 参考[BaseFlutterActivity]
//默认使用【MuffinFlutterActivity】
.setAttachVc(BaseFlutterActivity.class);
}
4.在 Manifest.xml文件中配置 FlutterActivity
5. 好了,Muffin已经集成完了。