一、flutter boost簡(jiǎn)介
眾所周知,flutter是一個(gè)由c++實(shí)現(xiàn)的flutter engine和由dart實(shí)現(xiàn)的framework組成的跨平臺(tái)技術(shù)框架。其中,flutter engine負(fù)責(zé)線程管理、dart vm狀態(tài)管理以及dart代碼加載等工作,而dart代碼所實(shí)現(xiàn)的framework則負(fù)責(zé)上層業(yè)務(wù)開發(fā),如flutter提供的組件等概念就是framework的范疇。
隨著flutter的發(fā)展,國(guó)內(nèi)越來(lái)越多的app開始接入flutter。為了降低風(fēng)險(xiǎn),大部分app采用漸進(jìn)式方式引入flutter,在app里選幾個(gè)頁(yè)面用flutter來(lái)編寫,但都碰到了相同的問(wèn)題,在原生頁(yè)面和flutter頁(yè)面共存的情況下,如何管理路由,以及原生頁(yè)面與flutter頁(yè)面之間的切換和通信都是混合開發(fā)中需要解決的問(wèn)題。然而,官方?jīng)]有提供明確的解決方案,只是在混合開發(fā)時(shí),官方建議開發(fā)者,應(yīng)該使用同一個(gè)引擎支持多窗口繪制的能力,至少在邏輯上做到flutterviewcontroller是共享同一個(gè)引擎里面的資源。換句話說(shuō),官方希望所有的繪制窗口共享同一個(gè)主isolate,而不是出現(xiàn)多個(gè)主isolate的情況。不過(guò),對(duì)于現(xiàn)在已經(jīng)出現(xiàn)的多引擎模式問(wèn)題,flutter官方也沒(méi)有提供好的解決方案。除了內(nèi)存消耗嚴(yán)重外,多引擎模式還會(huì)帶來(lái)如下一些問(wèn)題。
- 冗余資源問(wèn)題。多引擎模式下每個(gè)引擎的isolate是相互獨(dú)立的,雖然在邏輯上這并沒(méi)有什么壞處,但是每個(gè)引擎底層都維護(hù)了一套圖片緩存等比較消耗內(nèi)存的對(duì)象,因此設(shè)備的內(nèi)存消耗是非常嚴(yán)重的。
- 插件注冊(cè)問(wèn)題。在flutter插件中,消息傳遞需要依賴messenger,而messenger是由flutterviewcontroller去實(shí)現(xiàn)的。如果一個(gè)應(yīng)用中同時(shí)存在多個(gè)flutterviewcontroller,那么插件的注冊(cè)和通信將會(huì)變得混亂且難以維護(hù)。
- flutter組件和原生頁(yè)面的差異化問(wèn)題。通常,flutter頁(yè)面是由組件構(gòu)成的,原生頁(yè)面則是由viewcontroller或者activity構(gòu)成的。邏輯上來(lái)說(shuō),我們希望消除flutter頁(yè)面與原生頁(yè)面的差異,否則在進(jìn)行頁(yè)面埋點(diǎn)和其它一些操作時(shí)增加一些額外的工作量。
- 增加頁(yè)面通信的復(fù)雜度。如果所有的dart代碼都運(yùn)行在同一個(gè)引擎實(shí)例中,那么它們會(huì)共享同一個(gè)isolate,可以用統(tǒng)一的框架完成組件之間的通信,但是如果存在多個(gè)引擎實(shí)例會(huì)讓isolate的管理變得更加復(fù)雜。
如果不解決多引擎問(wèn)題,那么混合項(xiàng)目的導(dǎo)航棧如下圖所示。
目前,對(duì)于原生工程混編flutter工程出現(xiàn)的多引擎模式問(wèn)題,國(guó)內(nèi)主要有兩種解決方案,一種是字節(jié)跳動(dòng)的修改flutter engine源碼方案,另一種是閑魚開源的flutterboost。由于字節(jié)跳動(dòng)的混合開發(fā)的方案沒(méi)有開源,所以現(xiàn)在能使用的就剩下flutterboost方案。
flutterboost是閑魚技術(shù)團(tuán)隊(duì)開發(fā)的一個(gè)可復(fù)用頁(yè)面的插件,旨在把flutter容器做成類似于瀏覽器的加載方案。為此,閑魚技術(shù)團(tuán)隊(duì)為希望flutterboost能完成如下的基本功能:
- 可復(fù)用的通用型混合開發(fā)方案。
- 支持更加復(fù)雜的混合模式,比如支持tab切換的場(chǎng)景。
- 無(wú)侵入性方案,使用時(shí)不再依賴修改flutter的方案。
- 支持對(duì)頁(yè)面生命周期進(jìn)行統(tǒng)一的管理。
- 具有統(tǒng)一明確的設(shè)計(jì)概念。
并且,最近flutter boost升級(jí)了3.0版本,并帶來(lái)了如下的一些更新:
- 不侵入引擎,兼容flutter的各種版本,flutter sdk的升級(jí)不需要再升級(jí)flutterboost,極大降低升級(jí)成本。
- 不區(qū)分androidx和support分支。
- 簡(jiǎn)化架構(gòu)和接口,和flutterboost2.0比,代碼減少了一半。
- 雙端統(tǒng)一,包括接口和設(shè)計(jì)上的統(tǒng)一。
- 支持打開flutter頁(yè)面,不再打開容器場(chǎng)景。
- 頁(yè)面生命周期變化通知更方便業(yè)務(wù)使用。
- 解決了2.0中的遺留問(wèn)題,例如,fragment接入困難、頁(yè)面關(guān)閉后不能傳遞數(shù)據(jù)、dispose不執(zhí)行,內(nèi)存占用過(guò)高等。
二、flutter boost集成
在原生項(xiàng)目中集成flutter boost只需要將flutter boost看成是一個(gè)插件工程即可。和其他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' |
需要說(shuō)明的是,此處的所依賴的flutterboost的版本與flutter的版本是對(duì)應(yīng)的,如果不對(duì)應(yīng)使用過(guò)程中會(huì)出現(xiàn)版本不匹配的錯(cuò)誤。然后,使用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文件,繼續(xù)添加如下依賴腳本。
1
2
3
4
|
dependencies { implementation project( ':flutter_boost' ) implementation project( ':flutter' ) } |
重新編譯構(gòu)建原生android工程,如果沒(méi)有任何錯(cuò)誤則說(shuō)明android成功了集成flutterboost。使用flutter boost 之前,需要先執(zhí)行初始化。打開原生android工程,新建一個(gè)繼承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項(xiàng)目的,所以我們可以在native 打開和關(guān)閉flutter模塊的頁(yè)面。
1
2
|
flutterboost.instance().open( "flutterpage" ,params); flutterboost.instance().close( "uniqueid" ); |
而flutter dart的使用如下。首先,我們可以在main.dart文件的程序入口main()方法中進(jìn)行初始化。
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 ); } |
當(dāng)然,還可以監(jiān)聽頁(yè)面的生命周期,如下所示。
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), ), ) ], ))), ); } } |
然后,運(yùn)行項(xiàng)目,就可以從原生頁(yè)面跳轉(zhuǎn)到flutter頁(yè)面,如下圖所示效果。
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代碼中打開或關(guān)閉flutter頁(yè)面,可以使用下面的方式。
1
2
|
[[flutterboost instance] open:@ "flutterpage" arguments:@{@ "animated" :@(yes)} ]; [[flutterboost instance] open:@ "secondstateful" arguments:@{@ "present" :@(yes)}]; |
三、flutter boost架構(gòu)
對(duì)于混合工程來(lái)說(shuō),原生端和flutter端對(duì)于頁(yè)面的定義是不一樣的。對(duì)于原生端而言,頁(yè)面通常指的是一個(gè)viewcontroller或者activity,而對(duì)于flutter來(lái)說(shuō),頁(yè)面通常指的是flutter組件。flutterboost框架所要做的就是統(tǒng)一混合工程中頁(yè)面的概念,或者說(shuō)弱化flutter組件對(duì)應(yīng)容器頁(yè)面的概念。換句話說(shuō),當(dāng)有一個(gè)原生頁(yè)面存在的時(shí)候,flutteboost就能保證一定有一個(gè)對(duì)應(yīng)的flutter的容器頁(yè)面存在。
flutterboost框架其實(shí)就是由原生容器通過(guò)消息驅(qū)動(dòng)flutter頁(yè)面容器,從而達(dá)到原生容器與flutter容器同步的目的,而flutter渲染的內(nèi)容是由原生容器去驅(qū)動(dòng)的,下面是flutter boost 給的一個(gè)flutter boost 的架構(gòu)示意圖。
可以看到,flutter boost插件分為平臺(tái)和dart兩端,中間通過(guò)message channel連接。平臺(tái)側(cè)提供了flutter引擎的配置和管理、native容器的創(chuàng)建/銷毀、頁(yè)面可見性變化通知,以及flutter頁(yè)面的打開/關(guān)閉接口等。而dart側(cè)除了提供類似原生navigator的頁(yè)面導(dǎo)航接口的能力外,還負(fù)責(zé)flutter頁(yè)面的路由管理。
總的來(lái)說(shuō),正是基于共享同一個(gè)引擎的方案,使得flutterboost框架有效的解決了多引擎的問(wèn)題。簡(jiǎn)單來(lái)說(shuō),flutterboost在dart端引入了容器的概念,當(dāng)存在多個(gè)flutter頁(yè)面時(shí),flutterboost不需要再用棧的結(jié)構(gòu)去維護(hù)現(xiàn)有頁(yè)面,而是使用扁平化鍵值對(duì)映射的形式去維護(hù)當(dāng)前所有的頁(yè)面,并且每個(gè)頁(yè)面擁有一個(gè)唯一的id
四、flutterboost3.0更新
4.1 不入侵引擎
為了解決官方引擎復(fù)用引起的問(wèn)題,flutterboost2.0拷貝了flutter引擎embedding層的一些代碼進(jìn)行改造,這使得后期的升級(jí)成本極高。而flutterboost3.0采用繼承的方式擴(kuò)展flutteractivity/flutterfragment等組件的能力,并且通過(guò)在適當(dāng)時(shí)機(jī)給dart側(cè)發(fā)送appisresumed消息解決引擎復(fù)用時(shí)生命周期事件錯(cuò)亂導(dǎo)致的頁(yè)面卡死問(wèn)題,并且,flutterboost 3.0 也兼容最新的官方發(fā)布的 flutter 2.0。
4.2 不區(qū)分androidx和support分支
flutterboost2.0通過(guò)自己實(shí)現(xiàn)flutteractivityandfragmentdelegate.host接口來(lái)擴(kuò)展flutteractivity和flutterfragment的能力,而getlifecycle是必須實(shí)現(xiàn)的接口,這就導(dǎo)致對(duì)androidx的依賴。這也是為什么flutterboostview的實(shí)現(xiàn)沒(méi)有被放入flutterboost3.0插件中的原因。而flutterboost3.0通過(guò)繼承的方式擴(kuò)展flutteractivity/flutterfragment的能力的額外收益就是,可以做到不依賴androidx。
4.3 雙端設(shè)計(jì)統(tǒng)一,接口統(tǒng)一
很多flutter開發(fā)者只會(huì)一端,只會(huì)android 或者只會(huì)ios,但他需要接入雙端,所以雙端統(tǒng)一能降低他的 學(xué)習(xí)成本和接入成本。flutterboost3.0,在設(shè)計(jì)上 android和ios都做了對(duì)齊,特別接口上做到了參數(shù)級(jí)的對(duì)齊。
4.4 支持 【打開flutter頁(yè)面不再打開容器】 場(chǎng)景
在flutter模塊內(nèi)部,flutter 頁(yè)面跳轉(zhuǎn)flutter 頁(yè)面是可以不需要再打開flutter容器的,不打開容器,能節(jié)省內(nèi)存開銷。在flutterboost3.0上,打開容器和不打開容器的區(qū)別表現(xiàn)在用戶接口上僅僅是withcontainer參數(shù)是否為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( '打開內(nèi)部路由' , style: textstyle(fontsize: 22.0, color: colors.black), )), ontap: () => boostnavigator.of().push( "flutterpage" , withcontainer: true , arguments: <string, string>{ 'from' : widget.uniqueid}), ) |
4.5 生命周期的精準(zhǔn)通知
在flutterboost2.0上,每個(gè)頁(yè)面都會(huì)收到頁(yè)面生命周期通知,而flutterboost3.0只會(huì)通知頁(yè)面可見性實(shí)際發(fā)生了變化的頁(yè)面,接口也更符合flutter的設(shè)計(jì)。
4.6 其他issue
除了上面的一些特性外,flutter boost 3.0版本還解決了如下一些問(wèn)題:
- 頁(yè)面關(guān)閉后參數(shù)的傳遞,之前只有ios支持,android不支持,目前在dart側(cè)實(shí)現(xiàn),ios 和android 都支持。
- 解決了android 狀態(tài)欄字體和顏色問(wèn)題。
- 解決了頁(yè)面回退willpopscope不起作用問(wèn)題。
- 解決了不在棧頂?shù)捻?yè)面也收到生命周期回調(diào)的問(wèn)題
- 解決了多次setstate耗性能問(wèn)題。
- 提供了framgent 多種接入方式的demo,方便tab 場(chǎng)景的接入。
- 生命周期的回調(diào)代碼,可以用戶代碼里面with的方式接入,使用更簡(jiǎn)單。
- 全面簡(jiǎn)化了,接入成本,包括 dart側(cè),android側(cè)和ios
- 豐富了demo,包含了基本場(chǎng)景,方便用戶接入 和測(cè)試回歸
到此這篇關(guān)于flutter boost 混合開發(fā)框架的文章就介紹到這了,更多相關(guān)flutter boost內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!,希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://segmentfault.com/a/1190000039760722