一、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依賴插件,如下所示。
1
2
3
4
|
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文件中添加如下代碼。
1
2
3
4
|
setbinding( new binding([gradle: this ])) evaluate( new file( settingsdir.parentfile, 'flutter_library/.android/include_flutter.groovy' )) |
然后,打開原生android工程app目錄下的build.gradle文件,繼續添加如下依賴腳本。
1
2
3
4
|
dependencies { implementation project( ':flutter_boost' ) implementation project( ':flutter' ) } |
重新編譯構建原生android工程,如果沒有任何錯誤則說明android成功了集成flutterboost。使用flutter boost 之前,需要先執行初始化。打開原生android工程,新建一個繼承flutterapplication的application,然后在oncreate()方法中初始化flutterboost,代碼如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<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模塊的頁面。
1
2
|
flutterboost.instance().open( "flutterpage" ,params); flutterboost.instance().close( "uniqueid" ); |
而flutter dart的使用如下。首先,我們可以在main.dart文件的程序入口main()方法中進行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
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 ); } |
當然,還可以監聽頁面的生命周期,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
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 ,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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的代碼,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
@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頁面,可以使用下面的方式。
1
2
|
[[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就好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!,希望大家以后多多支持服務器之家!
原文鏈接:https://segmentfault.com/a/1190000039760722