如何無縫的將Flutter引入現有應用?

爲何寫thrio?

在早期Flutter發佈的時候,谷歌雖然提供了iOS和Android App上的Flutter嵌入方案,但主要針對的是純Flutter的情形,混合開發支持的並不友好。git

所謂的純RN、純weex應用的生命週期都不存在,因此也不會存在一個純Flutter的App的生命週期,由於咱們老是有須要複用現有模塊。github

因此咱們須要一套足夠完整的Flutter嵌入原生App的路由解決方案,因此咱們本身造了個輪子 thrio ,現已開源,遵循MIT協議。markdown

thrio的設計原則

  • 原則一,dart端最小改動接入
  • 原則二,原生端最小侵入
  • 原則三,三端保持一致的API

thrio全部功能的設計,都會遵照這三個原則。下面會逐步對功能層面一步步展開進行說明,後面也會有原理性的解析。weex

thrio的頁面路由

以dart中的 Navigator 爲主要參照,提供如下路由能力:數據結構

  • push,打開一個頁面並放到路由棧頂
  • pop,關閉路由棧頂的頁面
  • popTo,關閉到某一個頁面
  • remove,刪除任意頁面

Navigator中的API幾乎均可以經過組合以上方法實現,replace 方法暫未提供。閉包

不提供iOS中存在的 present 功能,由於會致使原生路由棧被覆蓋,維護複雜度會很是高,如確實須要能夠經過修改轉場動畫實現。app

頁面的索引

要路由,咱們須要對頁面創建索引,一般狀況下,咱們只須要給每一個頁面設定一個 url 就能夠了,若是每一個頁面都只打開一次的話,不會有任何問題。可是當一個頁面被打開屢次以後,僅僅經過url是沒法定位到明確的頁面實例的,因此在 thrio 中咱們增長了頁面索引的概念,具體在API中都會以 index 來表示,同一個url第一個打開的頁面的索引爲 1 ,以後同一個 url 的索引不斷累加。框架

如此,惟必定位一個頁面的方式爲 url + index,在dart中 routename 就是由 '$url.$index' 組合而成。async

不少時候,使用者不須要關注 index,只有當須要定位到多開的 url 的頁面中的某一個時才須要關注 index。最簡單獲取 index 的方式爲 push 方法的回調返回值。ide

頁面的push

  1. dart 端打開頁面
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'),
);
複製代碼
  1. iOS 端打開頁面
[ThrioNavigator pushUrl:@"flutter1"];
// 接收所打開頁面的關閉回調
[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) {
    ThrioLogV(@"biz2/flutter2 popped: %@", params);
}];
複製代碼
  1. Android 端打開頁面
ThrioNavigator.push(this, "biz1/flutter1",
        mapOf("k1" to 1),
        false,
        poppedResult = {
            Log.e("Thrio", "native1 popResult call params $it")
        }
)
複製代碼
  1. 連續打開頁面
  • dart端只須要await push,就能夠連續打開頁面
  • 原生端須要等待push的result回調返回才能打開第二個頁面
  1. 獲取所打開頁面關閉後的回調參數
  • 三端均可以經過閉包 poppedResult 來獲取

頁面的pop

  1. dart 端關閉頂層頁面
// 默認動畫開啓
ThrioNavigator.pop();
// 不開啓動畫,原生和dart頁面都生效
ThrioNavigator.pop(animated: false);
// 關閉當前頁面,並傳遞參數給push這個頁面的回調
ThrioNavigator.pop(params: 'popped flutter1'),
複製代碼
  1. iOS 端關閉頂層頁面
// 默認動畫開啓
[ThrioNavigator pop];
// 關閉動畫
[ThrioNavigator popAnimated:NO];
// 關閉當前頁面,並傳遞參數給push這個頁面的回調
[ThrioNavigator popParams:@{@"k1": @3}];
複製代碼
  1. Android 端關閉頂層頁面
ThrioNavigator.pop(this, params, animated)
複製代碼

頁面的popTo

  1. dart 端關閉到頁面
// 默認動畫開啓
ThrioNavigator.popTo(url: 'flutter1');
// 不開啓動畫,原生和dart頁面都生效
ThrioNavigator.popTo(url: 'flutter1', animated: false);
複製代碼
  1. iOS 端關閉到頁面
// 默認動畫開啓
[ThrioNavigator popToUrl:@"flutter1"];
// 關閉動畫
[ThrioNavigator popToUrl:@"flutter1" animated:NO];
複製代碼
  1. Android 端關閉到頁面
ThrioNavigator.popTo(context, url, index)
複製代碼

頁面的remove

  1. dart 端關閉特定頁面
ThrioNavigator.remove(url: 'flutter1');
// 只有當頁面是頂層頁面時,animated參數纔會生效
ThrioNavigator.remove(url: 'flutter1', animated: true);
複製代碼
  1. iOS 端關閉特定頁面
