Bootstrap

如何无缝的将Flutter引入现有应用?

为什么写thrio?

在早期Flutter发布的时候,谷歌虽然提供了iOS和Android App上的Flutter嵌入方案,但主要针对的是纯Flutter的情形,混合开发支持的并不友好。

所谓的纯RN、纯weex应用的生命周期都不存在,所以也不会存在一个纯Flutter的App的生命周期,因为我们总是有需要复用现有模块。

所以我们需要一套足够完整的Flutter嵌入原生App的路由解决方案,所以我们自己造了个轮子 ​,现已开源,遵循MIT协议。

thrio的设计原则

  • 原则一,dart端最小改动接入

  • 原则二,原生端最小侵入

  • 原则三,三端保持一致的API

thrio所有功能的设计,都会遵守这三个原则。下面会逐步对功能层面一步步展开进行说明,后面也会有原理性的解析。

thrio的页面路由

以dart中的 为主要参照,提供以下路由能力:

  • push,打开一个页面并放到路由栈顶

  • pop,关闭路由栈顶的页面

  • popTo,关闭到某一个页面

  • remove,删除任意页面

Navigator中的API几乎都可以通过组合以上方法实现, 方法暂未提供。

不提供iOS中存在的 功能,因为会导致原生路由栈被覆盖,维护复杂度会非常高,如确实需要可以通过修改转场动画实现。

页面的索引

要路由,我们需要对页面建立索引,通常情况下,我们只需要给每个页面设定一个 就可以了,如果每个页面都只打开一次的话,不会有任何问题。但是当一个页面被打开多次之后,仅仅通过url是无法定位到明确的页面实例的,所以在 中我们增加了页面索引的概念,具体在API中都会以 来表示,同一个url第一个打开的页面的索引为 ,之后同一个 的索引不断累加。

如此,唯一定位一个页面的方式为 + ,在dart中 的 就是由 组合而成。

很多时候,使用者不需要关注 ,只有当需要定位到多开的 的页面中的某一个时才需要关注 。最简单获取 的方式为 方法的回调返回值。

页面的push

ThrioNavigator.push(url: 'flutter1');
// 传入参数
ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}});
// 是否动画,目前在内嵌的dart页面中动画无法取消,原生iOS页面有效果
ThrioNavigator.push(url: 'native1', animated:true);
// 接收锁打开页面的关闭回调
ThrioNavigator.push(
    url: 'biz2/flutter2',
    params: {'1': {'2': '3'}},
    poppedResult: (params) => ThrioLogger.v('biz2/flutter2 popped: $params'),
);

[ThrioNavigator pushUrl:@"flutter1"];
// 接收所打开页面的关闭回调
[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) {
    ThrioLogV(@"biz2/flutter2 popped: %@", params);
}];

ThrioNavigator.push(this, "biz1/flutter1",
        mapOf("k1" to 1),
        false,
        poppedResult = {
            Log.e("Thrio", "native1 popResult call params $it")
        }
)

  • dart端只需要await push,就可以连续打开页面

  • 原生端需要等待push的result回调返回才能打开第二个页面

  • 三端都可以通过闭包 poppedResult 来获取

页面的pop

// 默认动画开启
ThrioNavigator.pop();
// 不开启动画,原生和dart页面都生效
ThrioNavigator.pop(animated: false);
// 关闭当前页面,并传递参数给push这个页面的回调
ThrioNavigator.pop(params: 'popped flutter1'),

// 默认动画开启
[ThrioNavigator pop];
// 关闭动画
[ThrioNavigator popAnimated:NO];
// 关闭当前页面,并传递参数给push这个页面的回调
[ThrioNavigator popParams:@{@"k1": @3}];

ThrioNavigator.pop(this, params, animated)

页面的popTo

// 默认动画开启
ThrioNavigator.popTo(url: 'flutter1');
// 不开启动画,原生和dart页面都生效
ThrioNavigator.popTo(url: 'flutter1', animated: false);

// 默认动画开启
[ThrioNavigator popToUrl:@"flutter1"];
// 关闭动画
[ThrioNavigator popToUrl:@"flutter1" animated:NO];

