技術基石
最初我們是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記憶體。
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
等操作,而且還能配套混合模式呼叫原生的方法。
要做些什麼?
考慮框架要支援大部分的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已經整合完了。
複製程式碼
資源
github github.com/meijian-io/…