帶你全面瞭解 Flutter,它好在哪裡?它的坑在哪裡? 應該怎麼學?

戀貓de小郭發表於2021-02-22

回顧了這段時間解答關於 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 裡,你只需要宣告一個 TextWidget ,並把 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 中也類似,當你通過這樣的 turefalse 去佈局時,是直接影響了 Widget 樹的結構乃至更底層的渲染邏輯,所以作為 Android 開發在學習 Flutter 的時候,就需要習慣這種開發模式,“放棄” 在獲取資料後,想要儲存或者持有一個介面控制元件進行操作的想法。另外在 Flutter 中,持有一個 Widget 控制元件去修改大部分時候是沒意義的,也是接下來我們要聊的內容

2、Widget 的背後

Flutter 內一切皆 WidgetWidget 是不可變的(immutable),每個 Widget 狀態都代表了一幀。

理解這段話是非常重要的,這句話也是很多一開始接觸 Flutter 的開發者比較迷惑的地方,因為 Flutter 中所有介面的展示效果,在程式碼層面都是通過 Widget 作為入口開始。

Widget 是不可變的,說明頁面發生變化時 Widget 一定是被重新構建, Widget 的固定狀態代表了一幀靜止的畫面,當畫面發生改變時,對應的 Widget 一定會變化。

舉個我經常說的例子,如下程式碼所示定義了一個 TestWidgetTestWidget 接受傳入的 titlecount 引數顯示到 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 宣告,從而在程式碼層面容易產生歧義。

因為前面說過 Widgetimmutable ,所以它的每次變化都會導致自身被重新構建,也就是 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 其實更像是配置檔案,而在其後面的 ElementRenderObjectLayer 等才是實際“幹活”的物件。

ElementRenderObjectLayer 才是需要學習理解的物件。

簡單舉個例子,如下程式碼所示,其中 testUseAll 這個 Text 在同一個頁面下在三處地方被使用,並且程式碼可以正常執行渲染,如果是一個真正的 View ,是不能在一個頁面下這樣被多個地方載入使用的。

在 Flutter 設定裡,Widget 是配置檔案告訴 Flutter 你想要怎麼渲染, Widget 在 Flutter 裡會經過 ElementRenderObject、乃至 Layer 最終去進行渲染,所以作為配置檔案的 Widget 可以是 @immutable,可以每次狀態更新都被重構。

所以回到最初說過的問題:Flutter 的巢狀很噁心?是的 Flutter 設定上確實導致它會有巢狀的客觀事實,但是當你把 Widget 理解成配置檔案,你就可以更好地組織程式碼,比如 Flutter 裡的 Container 就是一個抽象的配置模版。

參考 Container 你就學會了 Flutter 組織程式碼邏輯的第一步。

同時因為 Widget 並不是真正幹活的,所以巢狀事實上並不是巢狀 View ,一般情況下 Widget 的巢狀是不會帶來什麼效能問題,因為它不是正式幹活的,巢狀不會帶來嚴重的效能損失。

舉個例子,當你寫了一堆的 Widget 被載入時,第一次會對應產生出 Element ,之後 Element 持有了 WidgetRenderObject

簡單的來說,一般情況下畫面的改變,就是之後 Widget 的變化被更新到 RenderObject ,而在 Flutter 中能夠跨幀儲存的 State ,其實也是被 Element 所持有,從而可以用來跨 Widget 儲存資料。

所以 Widget 的巢狀一般不會帶來效能問題,每個 Widget 狀態都代表了一幀,可以理解為這個“配置資訊”代表了當前的一個畫面,在 Widget 的背後,巢狀的 PaddingAlign 這些控制元件,最後只是 canvas 時的一個“偏移計算”而已。