ThrioNavigator.popTo(context, url, index)

页面的remove

ThrioNavigator.remove(url: 'flutter1');
// 只有当页面是顶层页面时,animated参数才会生效
ThrioNavigator.remove(url: 'flutter1', animated: true);

[ThrioNavigator removeUrl:@"flutter1"];
// 只有当页面是顶层页面时,animated参数才会生效
[ThrioNavigator removeUrl:@"flutter1" animated:NO];

ThrioNavigator.remove(context, url, index)

thrio的页面通知

页面通知一般来说并不在路由的范畴之内,但我们在实际开发中却经常需要使用到,由此产生的各种模块化框架一个比一个复杂。

那么问题来了,这些模块化框架很难在三端互通,所有的这些模块化框架提供的能力无非最终是一个页面通知的能力,而且页面通知我们可以非常简单的在三端打通。

鉴于此,页面通知作为thrio的一个必备能力被引入了thrio。

发送页面通知

ThrioNavigator.notify(url: 'flutter1', name: 'reload');

[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];

ThrioNavigator.notify(url, index, params)

接收页面通知

使用 这个 来实现在任何地方接收当前页面收到的通知。

NavigatorPageNotify(
      name: 'page1Notify',
      onPageNotify: (params) =>
          ThrioLogger.v('flutter1 receive notify: $params'),
      child: Xxxx());

实现协议,通过 来接收页面通知

- (void)onNotify:(NSString )name params:(NSDictionary )params {
  ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}

实现协议,通过 来接收页面通知

class Activity : AppCompatActivity(), OnNotifyListener {
    override fun onNotify(name: String, params: Any?) {
    }
}

因为Android activity在后台可能会被销毁,所以页面通知实现了一个懒响应的行为,只有当页面呈现之后才会收到该通知,这也符合页面需要刷新的场景。

thrio的模块化

模块化在thrio里面只是一个非核心功能,仅仅为了实现原则二而引入原生端。

thrio的模块化能力由一个类提供,,很小巧,主要提供了 的注册链和初始化链,让代码可以根据路由url进行文件分级分类。

注册链将所有模块串起来,字母块由最近的父一级模块注册,新增模块的耦合度最低。

初始化链将所有模块需要初始化的代码串起来,同样是为了降低耦合度,在初始化链上可以就近注册模块的页面的构造器,页面路由观察者,页面生命周期观察者等,也可以在多引擎模式下提前启动某一个引擎。

模块间通信的能力由页面通知实现。

mixin ThrioModule {
    /// A function for registering a module, which will call
    /// the onModuleRegister function of the module.
    ///
    void registerModule(ThrioModule module);
    
    /// A function for module initialization that will call
    /// the onPageRegister, onModuleInit and onModuleAsyncInit
    /// methods of all modules.
    ///
    void initModule();
    
    /// A function for registering submodules.
    ///
    void onModuleRegister() {}

    /// A function for registering a page builder.
    ///
    void onPageRegister() {}

    /// A function for module initialization.
    ///
    void onModuleInit() {}

    /// A function for module asynchronous initialization.
    ///
    void onModuleAsyncInit() {}
    
    /// Register an page builder for the router.
    ///
    /// Unregistry by calling the return value VoidCallback.
    ///
    VoidCallback registerPageBuilder(String url, NavigatorPageBuilder builder);

    /// Register observers for the life cycle of Dart pages.
    ///
    /// Unregistry by calling the return value VoidCallback.
    ///
    /// Do not override this method.
    ///
    VoidCallback registerPageObserver(NavigatorPageObserver pageObserver);
    
    /// Register observers for route action of Dart pages.
    ///
    /// Unregistry by calling the return value VoidCallback.
    ///
    /// Do not override this method.
    ///
    VoidCallback registerRouteObserver(NavigatorRouteObserver routeObserver);
}

thrio的页面生命周期

原生端可以获得所有页面的生命周期,Dart 端只能获取自身页面的生命周期

class Module with ThrioModule, NavigatorPageObserver {
  @override
  void onPageRegister() {
    registerPageObserver(this);
  }

  @override
  void didAppear(RouteSettings routeSettings) {}