[ThrioNavigator removeUrl:@"flutter1"];
// 只有當頁面是頂層頁面時,animated參數纔會生效
[ThrioNavigator removeUrl:@"flutter1" animated:NO];
複製代碼
  1. Android 端關閉特定頁面
ThrioNavigator.remove(context, url, index)
複製代碼

thrio的頁面通知

頁面通知通常來講並不在路由的範疇以內,但咱們在實際開發中卻常常須要使用到,由此產生的各類模塊化框架一個比一個複雜。

那麼問題來了,這些模塊化框架很難在三端互通,全部的這些模塊化框架提供的能力無非最終是一個頁面通知的能力,並且頁面通知咱們能夠很是簡單的在三端打通。

鑑於此,頁面通知做爲thrio的一個必備能力被引入了thrio。

發送頁面通知

  1. dart 端給特定頁面發通知
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
複製代碼
  1. iOS 端給特定頁面發通知
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
複製代碼
  1. Android 端給特定頁面發通知
ThrioNavigator.notify(url, index, params)
複製代碼

接收頁面通知

  1. dart 端接收頁面通知

使用 NavigatorPageNotify 這個 Widget 來實如今任何地方接收當前頁面收到的通知。

NavigatorPageNotify(
      name: 'page1Notify',
      onPageNotify: (params) =>
          ThrioLogger.v('flutter1 receive notify: $params'),
      child: Xxxx());
複製代碼
  1. iOS 端接收頁面通知

UIViewController實現協議NavigatorPageNotifyProtocol,經過 onNotify 來接收頁面通知

- (void)onNotify:(NSString *)name params:(NSDictionary *)params {
  ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
複製代碼
  1. Android 端接收頁面通知

Activity實現協議OnNotifyListener,經過 onNotify 來接收頁面通知

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

由於Android activity在後臺可能會被銷燬,因此頁面通知實現了一個懶響應的行爲,只有當頁面呈現以後纔會收到該通知,這也符合頁面須要刷新的場景。

thrio的模塊化

模塊化在thrio裏面只是一個非核心功能,僅僅爲了實現原則二而引入原生端。

thrio的模塊化能力由一個類提供,ThrioModule,很小巧,主要提供了 Module 的註冊鏈和初始化鏈,讓代碼能夠根據路由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 端只能獲取自身頁面的生命週期

  1. 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) {}
}
複製代碼
  1. iOS 端獲取頁面的生命週期
@interface Module1 : ThrioModule<NavigatorPageObserverProtocol>

@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 頁面的路由行爲

  1. 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,
  ) {}
}
複製代碼
  1. iOS 端獲取頁面的路由行爲
@interface Module2 : ThrioModule<NavigatorRouteObserverProtocol>

@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 中,有一個 Widget 提供了該功能,thrio 無缺的保留了這個功能。

WillPopScope(
    onWillPop: () async => true,
    child: Container(),
);
複製代碼

在 iOS 中,thrio 提供了相似的功能,返回 NO 表示不會關閉,一旦設置會將側滑返回手勢禁用

viewController.thrio_willPopBlock = ^(ThrioBoolCallback _Nonnull result) {
  result(NO);
};
複製代碼

關於 FlutterViewController 的側滑返回手勢,Flutter 默認支持的是純Flutter應用,僅支持單一的 FlutterViewController 做爲整個App的容器,內部已經將 FlutterViewController 的側滑返回手勢去掉。但 thrio 要解決的是 Flutter 與原生應用的無縫集成,因此必須將側滑返回的手勢加回來。

thrio的設計解析

目前開源 Flutter 嵌入原生的庫,主要的仍是經過切換 FlutterEngine 上的原生容器來實現的,這是 Flutter 本來提供的原生容器之上最小改動而實現,須要當心處理好容器切換的時序,不然在頁面導航時會產生崩潰。基於 Flutter 提供的這個功能, thrio 構建了三端一致的頁面管理API。

dart 的核心類

dart 端只管理 dart頁面

  1. 基於 RouteSettings 進行擴展,複用現有的字段
  • name = url.index
  • isInitialRoute = !isNested
  • arguments = params
  1. 基於 MaterialPageRoute 擴展的 NavigatorPageRoute
  • 主要提供頁面描述和轉場動畫的是否配置的功能
  1. 基於 Navigator 擴展,封裝 NavigatorWidget,提供如下方法
Future<bool> push(RouteSettings settings, {
    bool animated = true,
    NavigatorParamsCallback poppedResult,
  });
  
  Future<bool> pop(RouteSettings settings, {bool animated = true});
  
  Future<bool> popTo(RouteSettings settings, {bool animated = true});

  Future<bool> remove(RouteSettings settings, {bool animated = false});
  