所以理解 Widget 控制元件很重要,Widget 不是真正的 View ,它只是配置資訊,只有理解了這點,你才會發現 Flutter 更廣闊的大陸,比如:

  • Flutter 的控制元件是從 Elemnt 才開始是真正的工作物件;
  • 要看一個 Widget 的介面效果是怎麼實現,應該去看它對應的 RenderObejcet 是怎麼繪製的;
  • 要知道不同堆疊或者模組的頁面為什麼不會互相干擾,就去看它的 Layer 是什麼邏輯;
  • 是不是所有的 Widget 都有 RenderObejcetWidgetElemntRenderObejcetLayer 的對應關係是什麼?

這些內容才是學 Flutter 需要如理解和融匯貫通的,當你瞭解了關於 Widget 背後的這一套複雜的邏輯支撐後,你就會發現 Flutter 是那麼的簡單,在實現複雜控制元件上是那麼地簡單,Canvas 組合起來的能力是真的香。

當然具體展開這部分內容不是三言兩語可以解釋完,在我出版的 《Flutter開發實戰詳解》 中第三章和第四章就著重講解的內容,也是這出版本書主要的靈魂之處,這部分內容不會因為 Flutter 的版本迭代而過時的內容。

這算做了個小廣告??

Flutter 是個有坑的框架

最後講講 Flutter 的坑,事實上沒有什麼框架是沒有坑的,如果框架完美得沒有問題,那我們競爭力反而會越來越弱,可替換性會更高。

這也是為什麼一開始 Andorid 和 iOS 開發很火熱,而現在客戶端開發招聘迴歸理性的原因,因為這個領域已經越來越成熟,自然就“卷”了。

事實上我一直覺得使用框架的我們並沒有什麼特殊價值,而解決使用框架所帶來的問題才是我們特有的價值。

而 Flutter 的問題也不少,比如:

  • WebView 的問題:Flutter 特有的 UI 機制,導致了 Flutter 需要通過特殊的方式來接入比如 WebViewMapView 這樣的控制元件,而這部分也導致了接入後不斷效能、鍵盤、輸入框等的技術問題,具體可以參考:《Hybrid Composition 深度解析》《 Android PlatformView 和鍵盤問題》

  • 圖片處理和載入:在圖片處理和載入上 Flutter 的能力無疑是比較弱的,同時對於單個大圖片的載入和大量圖片列表的顯示處理上,Flutter 很容易出現記憶體和部分 GPU 溢位的問題。而這部分問題處理起來特別麻煩,如果需要借用原生平臺來解決,則需要通過外界紋理的方式來完成,而這個實現的維護成本並不低。

  • 混合開發是避免不了的話題:因為 Flutter 的控制元件和頁面堆疊都脫離原生平臺,所以混合開發的結果就會導致維護成本的提高,現在較多使用的 flutter_boostflutter_thrio 都無法較好的真正解決混合開發中的痛點,所以對於 Flutter 來說這也是一大考驗。

然而事實上在我收到關於 Flutter 的問題裡,反而大部分和 Flutter 是沒有關係的,比如:

  • flutter doctor 執行之後卡住不動”
  • flutter run 執行之後出現報錯”
  • flutter pub get 執行之後為什麼提示 dart 版本不對”
  • “執行後出現 Gradle 報錯,顯示 timeout 之類問題”
  • “iOS 沒辦法執行到真機上”
  • “xxx這樣的控制元件有沒有現成的”

····

說實話,如果是這些問題,我覺得這並不是 Flutter 的問題,大部分時候是看 log 、看文件和網路的問題,甚至僅僅是搜尋引擎檢索技術的問題。。。。

雖然 Flutter 有著這樣那樣的問題,但是綜合考慮下來,它對我來現階段確實是最合適的 UI 框架。

最後

很久沒寫這麼長的內容了,一般寫這麼長的內容能看完的人也不多,只是希望這篇文章能讓你更全面地去理解 Flutter ,或者能幫你找到 Flutter 學習的方向,最後借用某位大佬說過的一句話:

“能大規模商用的技術,都不需要太高的智商,否則這種技術就不可能規模化。某些程式設計師們,請停止你們的蜜汁自信。”

相關文章