  @override
  void didDisappear(RouteSettings routeSettings) {}

  @override
  void onCreate(RouteSettings routeSettings) {}

  @override
  void willAppear(RouteSettings routeSettings) {}

  @override
  void willDisappear(RouteSettings routeSettings) {}
}

@interface Module1 : ThrioModule

@end

@implementation Module1

- (void)onPageRegister {
  [self registerPageObserver:self];
}

- (void)onCreate:(NavigatorRouteSettings )routeSettings { }

- (void)willAppear:(NavigatorRouteSettings )routeSettings { }

- (void)didAppear:(NavigatorRouteSettings )routeSettings { }

- (void)willDisappear:(NavigatorRouteSettings )routeSettings { }

- (void)didDisappear:(NavigatorRouteSettings *)routeSettings { }

@end

thrio的页面路由观察者

原生端可以观察所有页面的路由行为,dart 端只能观察 dart 页面的路由行为

class Module with ThrioModule, NavigatorRouteObserver {
  @override
  void onModuleRegister() {
    registerRouteObserver(this);
  }

  @override
  void didPop(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didPopTo(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didPush(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didRemove(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}
}

@interface Module2 : ThrioModule

@end

@implementation Module2

- (void)onPageRegister {
  [self registerRouteObserver:self];
}

- (void)didPop:(NavigatorRouteSettings )routeSettings
 previousRoute:(NavigatorRouteSettings  Nullable)previousRouteSettings {
}

- (void)didPopTo:(NavigatorRouteSettings )routeSettings
   previousRoute:(NavigatorRouteSettings  Nullable)previousRouteSettings {
}

- (void)didPush:(NavigatorRouteSettings )routeSettings
  previousRoute:(NavigatorRouteSettings  Nullable)previousRouteSettings {
}

- (void)didRemove:(NavigatorRouteSettings )routeSettings
    previousRoute:(NavigatorRouteSettings  Nullable)previousRouteSettings {
}

@end

thrio的额外功能

iOS 显隐当前页面的导航栏

原生的导航栏在 dart 上一般情况下是不需要的,但切换到原生页面又需要把原生的导航栏置回来,thrio 不提供的话,使用者较难扩展,我之前在目前一个主流的Flutter接入库上进行此项功能的扩展,很不流畅,所以这个功能最好的效果还是 thrio 直接内置,切换到 dart 页面默认会隐藏原生的导航栏,切回原生页面也会自动恢复。另外也可以手动隐藏原生页面的导航栏。

viewController.thrio_hidesNavigationBar = NO;

支持页面关闭前弹窗确认的功能

如果用户正在填写一个表单,你可能经常会需要弹窗确认是否关闭当前页面的功能。

在 dart 中,有一个 提供了该功能,thrio 完好的保留了这个功能。

WillPopScope(
    onWillPop: () async => true,
    child: Container(),
);

在 iOS 中,thrio 提供了类似的功能,返回 表示不会关闭,一旦设置会将侧滑返回手势禁用

viewController.thriowillPopBlock = ^(ThrioBoolCallback Nonnull result) {
  result(NO);
};

关于 的侧滑返回手势,Flutter 默认支持的是纯Flutter应用,仅支持单一的 作为整个App的容器,内部已经将 的侧滑返回手势去掉。但 thrio 要解决的是 Flutter 与原生应用的无缝集成,所以必须将侧滑返回的手势加回来。

thrio的设计解析

目前开源 Flutter 嵌入原生的库,主要的还是通过切换 FlutterEngine 上的原生容器来实现的,这是 Flutter 原本提供的原生容器之上最小改动而实现,需要小心处理好容器切换的时序,否则在页面导航时会产生崩溃。基于 Flutter 提供的这个功能, thrio 构建了三端一致的页面管理API。

dart 的核心类

dart 端只管理 dart页面

  • name = url.index

  • isInitialRoute = !isNested

  • arguments = params

  • 主要提供页面描述和转场动画的是否配置的功能

  Future push(RouteSettings settings, {
    bool animated = true,
    NavigatorParamsCallback poppedResult,
  });
  
  Future pop(RouteSettings settings, {bool animated = true});
  
  Future popTo(RouteSettings settings, {bool animated = true});

  Future remove(RouteSettings settings, {bool animated = false});
  

abstract class ThrioNavigator {
    /// Push the page onto the navigation stack.
    ///
    /// If a native page builder exists for the url, open the native page,
    /// otherwise open the flutter page.
    ///
    static Future push({
        @required String url,
        params,
        bool animated = true,
        NavigatorParamsCallback poppedResult,
    });
    
    /// Send a notification to the page.
    ///
    /// Notifications will be triggered when the page enters the foreground.
    /// Notifications with the same name will be overwritten.
    /// 
    static Future notify({
        @required String url,
        int index,
        @required String name,
        params,
    });
    
    /// Pop a page from the navigation stack.
    ///
    static Future pop({params, bool animated = true})

    static Future popTo({
        @required String url,
        int index,
        bool animated = true,
    });
    
    /// Remove the page with url in the navigation stack.
    ///  
    static Future remove({
        @required String url,
        int index,
        bool animated = true,
    });
}

iOS 的核心类


@interface NavigatorRouteSettings : NSObject

@property (nonatomic, copy, readonly) NSString url;

@property (nonatomic, strong, readonly) NSNumber index;

@property (nonatomic, assign, readonly) BOOL nested;

@property (nonatomic, copy, readonly, nullable) id params;

@end

  • 存储通知、页面关闭回调、NavigatorRouteSettings

  • route的双向链表

  • 提供一些列的路由内部接口

  • 并能兼容非 thrio 体系内的页面

  • 提供 容器上的 dart 页面的管理功能

  • 提供 popDisable 等功能

@interface ThrioNavigator : NSObject

/// Push the page onto the navigation stack.
///
/// If a native page builder exists for the url, open the native page,
/// otherwise open the flutter page.
///
+ (void)pushUrl:(NSString )url
         params:(id)params
       animated:(BOOL)animated
         result:(ThrioNumberCallback)result
   poppedResult:(ThrioIdCallback)poppedResult;

/// Send a notification to the page.
///
/// Notifications will be triggered when the page enters the foreground.
/// Notifications with the same name will be overwritten.
///
+ (void)notifyUrl:(NSString )url
            index:(NSNumber )index
             name:(NSString )name
           params:(id)params
           result:(ThrioBoolCallback)result;

/// Pop a page from the navigation stack.
///
+ (void)popParams:(id)params
         animated:(BOOL)animated
           result:(ThrioBoolCallback)result;

/// Pop the page in the navigation stack until the page with url.
///
+ (void)popToUrl:(NSString )url
           index:(NSNumber )index
        animated:(BOOL)animated
          result:(ThrioBoolCallback)result;

/// Remove the page with url in the navigation stack.
///
+ (void)removeUrl:(NSString )url
            index:(NSNumber )index
         animated:(BOOL)animated
           result:(ThrioBoolCallback)result;

@end

dart 与 iOS 路由栈的结构

dart 与 iOS push的时序图

dart 与 iOS pop的时序图

dart 与 iOS popTo的时序图

dart 与 iOS remove的时序图

总结

目前 Flutter 接入原生应用主流的解决方案应该是boost,https://github.com/alibaba/flutter_boost,笔者的团队在项目深度使用过 boost,也积累了很多对 boost 改善的需求,遇到的最大问题是内存问题,每打开一个 Flutter 页面的内存开销基本到了很难接受的程度,thrio,https://github.com/hellobike/thrio 把解决内存问题作为头等任务,最终效果还是不错的,比如以连续打开 5 个 Flutter 页面计算,boost 的方案会消耗 91.67M 内存,thrio 只消耗 42.76 内存,模拟器上跑出来的数据大致如下:

同样连续打开 5 个页面的场景,thrio 打开第一个页面跟 boost 耗时是一样的,因为都需要打开一个新的 Activity,之后 4 个页面 thrio 会直接打开 Flutter 页面,耗时会降下来,以下单位为 ms:

当然,thrio 跟 boost 的定位还是不太一样的,thrio 更多的偏向于解决我们业务上的需求,尽量做到开箱即用。