複製代碼
  1. 封裝 ThrioNavigator 路由API
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<int> 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<bool> notify({
        @required String url,
        int index,
        @required String name,
        params,
    });
    
    /// Pop a page from the navigation stack.
    ///
    static Future<bool> pop({params, bool animated = true})

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

iOS 的核心類

  1. NavigatorRouteSettings 對應於 dart 的 RouteSettings 類,並提供相同數據結構
@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

複製代碼
  1. NavigatorPageRoute 對應於 dart 的 NavigatorPageRoute
  • 存儲通知、頁面關閉回調、NavigatorRouteSettings
  • route的雙向鏈表
  1. 基於 UINavigationController 擴展,功能相似 dart 的 NavigatorWidget
  • 提供一些列的路由內部接口
  • 並能兼容非 thrio 體系內的頁面
  1. 基於 UIViewController 擴展
  • 提供 FlutterViewController 容器上的 dart 頁面的管理功能
  • 提供 popDisable 等功能
  1. 封裝 ThrioNavigator 路由API
@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 路由棧的結構

thrio-architecture

  1. 一個應用容許啓動多個Flutter引擎,可以讓每一個引擎運行的代碼物理隔離,按需啓用,劣勢是啓動多個Flutter引擎可能致使資源消耗過多而引發問題;
  2. 一個Flutter引擎經過切換能夠匹配到多個FlutterViewController,這是Flutter優雅嵌入原生應用的前提條件
  3. 一個FlutterViewController能夠內嵌多個Dart頁面,有效減小單個FlutterViewController只打開一個Dart頁面致使的內存消耗過多問題,關於內存消耗的問題,後續會有提到。

dart 與 iOS push的時序圖

thrio-push

  1. 全部路由操做最終匯聚於原生端開始,若是始於 dart 端,則經過 channel 調用原生端的API
  2. 經過 url+index 定位到頁面
  3. 若是頁面是原生頁面,則直接進行相關操做
  4. 若是頁面是 Flutter 容器,則經過 channel 調用 dart 端對應的路由 API
  5. 接4步,若是 dart 端對應的路由 API 操做完成後回調,若是成功,則執行原生端的路由棧同步,若是失敗,則回調入口 API 的result
  6. 接4不,若是 dart 端對應的路由 API操做成功,則經過 route channel 調用原生端對應的 route observer,經過 page channel 調用原生端對應的 page observer。

dart 與 iOS pop的時序圖

thrio-pop

  1. pop 的流程與 push 基本一致;
  2. pop 須要考慮頁面是否可關閉的問題;
  3. 但在 iOS 中,側滑返回手勢會致使問題, popViewControllerAnimated: 會在手勢開始的時候調用,致使 dart 端的頁面已經被 pop 掉,但若是手勢被放棄了,則致使兩端的頁面棧不一致,thrio 已經解決了這個問題,具體流程稍複雜,源碼可能更好的說明。

dart 與 iOS popTo的時序圖

thrio-popTo.png

  1. popTo 的流程與 push 基本一致;
  2. 但在多引擎模式下,popTo須要處理多引擎的路由棧同步的問題;
  3. 另外在 Dart 端,popTo其實是多個pop或者remove構成的,最終產生屢次的didPop或didRemove行爲,須要將多個pop或remove組合起來造成一個didPopTo行爲。

dart 與 iOS remove的時序圖

thrio-remove.png

  1. remove 的流程與 push 基本一致。

總結

目前 Flutter 接入原生應用主流的解決方案應該是boost,筆者的團隊在項目深度使用過 boost,也積累了不少對 boost 改善的需求,遇到的最大問題是內存問題,每打開一個 Flutter 頁面的內存開銷基本到了很難接受的程度,thrio把解決內存問題做爲頭等任務,最終效果仍是不錯的,好比以連續打開 5 個 Flutter 頁面計算,boost 的方案會消耗 91.67M 內存,thrio 只消耗 42.76 內存,模擬器上跑出來的數據大體以下:

demo 啓動 頁面 1 頁面 2 頁面 3 頁面 4 頁面 5
thrio 8.56 37.42 38.88 42.52 42.61 42.76
boost 6.81 36.08 50.96 66.18 78.86 91.67

一樣連續打開 5 個頁面的場景,thrio 打開第一個頁面跟 boost 耗時是同樣的,由於都須要打開一個新的 Activity,以後 4 個頁面 thrio 會直接打開 Flutter 頁面,耗時會降下來,如下單位爲 ms:

demo 頁面 1 頁面 2 頁面 3 頁面 4 頁面 5
thrio 242 45 39 31 37
boost 247 169 196 162 165

固然,thrio 跟 boost 的定位仍是不太同樣的,thrio 更多的偏向於解決咱們業務上的需求,儘可能作到開箱即用。

相關文章
相關標籤/搜索