因為最近公司來了新人,之前很少接觸過跨平臺應用開發,所以為了給他們介紹關于 Flutter 的一些基礎,這里特意整理了一份通用性質的常識性講解,結尾順便介紹一個有趣的案例。
一、單頁面應用
了解 Flutter 之前,首先介紹一個簡單基礎知識點,那就是大部分的移動端跨平臺框架都是“單頁面”應用。
什么是“單頁面”應用?也就是對于原生 Android 和 iOS 而言,整個跨平臺 UI 默認都是運行在一個 Activity
/ ViewController
上面,默認情況下只會有一個 Activity
/ ViewController
, Flutter、 ReactNative 、Weex 、Ionic 默認情況下都是如此,所以一般情況下框架的路由和原生的路由是沒有直接關系。
舉個例子,如下圖所示,
- 在當前 Flutter 端路由堆棧里有
FlutterA
和FlutterB
兩個頁面 Flutter 頁面; - 這時候打開新的
Activity
/ViewController
,啟動了原生頁面X,可以看到原生頁面X 作為新的原生頁面加入到原生層路由后,把FlutterActivity
/FlutterViewController
給擋住,也就是把FlutterA
和FlutterB
都擋住; - 這時候在 Flutter 層再打開新的
FlutterC
頁面,可以看到依然會被原生頁面X擋住;
所以通過這部分內容可以看出來,跨平臺應用默認情況下作為單頁面應用,他們的路由堆棧是和原生層存在不兼容的隔離。
當然這里面重復用了一個詞:“默認”,也就是其實可以支持自定義混合堆棧的,比如官方的
FlutterEngineGroup
,第三方框架flutter_boost
、mix_stack
、flutter_thrio
等等。
二、渲染邏輯
介紹完“單頁面”部分的不同,接下來講講 Flutter 在渲染層面的不同。
在渲染層面 Flutter 和其他跨平臺框架存在較大差異,如下圖所示是現階段常見的渲染模式對比:
- 對于原生 Android 而言,是原生代碼經過 skia 最后到 GPU 完成渲染繪制,Android 原生系統本身自帶了 skia;
- 對于 Flutter 而言,Dart 代碼里的控件經過 skia 最后到 GPU 完成渲染繪制,這里在 Andriod 上使用的系統的 skia ,而在 iOS 上使用的是打包到項目里的 skia ;
- 對于 ReactNative/Weex 等類似的項目,它們是運行在各自的 JS 引擎里面,最后通過映射為原生的控件,利用原生的渲染能力進行渲染;
- 對于 ionic 等這類 Hybird 的跨平臺框架,使用的主要就是 WebView 的渲染能力;
skia 在 Android 上根據不同情況就可能會是
OpenGL
或者Vulkan
,在 iOS 上如果有支持Metal
也會使用Metal
加速渲染。
通過前面的介紹,可以看出了:
ReactNative/Weex
這類跨平臺和原生平臺存在較大關聯:
- 好處就是:如果需要使用原生平臺的控件能力,接入成本會比較低;
- 壞處自然就是: 渲染嚴重依賴平臺控件的能力,耦合較多,不同系統之間原生控件的差異,同個系統的不同版本在控件上的屬性和效果差異,組合起來在后期開發過程中就是很大的維護成本。、
例如:在 iOS 上調試好的樣式,在 Android 上出現了異常;在 Android 上生效的樣式,在 iOS 上沒有支持;在 iOS 平臺的控件效果,在 Android 上出現了不一樣的展示,比如下拉刷新,Appbar等;
Flutter
與之不同的地方就是渲染直接利用 skia 和 GPU 交互,在 Android 和 iOS 平臺上實現了平臺無關的控件,簡單說就是 Flutter
里的 Widget
大部分都是和 Android 和 iOS 沒有關系。
本質上原生平臺是提供一個類似 Surface
的畫板,之后剩下的只需要由 Flutter 來渲染出對應的控件
一般是使用
FlutterView
作為渲染承載,它在 Android 上內部使用可以是SurfaceView
、TextureView
或者FlutterImageView
;在 iOS 上是UIView
通過Layer
實現的渲染。
所以 Flutter 的控件在不同平臺可以得到一致效果,但是和原生控件進行混合也會有較高的成本和難度,在接入原生控件的能力上,Flutter 提供了 PlatformView
的機制來實現接入, PlatformView
本身的實現會比較容易引發內存和鍵盤等問題,所以也帶來了較高的接入成本。
三、項目結構
如上圖所示,默認情況下 Flutter 工程結構是這樣的:
-
android
原生的工程目錄,可以配置原生的appName
,logo
,啟動圖,AndroidManifest
等等; -
ios
工程目錄,配置啟動圖,logo
,應用名稱,plist
文件等等; -
build
目錄,這個目錄是編譯后出現,一般是 git 的 ignore 目錄,打包過程和輸入結果都在這個目錄下,Android 原生的打包過程輸出也被重定向輸出到這里; -
lib
目錄,用來寫 dart 代碼的,入口文件一般是main.dart
; -
pubspec.yaml
文件,Flutter 工程里最重要的文件之一,不管是靜態資源引用(圖片,字體)、第三方庫依賴還是 Dart 版本聲明都寫在這里。
如下圖是使用是關于 pubspec.yaml
文件的結構介紹
需要注意,當這個文件發生改變時,需要重新執行
flutter pub get
,并且stop
應用之后重新運行項目,而不是使用hotload
。
如下所示是 Flutter 的插件工程,Flutter 中分為 Package
和 Plugin
,如果是
-
Package
項目屬于 Flutter 包工程,不會包含原生代碼; -
Plugin
項目屬于 Flutter 插件工程,包含了 Android 和 iOS 代碼;
四、打包調試
Flutter 運行之前都需要先執行 flutter pub get
來先同步下載第三方代碼,下載的第三方代碼一般存在于(Mac) /Users/你的用戶名/.pub-cache
目錄下 。
下載依賴成功后,可以直接通過 flutter run
或者 IDE 工具點擊運行來啟動 Flutter 項目,這個過程會需要原生工程的一些網絡同步工作,比如:
- Android 上的 Gradle 和 aar 依賴包同步;
- iOS 上的需要 pod install 同步一些依賴包;
如果需要在項目同步過程中查看進度:
- Android 可以到
android/
目錄下執行./gradlew assembleDebug
查看同步進度; - iOS 可以到
ios/
目錄下執行pod install
,查看下載進度;
同步的插件中,如果是 Plugin
帶有原生平臺的代碼邏輯,那么可以在項目根目錄下看到一個叫做 .flutter_plugins
和 .flutter-plugins-dependencies
的文件,它們是 git ignore 的文件,Android 和 iOS 中會根據這個文件對本地路徑的插件進行引用,后面 Flutter 運行時會根據這個路徑動態添加依賴。
默認情況下 Flutter 在 debug 下是 JIT 的運行模式所以運行效率會比較低,速度相對較慢,但是可以 hotload。
在 release 下是 AOT 模式,運行速度會快很多,同時 Flutter 在模擬器上一般默認會使用 CPU 運行,在真機上會使用 GPU 運行,所以性能表現也不同。
另外 iOS 14 真機上 debug 運行,斷后鏈接后再次啟動是無法運行的。
如果項目存在緩存問題,可以直接執行 flutter clean
來清理緩存。
最后說下 Flutter 的為什么不支持熱更新?
前面講過 ReactNative 和 Weex 是通過將 JS 代碼里的控件轉化為原生控件進行渲染,所以本質上 JS 代碼部分都只是文本而已,利用 code-push
推送文本內容本質上并不會違法平臺要求。
而 Flutter 打包后的文件是二進制文件,推送二進制文件明顯是不符合平臺要求的。
release 打包后的 Android 會生成
app.so
和flutter.so
兩個動態庫;iOS 會生成App.framework
和Flutter.framework
兩個文件。
五、Flutter 簡單介紹
最后簡單介紹下 Flutter Dart 部分相關的內容,對于原生開發來說,Flutter 主要優先了解這三點:響應式、Widget
和狀態管理 。
響應式
響應式編程也叫做聲明式編程,這是現在前端開發的主流,當然對于客戶端開發的一種趨勢,比如 Jetpack Compose
、SwiftUI
。
Jetpack Compose 和 Flutter 的在某些表層上看真的很相似。
響應式簡單來說其實就是你不需要手動更新界面,只需要把界面通過代碼“聲明”好,然后把數據和界面的關系接好,數據更新了界面自然就更新了。
從代碼層面看,對于原生開發而言,沒有 xml
的布局,沒有 storyboard
,布局完全由代碼完成,所見即所得,同時也不會需要操作界面“對象”去進行賦值和更新,你所需要做的就是配置數據和界面的關系。
響應式開發比數據綁定或者 MVVM 不同的地方是,它每次都是重新構建和調整整個渲染樹,而不是簡單的對 UI 進行
visibility
操作。
Widget
Widget
是 Flutter 里的基礎概念,也是我們寫代碼最直接接觸的對象,Flutter 內一切皆 Widget ,Widget 是不可變的(immutable),每個 Widget 狀態都代表了一幀。
所以 Widget
作為一個 immutable
對象,它不可能是真正工作的 UI 對象,在 Flutter 里真正的 View
級別對象是 Element
和 RenderObject
, 其中 Element
的抽象對象就是我們經常用到的 BuildContext
。
舉個例子,如下代碼所示,其中 testUseAll
這個 Text
在同一個頁面下在三處地方被使用,并且代碼可以正常運行渲染,如果是一個真正的 View
,是不能在一個頁面下這樣被多個地方加載使用的。
所以 Flutter 中 Widget
更多只是配置文件的地位,用于描述界面的配置代碼,具體它們的實現邏輯、關系還有分類,可以看我寫的書 《Flutter開發實戰詳解》中 的第三章和第四章部分。
狀態管理
Flutter 作為響應式開發框架,本質上它其實不再追求什么 MVC 、MVP、MVVVM 的設計模式,它更多是對界面狀態的管理。
就是要拋棄以前在原生平臺上,需要拿到
View
的對象,然后做對其進行 UI 設置這種思路。
Flutter 上更多需要管理數據的流向,比如:
- 數據是從哪里發出,然后再到哪里消費;
- 數據是單向還是雙向;
- 數據需要進過哪些中間轉化;
- 數據是從哪一層開始往下傳遞;
- 數據綁定了哪些地方;
- 如何實現多個地方的局部刷新;
因為對于界面來說,它只需要根據數據進行變化即可,我們不需要獲取它去單獨設置,所以 Flutter 中有各種數據管理和共享的框架,比較流行的有 provider
、 getx
、 flutter_redex
、flutter_mobx
等等。
有趣的問題
最后說一個比較有意思的問題,之前有人說 Flutter 里是傳遞值還是引用?這個問題看過網上有不少文章解釋得很奇怪,存在一些誤導性的解釋,其實這個問題很簡單:
Flutter 里一切皆是對象, 就連 int
、 double
、bool
也是對象,你覺得對象傳遞的是什么?
但是對于對象的操作是有區別的,比如對于 int
、 double
等 class
的 +
、-
、*
、 \
等操作,其實是執行了這個 class
的 operator
操作符的操作, 然后返回了一個 num
對象。
而對于這個操作,只需要要去 dart vm
看看 Double
對象在進行加減乘除時做了什么,如下圖所示,看完相信就知道方法里傳遞 int
、double
對象后進行操作會是什么樣的結果。