基於多Engine、Navigator2.0實現混合棧管理方案實踐

TangMaking發表於2021-08-25

技術基石

最初我們是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記憶體。

flutter.dev/docs/develo… flutter.dev/go/multiple…

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 等操作,而且還能配套混合模式呼叫原生的方法。

medium.com/flutter/lea… flutter.cn/community/t…

要做些什麼?

考慮框架要支援大部分的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 github.com/meijian-io/…

pub pub.dev/packages/mu…

相關文章