Flutter Boost 混合開發框架
一、Flutter Boost簡介
眾所周知,Flutter是一個由C++實現的Flutter Engine和由Dart實現的Framework組成的跨平臺技術框架。其中,Flutter Engine負責線程管理、Dart VM狀態管理以及Dart代碼加載等工作,而Dart代碼所實現的Framework則負責上層業務開發,如Flutter提供的組件等概念就是Framework的范疇。
隨著Flutter的發展,國內越來越多的App開始接入Flutter。為瞭降低風險,大部分App采用漸進式方式引入Flutter,在App裡選幾個頁面用Flutter來編寫,但都碰到瞭相同的問題,在原生頁面和Flutter頁面共存的情況下,如何管理路由,以及原生頁面與Flutter頁面之間的切換和通信都是混合開發中需要解決的問題。然而,官方沒有提供明確的解決方案,隻是在混合開發時,官方建議開發者,應該使用同一個引擎支持多窗口繪制的能力,至少在邏輯上做到FlutterViewController是共享同一個引擎裡面的資源。換句話說,官方希望所有的繪制窗口共享同一個主Isolate,而不是出現多個主Isolate的情況。不過,對於現在已經出現的多引擎模式問題,Flutter官方也沒有提供好的解決方案。除瞭內存消耗嚴重外,多引擎模式還會帶來如下一些問題。
- 冗餘資源問題。多引擎模式下每個引擎的Isolate是相互獨立的,雖然在邏輯上這並沒有什麼壞處,但是每個引擎底層都維護瞭一套圖片緩存等比較消耗內存的對象,因此設備的內存消耗是非常嚴重的。
- 插件註冊問題。在Flutter插件中,消息傳遞需要依賴Messenger,而Messenger是由FlutterViewController去實現的。如果一個應用中同時存在多個FlutterViewController,那麼插件的註冊和通信將會變得混亂且難以維護。
- Flutter組件和原生頁面的差異化問題。通常,Flutter頁面是由組件構成的,原生頁面則是由ViewController或者Activity構成的。邏輯上來說,我們希望消除Flutter頁面與原生頁面的差異,否則在進行頁面埋點和其它一些操作時增加一些額外的工作量。
- 增加頁面通信的復雜度。如果所有的Dart代碼都運行在同一個引擎實例中,那麼它們會共享同一個Isolate,可以用統一的框架完成組件之間的通信,但是如果存在多個引擎實例會讓Isolate的管理變得更加復雜。
如果不解決多引擎問題,那麼混合項目的導航棧如下圖所示。
目前,對於原生工程混編Flutter工程出現的多引擎模式問題,國內主要有兩種解決方案,一種是字節跳動的修改Flutter Engine源碼方案,另一種是閑魚開源的FlutterBoost。由於字節跳動的混合開發的方案沒有開源,所以現在能使用的就剩下FlutterBoost方案。
FlutterBoost是閑魚技術團隊開發的一個可復用頁面的插件,旨在把Flutter容器做成類似於瀏覽器的加載方案。為此,閑魚技術團隊為希望FlutterBoost能完成如下的基本功能:
- 可復用的通用型混合開發方案。
- 支持更加復雜的混合模式,比如支持Tab切換的場景。
- 無侵入性方案,使用時不再依賴修改Flutter的方案。
- 支持對頁面生命周期進行統一的管理。
- 具有統一明確的設計概念。
並且,最近Flutter Boost升級瞭3.0版本,並帶來瞭如下的一些更新:
- 不侵入引擎,兼容Flutter的各種版本,Flutter sdk的升級不需要再升級FlutterBoost,極大降低升級成本。
- 不區分Androidx和Support分支。
- 簡化架構和接口,和FlutterBoost2.0比,代碼減少瞭一半。
- 雙端統一,包括接口和設計上的統一。
- 支持打開Flutter頁面,不再打開容器場景。
- 頁面生命周期變化通知更方便業務使用。
- 解決瞭2.0中的遺留問題,例如,Fragment接入困難、頁面關閉後不能傳遞數據、dispose不執行,內存占用過高等。
二、Flutter Boost集成
在原生項目中集成Flutter Boost隻需要將Flutter Boost看成是一個插件工程即可。和其他Flutter插件的集成方式一樣,使用FlutterBoost之前需要先添加依賴。使用Android Studio打開混合工程的Flutter工程,在pubspec.yaml中添加FlutterBoost依賴插件,如下所示。
flutter_boost: git: url: 'https://github.com/alibaba/flutter_boost.git' ref: 'v3.0-hotfixes'
需要說明的是,此處的所依賴的FlutterBoost的版本與Flutter的版本是對應的,如果不對應使用過程中會出現版本不匹配的錯誤。然後,使用flutter packages get命令將FlutterBoost插件拉取到本地。
2.1 Android集成
使用Android Studio打開新建的原生Android工程,在原生Android工程的settings.gradle文件中添加如下代碼。
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_library/.android/include_flutter.groovy'))
然後,打開原生Android工程app目錄下的build.gradle文件,繼續添加如下依賴腳本。
dependencies { implementation project(':flutter_boost') implementation project(':flutter') }
重新編譯構建原生Android工程,如果沒有任何錯誤則說明Android成功瞭集成FlutterBoost。使用Flutter Boost 之前,需要先執行初始化。打開原生Android工程,新建一個繼承FlutterApplication的Application,然後在onCreate()方法中初始化FlutterBoost,代碼如下。
public class MyApplication extends FlutterApplication { @Override public void onCreate() { super.onCreate(); FlutterBoost.instance().setup(this, new FlutterBoostDelegate() { @Override public void pushNativeRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class); FlutterBoost.instance().currentActivity().startActivity(intent); } @Override public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID) .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque) .destroyEngineWithActivity(false) .url(pageName) .urlParams(arguments) .build(FlutterBoost.instance().currentActivity()); FlutterBoost.instance().currentActivity().startActivity(intent); } },engine->{ engine.getPlugins(); } ); } }
然後,打開原生Android工程下的AndroidManifest.xml文件,將Application替換成自定義的MyApplication,如下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.idlefish.flutterboost.example"> <application android:name="com.idlefish.flutterboost.example.MyApplication" android:label="flutter_boost_example" android:icon="@mipmap/ic_launcher"> <activity android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity" android:theme="@style/Theme.AppCompat" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" > <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/> </activity> <meta-data android:name="flutterEmbedding" android:value="2"> </meta-data> </application> </manifest>
由於Flutter Boost 是以插件的方式集成到原生Android項目的,所以我們可以在Native 打開和關閉Flutter模塊的頁面。
FlutterBoost.instance().open("flutterPage",params); FlutterBoost.instance().close("uniqueId");
而Flutter Dart的使用如下。首先,我們可以在main.dart文件的程序入口main()方法中進行初始化。
void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { static Map<String, FlutterBoostRouteFactory> routerMap = { '/': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => Container()); }, 'embedded': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => EmbeddedFirstRouteWidget()); }, 'presentFlutterPage': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => FlutterRouteWidget( params: settings.arguments, uniqueId: uniqueId, )); }}; Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) { FlutterBoostRouteFactory func =routerMap[settings.name]; if (func == null) { return null; } return func(settings, uniqueId); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return FlutterBoostApp( routeFactory ); }
當然,還可以監聽頁面的生命周期,如下所示。
class SimpleWidget extends StatefulWidget { final Map params; final String messages; final String uniqueId; const SimpleWidget(this.uniqueId, this.params, this.messages); @override _SimpleWidgetState createState() => _SimpleWidgetState(); } class _SimpleWidgetState extends State<SimpleWidget> with PageVisibilityObserver { static const String _kTag = 'xlog'; @override void didChangeDependencies() { super.didChangeDependencies(); print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this'); } @override void initState() { super.initState(); PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context)); print('$_kTag#initState, ${widget.uniqueId}, $this'); } @override void dispose() { PageVisibilityBinding.instance.removeObserver(this); print('$_kTag#dispose, ${widget.uniqueId}, $this'); super.dispose(); } @override void onForeground() { print('$_kTag#onForeground, ${widget.uniqueId}, $this'); } @override void onBackground() { print('$_kTag#onBackground, ${widget.uniqueId}, $this'); } @override void onAppear(ChangeReason reason) { print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this'); } void onDisappear(ChangeReason reason) { print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('tab_example'), ), body: SingleChildScrollView( physics: BouncingScrollPhysics(), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 80.0), child: Text( widget.messages, style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), Container( margin: const EdgeInsets.only(top: 32.0), child: Text( widget.uniqueId, style: TextStyle(fontSize: 22.0, color: Colors.red), ), alignment: AlignmentDirectional.center, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(30.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ) Container( height: 300, width: 200, child: Text( '', style: TextStyle(fontSize: 22.0, color: Colors.black), ), ) ], ))), ); } }
然後,運行項目,就可以從原生頁面跳轉到Flutter頁面,如下圖所示效果。
2.2 iOS集成
和Android的集成步驟一樣,使用Xcode打開原生iOS工程,然後在iOS的AppDelegate文件中初始化Flutter Boost ,如下所示。
@interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init]; [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) { } ]; return YES; } @end
下面是自定義的FlutterBoostDelegate的代碼,如下所示。
@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate> @property (nonatomic,strong) UINavigationController *navigationController; @end @implementation MyFlutterBoostDelegate - (void) pushNativeRoute:(FBCommonParams*) params{ BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]]; if(present){ [self.navigationController presentViewController:nvc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:nvc animated:animated]; } } - (void) pushFlutterRoute:(FBCommonParams*)params { FlutterEngine* engine = [[FlutterBoost instance ] getEngine]; engine.viewController = nil; FBFlutterViewContainer *vc = FBFlutterViewContainer.new ; [vc setName:params.pageName params:params.arguments]; BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; if(present){ [self.navigationController presentViewController:vc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:vc animated:animated]; } } - (void) popRoute:(FBCommonParams*)params result:(NSDictionary *)result{ FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController; if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){ [vc dismissViewControllerAnimated:YES completion:^{}]; }else{ [self.navigationController popViewControllerAnimated:YES]; } } @end
如果要在原生iOS代碼中打開或關閉Flutter頁面,可以使用下面的方式。
[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)} ]; [[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
三、Flutter Boost架構
對於混合工程來說,原生端和Flutter端對於頁面的定義是不一樣的。對於原生端而言,頁面通常指的是一個ViewController或者Activity,而對於Flutter來說,頁面通常指的是Flutter組件。FlutterBoost框架所要做的就是統一混合工程中頁面的概念,或者說弱化Flutter組件對應容器頁面的概念。換句話說,當有一個原生頁面存在的時候,FlutteBoost就能保證一定有一個對應的Flutter的容器頁面存在。
FlutterBoost框架其實就是由原生容器通過消息驅動Flutter頁面容器,從而達到原生容器與Flutter容器同步的目的,而Flutter渲染的內容是由原生容器去驅動的,下面是Flutter Boost 給的一個Flutter Boost 的架構示意圖。
可以看到,Flutter Boost插件分為平臺和Dart兩端,中間通過Message Channel連接。平臺側提供瞭Flutter引擎的配置和管理、Native容器的創建/銷毀、頁面可見性變化通知,以及Flutter頁面的打開/關閉接口等。而Dart側除瞭提供類似原生Navigator的頁面導航接口的能力外,還負責Flutter頁面的路由管理。
總的來說,正是基於共享同一個引擎的方案,使得FlutterBoost框架有效的解決瞭多引擎的問題。簡單來說,FlutterBoost在Dart端引入瞭容器的概念,當存在多個Flutter頁面時,FlutterBoost不需要再用棧的結構去維護現有頁面,而是使用扁平化鍵值對映射的形式去維護當前所有的頁面,並且每個頁面擁有一個唯一的id
四、FlutterBoost3.0更新
4.1 不入侵引擎
為瞭解決官方引擎復用引起的問題,FlutterBoost2.0拷貝瞭Flutter引擎Embedding層的一些代碼進行改造,這使得後期的升級成本極高。而FlutterBoost3.0采用繼承的方式擴展FlutterActivity/FlutterFragment等組件的能力,並且通過在適當時機給Dart側發送appIsResumed消息解決引擎復用時生命周期事件錯亂導致的頁面卡死問題,並且,FlutterBoost 3.0 也兼容最新的官方發佈的 Flutter 2.0。
4.2 不區分Androidx和Support分支
FlutterBoost2.0通過自己實現FlutterActivityAndFragmentDelegate.Host接口來擴展FlutterActivity和FlutterFragment的能力,而getLifecycle是必須實現的接口,這就導致對androidx的依賴。這也是為什麼FlutterBoostView的實現沒有被放入FlutterBoost3.0插件中的原因。而FlutterBoost3.0通過繼承的方式擴展FlutterActivity/FlutterFragment的能力的額外收益就是,可以做到不依賴androidx。
4.3 雙端設計統一,接口統一
很多Flutter開發者隻會一端,隻會Android 或者隻會IOS,但他需要接入雙端,所以雙端統一能降低他的 學習成本和接入成本。FlutterBoost3.0,在設計上 Android和IOS都做瞭對齊,特別接口上做到瞭參數級的對齊。
4.4 支持 【打開flutter頁面不再打開容器】 場景
在Flutter模塊內部,Flutter 頁面跳轉Flutter 頁面是可以不需要再打開Flutter容器的,不打開容器,能節省內存開銷。在FlutterBoost3.0上,打開容器和不打開容器的區別表現在用戶接口上僅僅是withContainer參數是否為true就好。
InkWell( child: Container( color: Colors.yellow, child: Text( '打開外部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ), InkWell( child: Container( color: Colors.yellow, child: Text( '打開內部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", withContainer: true, arguments: <String, String>{'from': widget.uniqueId}), )
4.5 生命周期的精準通知
在FlutterBoost2.0上,每個頁面都會收到頁面生命周期通知,而FlutterBoost3.0隻會通知頁面可見性實際發生瞭變化的頁面,接口也更符合flutter的設計。
4.6 其他Issue
除瞭上面的一些特性外,Flutter Boost 3.0版本還解決瞭如下一些問題:
- 頁面關閉後參數的傳遞,之前隻有iOS支持,android不支持,目前在dart側實現,Ios 和Android 都支持。
- 解決瞭Android 狀態欄字體和顏色問題。
- 解決瞭頁面回退willpopscope不起作用問題。
- 解決瞭不在棧頂的頁面也收到生命周期回調的問題
- 解決瞭多次setState耗性能問題。
- 提供瞭Framgent 多種接入方式的Demo,方便tab 場景的接入。
- 生命周期的回調代碼,可以用戶代碼裡面with的方式接入,使用更簡單。
- 全面簡化瞭,接入成本,包括 dart側,android側和ios
- 豐富瞭demo,包含瞭基本場景,方便用戶接入 和測試回歸
到此這篇關於Flutter Boost 混合開發框架的文章就介紹到這瞭,更多相關Flutter Boost內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!,希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Flutter LinearProgressIndicator使用指南分析
- Flutter應用框架運行微信小程序方法
- Flutter構建自定義Widgets的全過程記錄
- 在Flutter中制作翻轉卡片動畫的完整實例代碼
- Android集成Flutter