基于多Engine、Navigator2.0实现混合栈管理方案实践

技术基石

最初我们是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队列

image.png
  • 只有当从Native打开一个Flutter页面的时候,创建新的engine
  • 监听所有的原生页面生命周期,当打开一个原生页面时,将页面信息添加到StackManager中
  • 当打开Flutter页面时,即调用了push,将Flutter页面信息保存在Flutter侧的StackManager中并同步到原生的StackManager。即所有的页面信息栈维护在原生,Flutter只维护当前纯Flutter的路由栈。
  • Flutter pop 时检查当前栈内的路由表,当个数大于1时正常pop,否则退出当前Flutter Activity

一个完整的push流程

image.png

可以看到 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已经集成完了。

资源

github https://github.com/meijian-io/muffin

pub https://pub.dev/packages/muffin

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容