大家好,我是郭樹(shù)煜,Github GSY 系列開(kāi)源項(xiàng)目的作者,系列包括有 GSYVideoPlayer 、GSYGitGithubApp(Flutter\ReactNative\Kotlin\Weex)四大版本,目前總 star 在 17
k+ 左右,主要活躍在掘金社區(qū),id 是戀貓的小郭,主要專(zhuān)欄有《Flutter完整開(kāi)發(fā)實(shí)戰(zhàn)詳解》系列等,平時(shí)工作負(fù)責(zé)移動(dòng)端項(xiàng)目的開(kāi)發(fā),工作經(jīng)歷從 Android 到 React Native 、Weex 再到如今的 Flutter ,期間也參與過(guò) React 、 Vue 、小程序等相關(guān)的開(kāi)發(fā),算是一個(gè)大前端的選手吧。
這次主要是給大家分享 Flutter 相關(guān)的內(nèi)容,主要涉及做一些實(shí)戰(zhàn)和科普性質(zhì)的內(nèi)容。
一、移動(dòng)開(kāi)發(fā)的現(xiàn)狀
恰逢最近谷歌 IO 大會(huì)結(jié)束,大會(huì)后也在線上線下和大家有過(guò)交流,總結(jié)了下大家最關(guān)系的問(wèn)題有:
1、谷歌在 Kotlin-First 的口號(hào)下又推廣 Dart + Flutter 沖突嗎?
這個(gè)問(wèn)題算是被問(wèn)得最多的一個(gè),先說(shuō)觀點(diǎn):我個(gè)人認(rèn)為其實(shí)這并不沖突,因?yàn)橛袀€(gè) 誤區(qū)就是認(rèn)為跨平臺(tái)開(kāi)發(fā)就可以拋棄原生開(kāi)發(fā)!
如果從事過(guò)跨平臺(tái)開(kāi)發(fā)的同學(xué)應(yīng)該知道,平臺(tái)提供的功能向來(lái)是有限的,而面對(duì)產(chǎn)品經(jīng)理的各種 “點(diǎn)歪技能樹(shù)” 的需求,很多時(shí)候你是需要基于框架外提供支持,常見(jiàn)的就是 混合開(kāi)發(fā)或者原生插件支持 。
所以這里我表達(dá)的是,目前 Kotlin 和 Dart 更多是相輔相成 ,而一旦業(yè)務(wù)復(fù)雜度到一定程度,跨平臺(tái)框架還可能存在降低工作效率的問(wèn)題,比如針對(duì)新需求,需要重復(fù)開(kāi)發(fā) Android/IOS 的原生插件做支持,這也是 Aribnb 曾經(jīng)選擇放棄 React Native 的原因之一。
與我而言,跨平臺(tái)的意義在于解決的是端邏輯的統(tǒng)一 ,至少避免了邏輯重復(fù)實(shí)現(xiàn),或者 IOS 和 Android 之間爭(zhēng)論 誰(shuí)對(duì)誰(shuí)錯(cuò) 的問(wèn)題,甚至可以統(tǒng)一到 web 端等等。
2、React Native 和 Flutter 之間的對(duì)比
Flutter 作為后來(lái)者,難免會(huì)被用來(lái)和 React Native 進(jìn)行對(duì)比,在這個(gè)萬(wàn)物皆是 JS 的時(shí)代,Dart 和 Flutter 的出現(xiàn)顯得尤為扎眼。
在設(shè)計(jì)上它們有著許多相似之處,響應(yīng)式設(shè)計(jì)/async支持/setState更新 等等,同時(shí)也有著各種的差異,而大家最為關(guān)心的,無(wú)非 性能、支持、上手難易、穩(wěn)定性程度 這四方面:
-
性能上 Flutter 的確實(shí)會(huì)比 React Native 好 ,如下圖所示,這是由框架底層決定的,當(dāng)然目前
React Native也在進(jìn)行下一代的優(yōu)化, 而對(duì)此最直觀的數(shù)據(jù)就是:GSY系列 在18年用于閑魚(yú)測(cè)試下的對(duì)比數(shù)據(jù)了 。
同時(shí)注意不要用模擬器測(cè)試性能,特別是IOS模擬器做性能測(cè)試,因?yàn)?Flutter 在 IOS模擬器中純 CPU ,而實(shí)際設(shè)備會(huì)是 GPU 硬件加速,同時(shí)只在 Release 下對(duì)比性能。
支持上 Flutter 和 React Native , 都存在第三方包質(zhì)量參差不齊的問(wèn)題,而目前在這一塊 Flutter 是弱于 React Native 的 ,畢竟
React Native發(fā)展已久,雖然版本號(hào)一直不到 1.0,但是在JS的加持下生態(tài)豐富,同時(shí)也是因?yàn)槠脚_(tái)特性的原因,諸如 WebView 、地圖等控件的支持上現(xiàn)在依舊不夠好,這個(gè)后面也會(huì)說(shuō)道。上手難易度上,
Flutter配置環(huán)境和運(yùn)行的“成功率”比 React Native 高不少 ,這里面有node_module黑洞這個(gè)坑,也有React Native本身依賴(lài)平臺(tái)控件導(dǎo)致的,至少我曾經(jīng)試過(guò)接手一個(gè)React Native跑了一天都沒(méi)跑起來(lái)的經(jīng)歷,同時(shí)Flutter在運(yùn)行和SDK版本升級(jí)的陣痛也會(huì)少很多。穩(wěn)定性:
Flutter中大部分異常是不會(huì)引起應(yīng)用崩潰 ,更多會(huì)在 Debug 上體現(xiàn)為紅色錯(cuò)誤堆棧,Release 上 UI 異常等等。
如果你是前端,我會(huì)推薦你先學(xué)
React Native,如果你是原生開(kāi)發(fā),我推薦你學(xué)Flutter。在 React Native 0.59.x 版本開(kāi)始,React 已經(jīng)將許多內(nèi)置控件和庫(kù)移出主項(xiàng)目,希望模糊 React 和 React Native 的界線,統(tǒng)一開(kāi)發(fā),這里的理念和 Flutter 很像。
Flutter 暫時(shí)不支持熱更新?。。。。。。?!
二、Flutter 實(shí)戰(zhàn)
1、Dart 中有意思的一些東西
1.1、var 的語(yǔ)法糖和 dynamic
var 的語(yǔ)法糖是在賦值時(shí)才自推導(dǎo)出類(lèi)型的 ,而 dynamic 是動(dòng)態(tài)聲明,在運(yùn)行時(shí)檢測(cè),它們的使用有時(shí)候容易出現(xiàn)錯(cuò)誤。
如下圖所以說(shuō),
-
var初始化時(shí)被指定為dynamic類(lèi)型的。 - 然后賦值的時(shí)候初始化為
String類(lèi)型,這時(shí)候進(jìn)行 ++ 操作就會(huì)出現(xiàn)運(yùn)行時(shí)報(bào)錯(cuò), - 如下圖2如果在初始化指定類(lèi)型的,那么編譯時(shí)就會(huì)告訴你錯(cuò)誤了。
1.2、各類(lèi)操作符
如下圖所示,Dart 支持很多有意思的操作符,如下圖:
- 執(zhí)行的時(shí)候首先是判斷
AA如果為空,就返回999; - 之后如果
AA為空,就為AA賦值999; - 之后對(duì)
AA進(jìn)行整除999,輸出結(jié)果10。
1.3、支持操作符重載
如下圖所示,Dart 中是支持操作符重載的,這樣可以比較直觀我們的代碼邏輯,并且簡(jiǎn)化代碼時(shí)的調(diào)用。
1.4、方法當(dāng)做參數(shù)傳遞
如下圖所示,在 Dart 中方法時(shí)可以作為參數(shù)傳遞的,這樣的形式可以讓我們更靈活的組織代碼的邏輯。
1.5、async await / async* yield
在 Dart 中 async await / async* yield 等語(yǔ)法糖,代表 Dart 中的 Future 和 Stream 操作,它們對(duì)應(yīng) Dart 中的異步邏輯支持。
sync* / yield 對(duì)應(yīng)
Stream的同步操作。
1.6、Mixins
在 Dart 中支持混入的模式,如下圖所示,混入時(shí)的基礎(chǔ)順序是從右到左依次執(zhí)行的,而且和 super 有關(guān),同時(shí) Dart 還支持 mixin 關(guān)鍵字的定義。
Flutter 的啟動(dòng)類(lèi)用的就是 mixins 方式
1.7、isolate
Dart 中單線程模式中增加了 isolate 提供跨線程的真異步操作,而因?yàn)?Dart 中線程不會(huì)共享內(nèi)存,所以也不存在死鎖,從而也導(dǎo)致了 isolate 之間數(shù)據(jù)只能通過(guò) port 的端口方式發(fā)送接口,類(lèi)似于 Scoket 的方式,同時(shí)提供了 compute 的封裝接口方便調(diào)用。
1.8 call
Dart 為了讓類(lèi)可以像函數(shù)一樣調(diào)用,默認(rèn)都可以實(shí)現(xiàn) call() 方法,同樣 typedef 定義的方法也是具備 call() 條件。
比如我定義了一個(gè) CallObject
class CallObject {
List<Widget> footerButton = [];
call(int i, double e) => "$i xxxx $e";
}
就可以通過(guò)以下執(zhí)行
CallObject callObject = CallObject();
print(callObject(11, 11.0));
print(callObject?.call(11, 11.0));
然后我定義了
typedef void ValueFunction(int i);
ValueFunction vt = (int i){
print("zzz $i");
};
就可以通過(guò)直接執(zhí)行和判空?qǐng)?zhí)行處理
vt(666);
vt?.call(777);
2、Flutter 中常見(jiàn)的
2.1、ChangeNotifier
如下圖所示,ChangeNotifier 模式在 Flutter 中是十分常見(jiàn)的,比如 TextField 控件中,通過(guò) TextEditingController 可以快速設(shè)置值的顯示,這是為什么呢?
如下圖所示,這是因?yàn)?TextEditingController 它是 ChangeNotifier 的子類(lèi),而 TextField 的內(nèi)部對(duì)其進(jìn)行了 addListener,同時(shí)我們改變值的時(shí)候調(diào)用了notifyListener,觸發(fā)內(nèi)部 setState。
2.2、InheritedWidget
在 Flutter 中所有的狀態(tài)共享都是通過(guò)它實(shí)現(xiàn)的,如自帶的 Theme ,Localizations ,或者狀態(tài)管理的 scoope_model 、 flutter_redux 等等,都是基于它實(shí)現(xiàn)的。
如下圖是 SliderTheme 的自定義實(shí)現(xiàn)邏輯,默認(rèn) Theme 中是包含了 SliderTheme,但是我們可以通過(guò)覆蓋一個(gè)新的 SliderTheme 嵌套去實(shí)現(xiàn)自定義,然后通過(guò) SliderTheme theme = SliderTheme(context); 獲取,其中而 context 的實(shí)現(xiàn)就是 Element。
在 Element 的 inheritFromWidgetOfExactType 方法實(shí)現(xiàn)里,有一個(gè) Map<Type, InheritedElement> _inheritedWidgets 的對(duì)象。
_inheritedWidgets 一般情況下是空的,只有當(dāng)父控件是 InheritedWidget 或者本身是 InheritedWidgets 時(shí)才會(huì)有被初始化,而當(dāng)父控件是 InheritedWidget 時(shí),這個(gè) Map 會(huì)被一級(jí)一級(jí)往下傳遞與合并 。
所以當(dāng)我們通過(guò) context 調(diào)用 inheritFromWidgetOfExactType 時(shí),就可以往上查找到父控件的 Widget 。
2.3、StreamBuilder
StreamBuilder 一般用于通過(guò) Stream 異步構(gòu)建頁(yè)面的,如下圖所示,通過(guò)點(diǎn)擊之后,綠色方框的文字會(huì)變成 addNewxxx,因?yàn)?Stream 進(jìn)行了 map 變化,同時(shí)一般實(shí)現(xiàn) bloc 模式的時(shí)候,經(jīng)常會(huì)用到它們。
類(lèi)似的還有 FutureBuilder
2.4、State 中的參數(shù)使用
一般 Widget 都是一幀的,而 State 實(shí)現(xiàn)了 Widget 的跨幀繪制,一般定義的時(shí)候,我們可以如下圖一樣實(shí)現(xiàn),而如下圖尖頭所示,這時(shí)候我們點(diǎn)擊 setState 改變的時(shí)候,是不會(huì)出現(xiàn)效果的,為什么呢?
其實(shí) State 對(duì)象的創(chuàng)建和更新時(shí)機(jī)導(dǎo)致的:
1、createState 只在 StatefulElement 創(chuàng)建時(shí)才會(huì)被創(chuàng)建的。
2、StatefulElement 的 createElement 一般只在 inflateWidget 調(diào)用。
3、updateChild 執(zhí)行 inflateWidget 時(shí), 如果 child 存在可以更新的話,不會(huì)執(zhí)行 inflateWidget。
3、四棵樹(shù)
Flutter 中主要有
Widget 、Element 、RenderObject 、Layer 四棵樹(shù),它們的作用是:
Widget:就是我們平常寫(xiě)的控件,Flutter宇宙中萬(wàn)物皆Widget,它們都是不可變一幀,同時(shí)也是被人吐槽很多的嵌套模式,當(dāng)然換個(gè)角度,事實(shí)上你把他當(dāng)作Widget配置文件來(lái)寫(xiě)或者就好理解了。Element:它是BuildContext的實(shí)現(xiàn)類(lèi),Widget實(shí)現(xiàn)跨幀保存的state就是存放在這里,同時(shí)它也充當(dāng)了Widget和RenderObject之間的橋梁。RenderObject:它才是真正干活(layout、paint)等,同時(shí)它才是真實(shí)的 “dom” 。Layer:一整塊的重繪區(qū)域(isRepaintBoundary),決定重繪的影響區(qū)域。
skia在繪制的時(shí)候,saveLayer是比較消耗性能的,比如透明合成、clipRRect等等都會(huì)可能需要saveLayer的調(diào)用, 而saveLayer會(huì)清空GPU繪制的緩存,導(dǎo)致性能上的損耗,所以開(kāi)發(fā)過(guò)程中如果掉幀嚴(yán)重,可以針對(duì)這一塊進(jìn)行優(yōu)化。
4、手勢(shì)
Flutter 在手勢(shì)中引入了競(jìng)技的概念, Down 事件在 Flutter 中尤為重要。
PointerDownEvent是一切的起源,在Down事件中一般不會(huì)決出勝利者。在
MOVE和UP的時(shí)候才競(jìng)爭(zhēng)得到響應(yīng)。以點(diǎn)擊為例子:
Down時(shí)添加進(jìn)去參與競(jìng)爭(zhēng),UP的時(shí)候才決定誰(shuí)勝利,勝利條件是:
I、UP 的時(shí)候如果只有一個(gè),那么就是它了。
II、UP 的時(shí)候如果有多個(gè),那么強(qiáng)制隊(duì)列里第一個(gè)直接勝利。
- 這里包含了有趣的點(diǎn)就是,都在
UP的時(shí)候才響應(yīng),那么 Down 事件怎么先傳遞出去了?
FLutter 在這里做了一個(gè) didExceedDeadline 機(jī)制 ,事實(shí)上在上面的 addPointer 的時(shí)候,會(huì)啟動(dòng)了一個(gè)定時(shí)器,默認(rèn) 100 ms,如果超過(guò)指定時(shí)間沒(méi) UP ,那就先執(zhí)行這個(gè) didExceedDeadline 響應(yīng) Down 事件。
- 那問(wèn)題又來(lái)了,如果這時(shí)候隊(duì)列里兩個(gè)呢?
它們的 onTapDown 都會(huì)被觸發(fā),但是 onTap 只有一個(gè)獲得。
- 如果有兩個(gè)滑動(dòng)
ScrollView嵌套呢?
舉個(gè)簡(jiǎn)單的例子,兩個(gè) SingleChildScrollView 的嵌套時(shí),在布局會(huì)經(jīng)歷:
performLayout->applyContentDimensions->applyNewDimensions->context.setCanDrag(physics.shouldAcceptUserOffset(this));
只有 shouldAcceptUserOffset 為 ture 時(shí),才會(huì)添加 VerticalDragGestureRecognizer 去處理手勢(shì)。
而判斷條件主要是 return math.max(0.0, child.size.height - size.height); ,也就是如果 child Scroll 的 height 小于父控件 Scroll 的時(shí)候,就會(huì)出現(xiàn) child 不添加 VerticalDragGestureRecognizer 的情況,這時(shí)候根本就沒(méi)有競(jìng)爭(zhēng)了。
5、動(dòng)畫(huà)
Flutter 中的動(dòng)畫(huà)是怎么執(zhí)行的呢?
我們先看一段代碼,然后這段代碼執(zhí)行的效果如下圖2所示。
那既然 Widget 都是一幀,那么動(dòng)畫(huà)肯定有 setState 的地方了。
首先這里有個(gè)地方可以看下,這時(shí)候 200 這個(gè)數(shù)值執(zhí)行后是會(huì)報(bào)錯(cuò)的,因?yàn)榘卓騼?nèi)可見(jiàn) Tween 中的 T 在這時(shí)候會(huì)出現(xiàn)既有 int 又有 double ,無(wú)法判斷的問(wèn)題,所以真實(shí)應(yīng)該是 200.0 。
同時(shí)你發(fā)現(xiàn)沒(méi)有,代碼中 parent 的 Container 在 只有100的情況下,它的 child 可以正常的畫(huà) 200,這是因?yàn)槲覀兊?paint 沒(méi)有跟著 RenerObjcet 的大小走, 所以一般情況下,整個(gè)屏幕都是我們的畫(huà)版,Canvas 繪制與父控件大小可以沒(méi)關(guān)系。
同時(shí)動(dòng)畫(huà)是通過(guò) vsync 同步信號(hào)去觸發(fā)的,就是我們 mixin 的 SingleTickerProviderStateMixin,它內(nèi)部的 Ticker 會(huì)通過(guò) SchedulerBinding 的 scheduleFrameCallback 同步信號(hào)觸發(fā)重繪 。
動(dòng)畫(huà)后的控件的點(diǎn)擊區(qū)域,和你的動(dòng)畫(huà)數(shù)據(jù)改變的是 paint 還是 layout 有關(guān) 。
6、狀態(tài)管理
scope_model 、flutter_redux、fish_redux 、甚至還有有 dva_flutter 等等,可以看出狀態(tài)管理在 flutter 中和前端十分相近。
這里簡(jiǎn)單說(shuō)說(shuō) scope_model ,它只有一個(gè)文件,但是很巧妙,它利用的就是 AnimationBuilder 的特性。
如下圖是使用代碼,在前面我們知道,狀態(tài)管理使用的是 InheritedWidget 實(shí)現(xiàn)共享的,而當(dāng)我們對(duì) Model 進(jìn)行數(shù)據(jù)改變時(shí),通過(guò)調(diào)用 notifyListeners 通知頁(yè)面更新了。
這里的原理是什么呢?
其實(shí)
scope_model內(nèi)部利用了AnimationBuilder,而Model實(shí)現(xiàn)了Listenable接口。當(dāng)
Model設(shè)置給了AnimationBuilder時(shí),AnimationBuilder會(huì)執(zhí)行addListener添加監(jiān)聽(tīng),而監(jiān)聽(tīng)方法里會(huì)執(zhí)行setState。所以我們改變
set方法時(shí)調(diào)用notifyListeners就觸發(fā)了setState去更新了,這樣體現(xiàn)出了前面說(shuō)的FLutter常見(jiàn)的開(kāi)發(fā)模式。
三、混合開(kāi)發(fā)
以 Android 的角度來(lái)說(shuō),從方便調(diào)試和解耦集成上,我們一般會(huì)以 aar 的形式集成混合開(kāi)發(fā),這里就會(huì)涉及到 gradle 打包的一個(gè)概念。
1、如下代碼所示,在項(xiàng)目中進(jìn)行 gradle 腳本修改,組件化開(kāi)發(fā)模式,用 apk 開(kāi)發(fā),用 aar 提供集成,正常修改 gradle 代碼即可快速打包。
那如果 Flutter 的項(xiàng)目插件帶有本地代碼呢?
如果開(kāi)發(fā)過(guò)
React Native的應(yīng)該知道,在原生插件安裝時(shí)會(huì)需要執(zhí)行react-native link,而這時(shí)候會(huì)修改項(xiàng)目的gradle 和java代碼。
2、 和 React Native 很有侵入性相比, Flutter 就很巧妙了。
如下圖所示,安裝過(guò)的插件會(huì)出現(xiàn)在 .flutter_plugins 文件中,然后通過(guò)讀取文件,動(dòng)態(tài)在 setting.gradle 和 flutter.gradle 中引入和依賴(lài):
所以這時(shí)候我們可以參考打包,修改我們的gradle腳本,利用 fat-aar 插件將本地 projcet 也打包的 aar 里。
3、混合開(kāi)發(fā)的最大痛點(diǎn)是什么?
肯定是堆棧管理!!! 所以項(xiàng)目開(kāi)發(fā)了 flutter_boost 來(lái)解決這個(gè)問(wèn)題。
- 堆棧統(tǒng)一到了原生層。
- 通過(guò)一個(gè)唯一
engine,切換Surface渲染顯示。 - 每個(gè)
Activity就是一個(gè)Surface,不渲染的頁(yè)面通過(guò)截圖緩存畫(huà)面。
flutter_boost截止到我測(cè)試的時(shí)間 2019-05-16, 只支持 1.2之前的版本
四、PlatformView
混合開(kāi)發(fā)除了集成到原生工程,也有將原生控件集成到 Flutter 渲染樹(shù)里里的需求。
首先我們看看沒(méi)有 PlatformView 之前是如何實(shí)現(xiàn) WebView 的,這樣會(huì)有什么問(wèn)題?
如下圖所示,事實(shí)上 dart 中僅僅是用了一個(gè) SingleChildRenderObjectWidget 用于占位,將大小傳遞給原生代碼,然后在原生代碼里顯示出來(lái)而已。
這樣的時(shí)候必定會(huì)代碼畫(huà)面堆棧問(wèn)題,因?yàn)檫@個(gè)顯示脫離了 Flutter 的渲染樹(shù),通過(guò)出現(xiàn)動(dòng)畫(huà)肯定會(huì)不一致。
4.1 AndroidView
AndroidView -> TextureLayer,利用Android 上的副屏顯示與虛擬內(nèi)存顯示原理。
共享內(nèi)存,實(shí)時(shí)截圖渲染技術(shù)。
存在問(wèn)題,耗費(fèi)內(nèi)存,頁(yè)面復(fù)雜時(shí)慢。
這部分因?yàn)橹耙郧傲倪^(guò),就不贅述了
三、Flutter Web
RN因?yàn)槭窃丶?,所以在react 和react native 整合這件事上存在難度。
flutter 作為一個(gè)UI 框架,與平臺(tái)無(wú)關(guān),在web上利用的是dart2js的能力。 比如Image
- 因?yàn)?Flutter 是一套 UI 框架,整體 UI 幾乎和平臺(tái)無(wú)關(guān),這和 React Native 有很大的區(qū)別。(我在開(kāi)發(fā)過(guò)程中幾乎無(wú)知覺(jué))
- 在 flutter_web 中 UI 層面與渲染邏輯和 Flutter 幾乎沒(méi)有什么區(qū)別,底層的一些區(qū)別如: flutter_web 中的 Canvas 是 EngineCanvas 抽象,內(nèi)部會(huì)借助 dart2js 的能力去生成標(biāo)簽。
- React Native 平臺(tái)關(guān)聯(lián)性太強(qiáng),而 Flutter 在多平臺(tái)上優(yōu)勢(shì)明顯。我們期待官方幫我們解決大部分的適配問(wèn)題。
Flutter 的平臺(tái)無(wú)關(guān)能力能帶來(lái)什么?
1、某些功能頁(yè)面,可以一套代碼實(shí)現(xiàn),利用插件安裝引入,在web、移動(dòng)app、甚至 pc 上,都可以編譯出對(duì)應(yīng)平臺(tái)的高性能代碼,而不會(huì)像 Weex 等一樣存在各種兼容問(wèn)題。
2、在應(yīng)用上可以快速實(shí)現(xiàn)“降級(jí)策略”,比如某種情況下應(yīng)用產(chǎn)生奔潰了,可以替換為同等 UI 的 h5 顯示,而這些代碼只需要維護(hù)一份。
資源推薦
Github : https://github.com/CarGuo
RTC社區(qū) : https://rtcdeveloper.com
