回顧了這段時間解答關于 Flutter 的各種問題后,我突然發現很多剛剛接觸 Flutter 的萌新,對于 Flutter 都有著不同程度的誤解,而每次重復的解釋又十分浪費時間,最終我還是決定寫篇文章來做個總結。
內容有點長,但是相信能幫你更好地去認識 Flutter 。
Flutter 的起源
Flutter 的誕生其實比較有意思,Flutter 誕生于 Chrome 團隊的一場內部實驗, 谷歌的前端團隊在把前端一些“亂七八糟“的規范去掉后,發現在基準測試里性能居然提高了 20 倍,機緣巧合下 Flutter 就這么被立項。
所以 Flutter 是基于前端誕生,同時基于它的誕生緣由,可以看到 Flutter 本身就不會有特別多的語法糖,作為框架它比較“保守”,選擇的 Dart 語言也是保守型的語言。而它的編程模式,語法都帶有濃厚的前端色彩,可是它卻最先運用在移動客戶端的開發。
所以當 Flutter 面世的時候,就需要面對一個很尷尬的狀態:
對于客戶端原生開發而言,聲明式的開發方式一上手就不習慣,習慣了代碼與布局分離(java\kotlin + xml )和命令式的對象編程,聲明式開發需要額外的學習成本;同時也覺得 Flutter 的嵌套很“惡心”。
對于前端開發而言,Flutter 的環境配置很煩人,除了 VSCode 和 Flutter SDK 之外,還需要原生的如 Java 、Gradle 、Android SDK 、XCode 等“出圈”的環境變量(時不時遇上網絡問題),而且 Flutter 所需要的原生平臺知識點對前端來說很不友好;同時也覺得 Flutter 的嵌套很“惡心”。
發現沒有?我沒有說 Dart 語言是學習成本,因為無論對于擅長 JS 的前端而言,還是對于掌握 Java\Kotlin\Swift 的客戶端而言,Dart 無論怎么看都是“弟弟”。
另外不管是前端還是客戶端,都會對 Flutter 的嵌套很“惡心”做出抨擊,但是嵌套問題嚴重嗎?這個我們后面會聊到。
綜上所述, Flutter 對于前端入坑或者客戶端入坑的萌新來說,都會有一定程度的門檻和心理抵觸。那對于前端或者客戶端來說,有沒有必須要學習 Flutter 呢?
學習 Flutter 的理由
在我接觸在大多 Flutter 萌新里,有很大一部分其實是“被迫”使用 Flutter,因為領導或者老板要求用 Flutter ,所以不得不“欲拒還迎”地開始學習 Flutter,這就是最“有力的”理由之一 :“老板(領導)要”,除非你選擇“跳槽”飛出三界。
1、個人競爭力層面
其實開發這個圈子很有意思,我們經常在長時間使用一項技術后,很容易就覺得這項技術很火,因為周邊的人都在用,而其他的框架要涼,因為沒人用的錯覺,特別是在“媒體”的煽動下,“孕婦效應”很容易就帶來認知上的誤解。
去年中旬我在 《國內大廠在移動端跨平臺的框架接入分析》 就針對 53 個樣本做過簡單的數據分析,可以看到其中 flutter(19) 、weex(17)、react-native(22) ,同時下圖是在個人手機用 libChecker
統計出來使用 Flutter 的生產應用。
介紹這個只要是想表達:Flutter 現在已經不是曾經的小眾框架,這兩年里它已經逐步成為主流的跨平臺開發框架之一。
所以 Flutter 確確實實可以成為你找工作的一個幫助,當然我并不推薦你從零開始學習 Flutter ,因為 Flutter 本身只是一個跨平臺 UI 框架。
理解上面這句話很重要,因為他可以避免你被“販賣焦慮”, Flutter 盡管支持移動端、Web 端和 PC 端,但是作為 UI 框架,它主要幫助我們解決的是 UI 和部分業務邏輯的“跨平臺”, 而和平臺相關的諸如藍牙、平臺交互、數據存儲、打包構建等等都離不開原生的支持。
現階段的跨平臺框架,不管的 Flutter 還是 react-native 和 weex ,它們的定位都是 UI 框架,它們解決的是 UI 業務跨平臺的成本,它們的發展都離不開原生平臺開發的支持。
如果原生平臺都掛了,那還跨個蛋?比如現在誰還會說要跨 WinPhone ?所以 Flutter 和原生平臺應該是相互成長的局勢,而不是那些《xxx制霸,###要涼的》的“節奏黨”,都是寄生和共生的關系,沒有對應平臺的開發經驗,是很難把 Flutter 用得“愉悅”。
不過現在 Flutter 確確實實可以幫助到你的職業發展,因為通過 Flutter 放大你的業務開發能力,讓你參與到更多的平臺開發中,不過是大前端還是KPI。當然這些 react-native、 uni-app 也可以帶給你,甚至對于前端開發來說可能更低,那為什么還要選擇 Flutter 呢?
事實上還有一個有意思的點,對于 Android 原生開發來說,學會 Flutter 等于學會了 70% 以上的 Jetpack Compose 。
2、Flutter 的一致性
事實上從我個人一直比較推薦客戶端學 Flutter ,因為對于前端來說 react-native、 uni-app 確實是性價更高的,當然好像各位的領導和老板們不是這么覺得。
那么使用 Flutter 有什么額外的好處呢?那就是 Flutter 的性能和一致性。
因為 Flutter 作為 UI 框架,它是真的跨平臺! 為什么要強掉 “真·跨平臺” ,因為和 react-native 、 weex 不同,Flutter 的控件不是通過原生控件去實現的渲染,而是由 Flutter Engine 提供的平臺無關的渲染能力,也就是 Flutter 的控件和平臺沒關系。
簡單來說,原生平臺提供一個
Surface
作為畫板,之后剩下的只需要由 Flutter 來渲染出對應的控件,而這個過程最終是打包成 AOT 的二進制完成。
所以 Flutter 的 UI 控件可以做到所見即所得,這個對我個人來說是很重要的進步。為什么這么說呢?這時候就需要拿 react-native 來做對比。
因為 react-native 是通過將 JS 里的控件轉化為原生控件進行渲染,所以 rn 里的控件是需要依賴原生平臺的控件,所以不同系統之間原生控件的差異,同個系統的不同版本在控件上的屬性和效果差異,組合起來在后期開發過程中就是很大的維護成本。
在我 react-native 開發生涯中,就經常出現:
- 在 iOS 上調試好的樣式,在 Android 上出現了異常;
- 在 Android 上生效的樣式,在 iOS 上沒有支持;
- 在 iOS 平臺的控件效果,在 Android 上出現了不一樣的展示,比如下拉刷新,
Appbar
等;
當然,這些問題最終都可以通過 if
else
和自定義平臺控件來解決,但是隨著項目的發展,這樣的結果無疑違背了我使用跨平臺的初衷。
而 Flutter 的控件特性決定了它沒有這些問題,我甚至經常只在 iOS 模擬器上開發測試所有界面邏輯,而不用擔心 Android 上的兼容,當然屏幕大小的適配是不可避免的。
從這個角度上不嚴謹地說, Flutter 更像是一個類 unity 的輕度游戲引擎,不過它提供的是 2D 的控件。
當然,Flutter 這樣實現也有壞處,那就是當你需要使用平臺的控件作為混合開發時,Flutter 的成本和體驗無疑被放大 ,這一點上 react-native 反而有著先天的優勢。
3、Flutter 的性能
其實前面也介紹過 Flutter 的性能一般情況下是比 react-native 好,關于這個也有 《Flutter vs React Native vs Native:深度性能比較》 的文章做深入的對比,這里主要介紹幾個誤區:
1、Flutter 在 debug 和 release 下的性能差距是巨大的,因為它們之間是 JIT 和 AOT 的區別。
2、不要在模擬器上測試性能,這個根本沒有意義,因為在手機上 Flutter 會更多依賴 GPU 的能力。
3、混合開發 Flutter 是有性能有影響的,比如在原有 Android 項目里,把某個模塊業務邏輯改用 Flutter 實現,這對性能和內存會有很大的考驗,至于為什么?就是前面說過 Flutter 獨立的控件渲染和堆棧管理帶來的負面效果。
4、同一個框架在不同人手下會寫出不一樣的結果,一般情況下對于普通開發者來說,流行的框架一般不會帶來很大的性能瓶頸,反而是開發能力比較多導致項目的瓶頸。
怎么學 Flutter ?
當你快速搭建好環境,簡單了解 Flutter 的 API 之后,學習 Flutter 在我看來主要有兩個核心點:響應式開發和 Widget 的背后是什么?
1、響應式開發
響應式開發相信對于前端來說再熟悉不過,這部分內容對于前端開發來說其實可以略過,響應式編程也叫做聲明式編程,這是現在前端開發的主流,當然對于客戶端開發的一種趨勢,比如 Jetpack Compose 、SwiftUI 。
Jetpack Compose 和 Flutter 的相似程度絕對讓你驚訝。
什么是響應式開發呢?簡單來說其實就是你不需要手動更新界面,只需要把界面通過代碼“聲明”好,然后把數據和界面的關系接好,數據更新了界面自然就更新了。
從代碼層面看,對于原生開發而言,響應式開發中沒有 xml 的布局,布局完全由代碼完成,所見即所得,同時你也不會需要操作界面“對象”去進行賦值和更新,你所需要做的就是配置數據和界面的關系。
舉個例子:
- 以前在 Android 上你需要寫一個 xml ,然后布局一個
TextView
,通過findViewById
得到這個對象,再調用setText
去賦值; - 現在 Flutter 里,你只需要聲明一個
Text
的Widget
,并把data.title
這樣的數據配置給Text
,當數據改變了,Text
的顯示內容也隨之改變;
對于 Android 開發而言,大家可能覺得這不就是 MVVM
下的 DataBinding
也一樣嗎?其實還不大一樣,更形象的例子,這里借用扔物線大佬在谷歌大會關于 Jetpack Compose 的分享,為什么 Data Binding
模式不是響應式開發:
因為
Data Binding
(不管是這個庫還是這種編程模式)并不能做到「聲明式 UI」,或者說 聲明式 UI 是一種比數據綁定更強的數據綁定,比如在 Compose 里你除了簡單地綁定字符串的值,還可以用布爾類型的數據來控制界面元素是否存在,例如再創建另外一個布爾類型的變量,用它來控制你的某個文字的顯示:
注意,當
show
先是true
然后又變成false
的時候,不是設置了一個setVisibility(GONE)
這樣的做法,而是直接上面的Text()
在界面代碼中消失了,每次數據改變所導致的界面更新看起來就跟界面關閉又重啟、并用新的數據重新初始化了一遍一樣,這才叫聲明式 UI,這是數據綁定做不到的。當然 Compose 并不是真的把界面重啟了,它只會刷新那些需要刷新的部分,這樣的話就能保證,它自動的更新界面跟我們手動更新一樣高效。
在 Flutter 中也類似,當你通過這樣的 ture
和 false
去布局時,是直接影響了 Widget
樹的結構乃至更底層的渲染邏輯,所以作為 Android 開發在學習 Flutter 的時候,就需要習慣這種開發模式,“放棄” 在獲取數據后,想要保存或者持有一個界面控件進行操作的想法。另外在 Flutter 中,持有一個 Widget 控件去修改大部分時候是沒意義的,也是接下來我們要聊的內容。
2、Widget 的背后
Flutter 內一切皆 Widget
,Widget
是不可變的(immutable),每個 Widget
狀態都代表了一幀。
理解這段話是非常重要的,這句話也是很多一開始接觸 Flutter 的開發者比較迷惑的地方,因為 Flutter 中所有界面的展示效果,在代碼層面都是通過 Widget
作為入口開始。
Widget
是不可變的,說明頁面發生變化時 Widget
一定是被重新構建, Widget
的固定狀態代表了一幀靜止的畫面,當畫面發生改變時,對應的 Widget 一定會變化。
舉個我經常說的例子,如下代碼所示定義了一個 TestWidget
,TestWidget
接受傳入的 title
和 count
參數顯示到 Text
上,同時如果 count
大于 99,則只顯示 99。
/// Warnning
/// This class is marked as '@immutable'
/// but one or more of its instance fields are not final
class TestWidget extends StatelessWidget {
final String title;
int count;
TestWidget({this.title, this.count});
@override
Widget build(BuildContext context) {
this.count = (count > 99) ? 99 : count;
return Container(
child: new Text("$title $count"),
);
}
}
這段代碼看起來沒有什么問題,也可以正常運行,但是在編譯器上會有 “This class is marked as '@immutable',but one or more of its instance fields are not final” 的提示警告,這是因為 TestWidget
內的 count
成員變量沒有加上 final
聲明,從而在代碼層面容易產生歧義。
因為前面說過
Widget
是immutable
,所以它的每次變化都會導致自身被重新構建,也就是TestWidget
內的count
成員變量其實是不會被保存且二次使用。
如上所示代碼中 count
成員沒有 final
聲明,所以理論是可以對 count
進行二次修改賦值,造成 count
成員好像被保存在 TestWidget
中被二次使用的錯覺,容易產生歧義,比如某種情況下的 widget.count
,所以需要加這個 final
就可以看出來 Widget
的不可變邏輯。
如果把 StatelessWidget
換成 StatefulWidget
,然后把 build
方法放到 State
里,State
里的 count
就可以就可以實現跨幀保存。
class TestWidgetWithState extends StatefulWidget {
final String title;
TestWidgetWithState({this.title});
@override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidgetWithState> {
int count;
@override
Widget build(BuildContext context) {
this.count = (count > 99) ? 99 : count;
return InkWell(
onTap: () {
setState(() {
count++;
});
},
child: Container(
child: new Text("${widget.title} $count"),
),
);
}
}
所以這里最重要的是,首先要理解 Widget
的不可變性質,然后知道了通過 State
就可以實現數據的跨 Widget
保存和恢復,那為什么 State
就可以呢?
這就涉及到 Flutter 中另外一個很重要的知識點,Widget
的背后又是什么?事實上在 Flutter 中 Widget 并不是真正控件,在 Flutter 的世界里,我們最常使用的 Widget
其實更像是配置文件,而在其后面的 Element
、RenderObject
、Layer
等才是實際“干活”的對象。
Element
、RenderObject
、Layer
才是需要學習理解的對象。
簡單舉個例子,如下代碼所示,其中 testUseAll
這個 Text
在同一個頁面下在三處地方被使用,并且代碼可以正常運行渲染,如果是一個真正的 View
,是不能在一個頁面下這樣被多個地方加載使用的。
在 Flutter 設定里,Widget
是配置文件告訴 Flutter 你想要怎么渲染, Widget
在 Flutter 里會經過 Element
、RenderObject
、乃至 Layer
最終去進行渲染,所以作為配置文件的 Widget
可以是 @immutable
,可以每次狀態更新都被重構。
所以回到最初說過的問題:Flutter 的嵌套很惡心?是的 Flutter 設定上確實導致它會有嵌套的客觀事實,但是當你把 Widget
理解成配置文件,你就可以更好地組織代碼,比如 Flutter 里的 Container
就是一個抽象的配置模版。
參考
Container
你就學會了 Flutter 組織代碼邏輯的第一步。
同時因為 Widget
并不是真正干活的,所以嵌套事實上并不是嵌套 View
,一般情況下 Widget
的嵌套是不會帶來什么性能問題,因為它不是正式干活的,嵌套不會帶來嚴重的性能損失。
舉個例子,當你寫了一堆的 Widget
被加載時,第一次會對應產生出 Element
,之后 Element
持有了 Widget
和 RenderObject
。
簡單的來說,一般情況下畫面的改變,就是之后 Widget
的變化被更新到 RenderObject
,而在 Flutter 中能夠跨幀保存的 State
,其實也是被 Element
所持有,從而可以用來跨 Widget
保存數據。
所以
Widget
的嵌套一般不會帶來性能問題,每個Widget
狀態都代表了一幀,可以理解為這個“配置信息”代表了當前的一個畫面,在Widget
的背后,嵌套的Padding
、Align
這些控件,最后只是canvas
時的一個“偏移計算”而已。
所以理解 Widget
控件很重要,Widget
不是真正的 View
,它只是配置信息,只有理解了這點,你才會發現 Flutter 更廣闊的大陸,比如:
- Flutter 的控件是從
Elemnt
才開始是真正的工作對象; - 要看一個
Widget
的界面效果是怎么實現,應該去看它對應的RenderObejcet
是怎么繪制的; - 要知道不同堆棧或者模塊的頁面為什么不會互相干擾,就去看它的
Layer
是什么邏輯; - 是不是所有的
Widget
都有RenderObejcet
?Widget
、Elemnt
、RenderObejcet
、Layer
的對應關系是什么?
這些內容才是學 Flutter 需要如理解和融匯貫通的,當你了解了關于 Widget
背后的這一套復雜的邏輯支撐后,你就會發現 Flutter 是那么的簡單,在實現復雜控件上是那么地簡單,Canvas
組合起來的能力是真的香。
當然具體展開這部分內容不是三言兩語可以解釋完,在我出版的 《Flutter開發實戰詳解》 中第三章和第四章就著重講解的內容,也是這出版本書主要的靈魂之處,這部分內容不會因為 Flutter 的版本迭代而過時的內容。
這算做了個小廣告??
Flutter 是個有坑的框架
最后講講 Flutter 的坑,事實上沒有什么框架是沒有坑的,如果框架完美得沒有問題,那我們競爭力反而會越來越弱,可替換性會更高。
這也是為什么一開始 Andorid 和 iOS 開發很火熱,而現在客戶端開發招聘回歸理性的原因,因為這個領域已經越來越成熟,自然就“卷”了。
事實上我一直覺得使用框架的我們并沒有什么特殊價值,而解決使用框架所帶來的問題才是我們特有的價值。
而 Flutter 的問題也不少,比如:
WebView
的問題:Flutter 特有的 UI 機制,導致了 Flutter 需要通過特殊的方式來接入比如WebView
、MapView
這樣的控件,而這部分也導致了接入后不斷性能、鍵盤、輸入框等的技術問題,具體可以參考:《Hybrid Composition 深度解析》 和 《 Android PlatformView 和鍵盤問題》 。圖片處理和加載:在圖片處理和加載上 Flutter 的能力無疑是比較弱的,同時對于單個大圖片的加載和大量圖片列表的顯示處理上,Flutter 很容易出現內存和部分 GPU 溢出的問題。而這部分問題處理起來特別麻煩,如果需要借用原生平臺來解決,則需要通過外界紋理的方式來完成,而這個實現的維護成本并不低。
混合開發是避免不了的話題:因為 Flutter 的控件和頁面堆棧都脫離原生平臺,所以混合開發的結果就會導致維護成本的提高,現在較多使用的
flutter_boost
和flutter_thrio
都無法較好的真正解決混合開發中的痛點,所以對于 Flutter 來說這也是一大考驗。
然而事實上在我收到關于 Flutter 的問題里,反而大部分和 Flutter 是沒有關系的,比如:
- “
flutter doctor
運行之后卡住不動” - “
flutter run
運行之后出現報錯” - “
flutter pub get
運行之后為什么提示 dart 版本不對” - “運行后出現 Gradle 報錯,顯示 timeout 之類問題”
- “iOS 沒辦法運行到真機上”
- “xxx這樣的控件有沒有現成的”
····
說實話,如果是這些問題,我覺得這并不是 Flutter 的問題,大部分時候是看 log 、看文檔和網絡的問題,甚至僅僅是搜索引擎檢索技術的問題。。。。
雖然 Flutter 有著這樣那樣的問題,但是綜合考慮下來,它對我來現階段確實是最合適的 UI 框架。
最后
很久沒寫這么長的內容了,一般寫這么長的內容能看完的人也不多,只是希望這篇文章能讓你更全面地去理解 Flutter ,或者能幫你找到 Flutter 學習的方向,最后借用某位大佬說過的一句話:
“能大規模商用的技術,都不需要太高的智商,否則這種技術就不可能規模化。某些程序員們,請停止你們的蜜汁自信。”