你們好,我是郭樹煜,Github GSY 系列開源項目的做者,系列包括有 GSYVideoPlayer 、GSYGitGithubApp(Flutter\ReactNative\Kotlin\Weex)四大版本,目前總 star 在 17 k+ 左右,主要活躍在掘金社區,id 是戀貓的小郭,主要專欄有《Flutter完整開發實戰詳解》系列等,平時工做負責移動端項目的開發,工做經歷從 Android 到 React Native 、Weex 再到現在的 Flutter ,期間也參與過 React 、 Vue 、小程序等相關的開發,算是一個大前端的選手吧。前端
此次主要是給你們分享 Flutter 相關的內容,主要涉及作一些實戰和科普性質的內容。java
恰逢最近谷歌 IO 大會結束,大會後也在線上線下和你們有過交流,總結了下你們最關係的問題有:node
這個問題算是被問得最多的一個,先說觀點:我我的認爲其實這並不衝突,由於有個 誤區就是認爲跨平臺開發就能夠拋棄原生開發!react
若是從事過跨平臺開發的同窗應該知道,平臺提供的功能向來是有限的,而面對產品經理的各類 「點歪技能樹」 的需求,不少時候你是須要基於框架外提供支持,常見的就是 混合開發或者原生插件支持 。git
因此這裏我表達的是,目前 Kotlin
和 Dart
更可能是相輔相成 ,而一旦業務複雜度到必定程度,跨平臺框架還可能存在下降工做效率的問題,好比針對新需求,須要重複開發 Android/IOS
的原生插件作支持,這也是 Aribnb 曾經選擇放棄 React Native
的緣由之一。github
與我而言,跨平臺的意義在於解決的是端邏輯的統一 ,至少避免了邏輯重複實現,或者 IOS
和 Android
之間爭論 誰對誰錯 的問題,甚至能夠統一到 web 端等等。web
Flutter
做爲後來者,不免會被用來和 React Native
進行對比,在這個萬物皆是 JS
的時代,Dart
和 Flutter
的出現顯得尤其扎眼。redux
在設計上它們有着許多類似之處,響應式設計/async支持/setState更新 等等,同時也有着各類的差別,而你們最爲關心的,無非 性能、支持、上手難易、穩定性程度 這四方面:小程序
React Native
也在進行下一代的優化, 而對此最直觀的數據就是:GSY系列 在18年用於閒魚測試下的對比數據了 。同時注意不要用模擬器測試性能,特別是IOS模擬器作性能測試,由於 Flutter 在 IOS模擬器中純 CPU ,而實際設備會是 GPU 硬件加速,同時只在 Release 下對比性能。react-native
支持上 Flutter 和 React Native , 都存在第三方包質量良莠不齊的問題,而目前在這一塊 Flutter 是弱於 React Native 的 ,畢竟 React Native
發展已久,雖然版本號一直不到 1.0,可是在 JS
的加持下生態豐富,同時也是由於平臺特性的緣由,諸如 WebView 、地圖等控件的支持上如今依舊不夠好,這個後面也會說道。
上手難易度上,Flutter
配置環境和運行的「成功率」比 React Native 高很多 ,這裏面有 node_module
黑洞這個坑,也有 React Native
自己依賴平臺控件致使的,至少我曾經試過接手一個 React Native
跑了一天都沒跑起來的經歷,同時 Flutter
在運行和SDK版本升級的陣痛也會少不少。
穩定性:Flutter
中大部分異常是不會引發應用崩潰 ,更多會在 Debug 上體現爲紅色錯誤堆棧,Release 上 UI 異常等等。
若是你是前端,我會推薦你先學
React Native
,若是你是原生開發,我推薦你學Flutter
。在 React Native 0.59.x 版本開始,React 已經將許多內置控件和庫移出主項目,但願模糊 React 和 React Native 的界線,統一開發,這裏的理念和 Flutter 很像。
Flutter 暫時不支持熱更新!!!!!!!!
var
的語法糖是在賦值時才自推導出類型的 ,而 dynamic
是動態聲明,在運行時檢測,它們的使用有時候容易出現錯誤。
以下圖因此說,
var
初始化時被指定爲 dynamic
類型的。String
類型,這時候進行 ++ 操做就會出現運行時報錯,以下圖所示,Dart
支持不少有意思的操做符,以下圖:
AA
若是爲空,就返回 999
;AA
爲空,就爲 AA
賦值 999
;AA
進行整除 999
,輸出結果 10
。以下圖所示,Dart
中是支持操做符重載的,這樣能夠比較直觀咱們的代碼邏輯,而且簡化代碼時的調用。
以下圖所示,在 Dart
中方法時能夠做爲參數傳遞的,這樣的形式可讓咱們更靈活的組織代碼的邏輯。
在 Dart
中 async await / async* yield
等語法糖,表明 Dart
中的 Future
和 Stream
操做,它們對應 Dart
中的異步邏輯支持。
sync* / yield 對應
Stream
的同步操做。
在 Dart
中支持混入的模式,以下圖所示,混入時的基礎順序是從右到左依次執行的,並且和 super
有關,同時 Dart
還支持 mixin
關鍵字的定義。
Flutter 的啓動類用的就是 mixins 方式
Dart
中單線程模式中增長了 isolate
提供跨線程的真異步操做,而由於 Dart
中線程不會共享內存,因此也不存在死鎖,從而也致使了 isolate
之間數據只能經過 port
的端口方式發送接口,相似於 Scoket
的方式,同時提供了 compute
的封裝接口方便調用。
Dart 爲了讓類能夠像函數同樣調用,默認均可以實現 call()
方法,一樣 typedef
定義的方法也是具有 call()
條件。
好比我定義了一個 CallObject
class CallObject {
List<Widget> footerButton = [];
call(int i, double e) => "$i xxxx $e";
}
複製代碼
就能夠經過如下執行
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");
};
複製代碼
就能夠經過直接執行和判空執行處理
vt(666);
vt?.call(777);
複製代碼
以下圖所示,ChangeNotifier
模式在 Flutter
中是十分常見的,好比 TextField
控件中,經過 TextEditingController
能夠快速設置值的顯示,這是爲何呢?
以下圖所示,這是由於 TextEditingController
它是 ChangeNotifier
的子類,而 TextField
的內部對其進行了 addListener
,同時咱們改變值的時候調用了notifyListener
,觸發內部 setState
。
在 Flutter
中全部的狀態共享都是經過它實現的,如自帶的 Theme
,Localizations
,或者狀態管理的 scoope_model
、 flutter_redux
等等,都是基於它實現的。
以下圖是 SliderTheme
的自定義實現邏輯,默認 Theme
中是包含了 SliderTheme
,可是咱們能夠經過覆蓋一個新的 SliderTheme
嵌套去實現自定義,而後經過 SliderTheme theme = SliderTheme(context);
獲取,其中而 context
的實現就是 Element
。
在 Element
的 inheritFromWidgetOfExactType
方法實現裏,有一個 Map<Type, InheritedElement> _inheritedWidgets
的對象。
_inheritedWidgets
通常狀況下是空的,只有當父控件是 InheritedWidget
或者自己是 InheritedWidgets
時纔會有被初始化,而當父控件是 InheritedWidget
時,這個 Map
會被一級一級往下傳遞與合併 。 因此當咱們經過 context
調用 inheritFromWidgetOfExactType
時,就能夠往上查找到父控件的 Widget
。
StreamBuilder
通常用於經過 Stream
異步構建頁面的,以下圖所示,經過點擊以後,綠色方框的文字會變成 addNewxxx
,由於 Stream
進行了 map
變化,同時通常實現 bloc
模式的時候,常常會用到它們。
相似的還有 FutureBuilder
通常 Widget
都是一幀的,而 State
實現了 Widget
的跨幀繪製,通常定義的時候,咱們能夠以下圖同樣實現,而以下圖尖頭所示,這時候咱們點擊 setState
改變的時候,是不會出現效果的,爲何呢?
其實 State 對象的建立和更新時機致使的:
一、createState 只在 StatefulElement 建立時纔會被建立的。
二、StatefulElement 的 createElement 通常只在 inflateWidget 調用。
三、updateChild 執行 inflateWidget 時, 若是 child 存在能夠更新的話,不會執行 inflateWidget。
Flutter 中主要有 Widget
、Element
、RenderObject
、Layer
四棵樹,它們的做用是:
Widget
:就是咱們日常寫的控件,Flutter
宇宙中萬物皆 Widget
,它們都是不可變一幀,同時也是被人吐槽不少的嵌套模式,固然換個角度,事實上你把他看成 Widget
配置文件來寫或者就好理解了。
Element
:它是 BuildContext
的實現類,Widget
實現跨幀保存的 state
就是存放在這裏,同時它也充當了 Widget
和 RenderObject
之間的橋樑。
RenderObject
:它纔是真正幹活(layout、paint)等,同時它纔是真實的 「dom」 。
Layer
:一整塊的重繪區域(isRepaintBoundary),決定重繪的影響區域。
skia
在繪製的時候,saveLayer
是比較消耗性能的,好比透明合成、clipRRect
等等都會可能須要saveLayer
的調用, 而saveLayer
會清空GPU繪製的緩存,致使性能上的損耗,因此開發過程當中若是掉幀嚴重,能夠針對這一塊進行優化。
Flutter
在手勢中引入了競技的概念, Down
事件在 Flutter
中尤其重要。
PointerDownEvent
是一切的起源,在 Down
事件中通常不會決出勝利者。
在 MOVE
和 UP
的時候才競爭獲得響應。
以點擊爲例子:Down
時添加進去參與競爭,UP
的時候才決定誰勝利,勝利條件是:
I、UP
的時候若是隻有一個,那麼就是它了。
II、UP
的時候若是有多個,那麼強制隊列裏第一個直接勝利。
UP
的時候才響應,那麼 Down 事件怎麼先傳遞出去了?FLutter
在這裏作了一個 didExceedDeadline
機制 ,事實上在上面的 addPointer
的時候,會啓動了一個定時器,默認 100 ms,若是超過指定時間沒 UP
,那就先執行這個 didExceedDeadline
響應 Down
事件。
它們的 onTapDown
都會被觸發,可是 onTap
只有一個得到。
ScrollView
嵌套呢?舉個簡單的例子,兩個 SingleChildScrollView
的嵌套時,在佈局會經歷:
performLayout
->applyContentDimensions
->applyNewDimensions
->context.setCanDrag(physics.shouldAcceptUserOffset(this));
只有 shouldAcceptUserOffset
爲 ture
時,纔會添加 VerticalDragGestureRecognizer
去處理手勢。
而判斷條件主要是 return math.max(0.0, child.size.height - size.height);
,也就是若是 child Scroll 的 height 小於父控件 Scroll 的時候,就會出現 child 不添加 VerticalDragGestureRecognizer 的狀況,這時候根本就沒有競爭了。
Flutter
中的動畫是怎麼執行的呢?
咱們先看一段代碼,而後這段代碼執行的效果以下圖2所示。
那既然 Widget
都是一幀,那麼動畫確定有 setState
的地方了。
首先這裏有個地方能夠看下,這時候 200 這個數值執行後是會報錯的,由於白框內可見 Tween
中的 T
在這時候會出現既有 int
又有 double
,沒法判斷的問題,因此真實應該是 200.0 。
同時你發現沒有,代碼中 parent
的 Container
在 只有100的狀況下,它的 child
能夠正常的畫 200,這是由於咱們的 paint
沒有跟着 RenerObjcet
的大小走, 因此通常狀況下,整個屏幕都是咱們的畫版,Canvas 繪製與父控件大小能夠不要緊。
同時動畫是經過 vsync
同步信號去觸發的,就是咱們 mixin 的 SingleTickerProviderStateMixin
,它內部的 Ticker
會經過 SchedulerBinding
的 scheduleFrameCallback
同步信號觸發重繪 。
動畫後的控件的點擊區域,和你的動畫數據改變的是 paint 仍是 layout 有關 。
scope_model
、flutter_redux
、fish_redux
、甚至還有有 dva_flutter
等等,能夠看出狀態管理在 flutter
中和前端十分相近。
這裏簡單說說 scope_model
,它只有一個文件,可是很巧妙,它利用的就是 AnimationBuilder
的特性。
以下圖是使用代碼,在前面咱們知道,狀態管理使用的是 InheritedWidget
實現共享的,而當咱們對 Model
進行數據改變時,經過調用 notifyListeners
通知頁面更新了。
這裏的原理是什麼呢?
其實 scope_model
內部利用了 AnimationBuilder
,而 Model
實現了 Listenable
接口。
當 Model
設置給了 AnimationBuilder
時, AnimationBuilder
會執行 addListener
添加監聽,而監聽方法裏會執行 setState
。
因此咱們改變 set
方法時調用 notifyListeners
就觸發了 setState
去更新了,這樣體現出了前面說的 FLutter
常見的開發模式。
以 Android
的角度來講,從方便調試和解耦集成上,咱們通常會以 aar
的形式集成混合開發,這裏就會涉及到 gradle
打包的一個概念。
一、以下代碼所示,在項目中進行 gradle
腳本修改,組件化開發模式,用 apk
開發,用 aar
提供集成,正常修改 gradle
代碼便可快速打包。
那若是 Flutter
的項目插件帶有本地代碼呢?
若是開發過
React Native
的應該知道,在原生插件安裝時會須要執行react-native link
,而這時候會修改項目的gradle 和java代碼。
二、 和 React Native
頗有侵入性相比, Flutter
就很巧妙了。
以下圖所示,安裝過的插件會出如今 .flutter_plugins
文件中,而後經過讀取文件,動態在 setting.gradle
和 flutter.gradle
中引入和依賴:
因此這時候咱們能夠參考打包,修改咱們的gradle腳本,利用 fat-aar 插件將本地 projcet 也打包的 aar 裏。
官方將來將有
Flutter build aar
的方法可提供使用。
三、混合開發的最大痛點是什麼?
確定是堆棧管理!!! 因此項目開發了 flutter_boost
來解決這個問題。
engine
,切換 Surface
渲染顯示。Activity
就是一個 Surface
,不渲染的頁面經過截圖緩存畫面。
flutter_boost
截止到我測試的時間 2019-05-16, 只支持 1.2以前的版本
混合開發除了集成到原生工程,也有將原生控件集成到 Flutter 渲染樹裏裏的需求。
首先咱們看看沒有 PlatformView
以前是如何實現 WebView
的,這樣會有什麼問題?
以下圖所示,事實上 dart 中僅僅是用了一個 SingleChildRenderObjectWidget
用於佔位,將大小傳遞給原生代碼,而後在原生代碼裏顯示出來而已。
這樣的時候一定會代碼畫面堆棧問題,由於這個顯示脫離了 Flutter 的渲染樹,經過出現動畫確定會不一致。
AndroidView -> TextureLayer,利用Android 上的副屏顯示與虛擬內存顯示原理。
共享內存,實時截圖渲染技術。
存在問題,耗費內存,頁面複雜時慢。
這部分由於以前之前聊過,就不贅述了
RN由於是原生控件,因此在react 和react native 整合這件事上存在難度。
flutter 做爲一個UI 框架,與平臺無關,在web上利用的是dart2js的能力。 好比Image
一、某些功能頁面,能夠一套代碼實現,利用插件安裝引入,在web、移動app、甚至 pc 上,均可以編譯出對應平臺的高性能代碼,而不會像 Weex 等同樣存在各類兼容問題。
二、在應用上能夠快速實現「降級策略」,好比某種狀況下應用產生奔潰了,能夠替換爲同等 UI 的 h5 顯示,而這些代碼只須要維護一份。
Github : github.com/CarGuo
RTC社區 : rtcdeveloper.com
開源 Flutter 完整項目:github.com/CarGuo/GSYG…
開源 Flutter 多案例學習型項目: github.com/CarGuo/GSYF…
開源 Fluttre 實戰電子書項目:github.com/CarGuo/GSYF…