Flutter入門系列(三)---攜程Flutter實踐
文件來源:攜程技術中心
Flutter已經開源了三年,但是最近兩年才開始在開源社群活躍起來,尤其是最近還發布了Preview 1版本。作為可以實現一套程式碼同時在iOS、Android平臺上執行的又一個新的UI框架,Flutter提供給開發者的不僅僅是高速實現,還有高質量、流暢的UI。免費開源的協議對於開發者來說也很友好。
本文將從Flutter架構理念與UI渲染邏輯,來解釋為什麼Flutter的渲染效率非常高,以及從Flutter開發實踐的角度,介紹框架的特性及Flutter開發中所遇到的問題,希望給對Flutter感興趣的小夥伴在選型時一些啟發和思考,避免重複踩坑。
一、Flutter Layers
Flutter的主要設計人之一Ian Hickson,之前是HTML規範編寫者,因此Flutter的設計理念也與HTML的實現方法有很多相似之處。
Flutter最初的理念是實現跨平臺的Material Design的跨平臺框架。平臺框架大致可以分為四層:
-
dart:ui : 最底層的是UI層,由Flutter引擎所暴露的庫,可以理解為一個佈局層。
-
Rendering : 這一層是抽象的佈局層,它依賴於UI層,可以構建一個UI樹,通過更新UI樹來更新UI。
-
Material與Widgets : 最後就是Material層使用Widget層來構建UI。
起初Flutter是沒有Rendering層的,直接通過座標計算每個畫素點需要顯示什麼,這讓框架的程式碼變得特別複雜,每當UI更新的時候需要重新計算這些座標是否需要改變。後來增加Randering層來抽象UI顯示的位置,通過抽象位置來判斷畫素點是否需要更新。
在Flutter專案的初期,Dart-lang也不是特別成熟。Dart虛擬機器在垃圾回收的頻率與回收機制表現當時並不是特別好,比如當時Flutter如果執行一個時間很長的動畫,動畫結束之後所佔用的記憶體對於Flutter框架就是一個很大的垃圾。後來Dart團隊在垃圾回收上進行了很多優化,使Flutter在UI顯示更流暢。
如今,國內最大的使用廠商應該就是阿里閒魚了,在Flutter釋出Preview 1版本的時候,閒魚App也一起協同展示了他們用Flutter編寫的商品詳情頁面。我也在使用Flutter仿小米計算器開發後,體驗到release版的流暢度確實堪比原生:
(已上架Google,可以通過包名搜尋下載體驗:top.basking.calculator)
二、Flutter的UI渲染
Flutter渲染效率堪比原生,快於RN。Flutter更新UI的時候,並不是更新整個UI,而是更新所需要更新的部分。比如從網路非同步下載一個圖片,設定到“Image”(ImageView)中,如果這個Image Widget大小並沒有改變,只需要將圖片物件傳入Widget中,接著直接重新繪製這一個Widget就可以了。為了達到這樣的UI渲染理念,Flutter是如何設計的呢?
FlutterUI渲染過程
Flutter 的UI渲染過程簡單可以分為3個分支,Widget樹、Element樹、Rendering樹。
當Widget改變的時候,只有將它新增到Element樹上時,才會改變Rendering樹,展示到UI介面上。將它新增到Element樹的方法就是setState()方法,它會自動尋找改變了的Widget,然後新增到Element樹,等待後續的操作。
可以看到,矩形的子Widget並沒有改變,所以在Element樹上也沒有改變,到了Rendering樹也沒有重新渲染,這種設計理念對於重新整理UI操作可以大大提高效率。
FlutterUI渲染 —— onDraw與onLayout
與其他的UI框架渲染邏輯不同的是,Widget的Draw與Layout的順序不一定相同。比如在Android端onDraw與onLayout的順序是相同的。關於Flutter框架的渲染順序大家可以看以下的例子:
在Row Widget中有三個子Widget,其中中間的是固定寬度的Widget,還有兩個是根據剩下寬度比例佔用位置的Widget,其中綠色Widget是橙色的寬度的兩倍。而他們的layout order與rendering order如下:
這麼做是因為Flutter為了保證對於每個Widget的訪問是單一線性的。所以在layout order中Flutter框架就會先layout固定寬度的Widget,然後再layout比例寬度的Widget。接著到了Rendering樹再會根據Element樹的順序逐個對每個Widget進行渲染。
三、Flutter框架UI特性
Dart語言
Flutter的開發語言是由ChromeV8引擎團隊的領導者Lars Bak主持開發的Dart。Dart語言語法類似於C。Dart語言為了更好的適應FlutterUI框架,在記憶體分配和垃圾回收做了很多優化。
因為Dart在連續分配多個物件的時候,所需消耗的資源非常少。Dart虛擬機器可以快速分配記憶體給短期生存的物件,這樣可以使很複雜的UI在60ms內完成一幀的渲染(實際感覺每一幀渲染時間更短),這樣就保證了Flutter可以平滑的展示UI滑動及動畫等效果。Flutter團隊與Dart團隊的密切合作讓提升效率變得更加容易。
FlutterUI開發樣式
Flutter在開發UI介面的時候,又比較像HTML的標籤式語言,前文也提到,這是受Flutter創始人之一的Ian Hickson影響。其實很多UI佈局都是類似標籤的樣式來編寫的,比如Android的XML以及網頁的HTML,所以Flutter會採用這樣一個成熟的佈局開發樣式。
new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
new FlatButton(
color: Colors.blue,
)
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
Flutter外掛、依賴與包管理器
Flutter與RN一樣,在原生開發中很依賴於外掛來呼叫系統API,畢竟它是一個UI框架。但是現階段的Flutter外掛並不是像RN那麼全,可以看到維護Flutter的開發者只有200多人,而維護react-native的開發者已經近1700人了,一個數量級之差的維護者肯定在外掛數量與開發體驗上差別很大。
在包管理上,flutter並不需要依賴第三方類似於RN的npm包管理器來新增依賴,flutter本身就自帶了包管理器,只需要在pubspec.yaml檔案中新增相關依賴即可。但是,因為Google的庫在國不能訪問,需要新增環境變數指定庫映象才可以使用。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
Flutter框架特性
在程式碼實現上,Flutter並沒有Android的findViewById,頁面佈局是通過有狀態Widget(StatefulWidget)和無狀態Widget(StatelessWidget)實現的。顧名思義,無狀態的Widget就是一些不可以改變的UI,而需要改變的UI則是通過有狀態的Widget來實現,並且通過setStatus()來重新整理UI的狀態:
...
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
...
setState(() {
_counter--;
});
這種方法很簡單的實現了動態化的UI及Android長久以來希望達到的目標 —— data binding。
四、Flutter待完善的方面及使用中遇到的問題
Flutter至今沒有反射
Dart並不是沒有反射,dart:mirrors就具有Mirror概念的反射。在安全、分發、部署方面,Mirror-Base具有很大優勢。但是反射生成的程式碼冗長,會使Flutter編譯過後的包很大。Flutter通過將Dart編譯成原生程式碼本身就會增加包大小,再加上反射的話包大小更會進一步擴大。所以Flutter團隊在現階段並沒有開放dart:mirrors的使用。
沒有反射也就意味著Json String to Model 也沒有辦法完成,對於這一點,官方也比較無奈。至今Flutter中Dart只支援將JsonString 轉化為Map,然後再由開發者手寫程式碼將key值一一對應到相應的欄位上。
/**
"result": {
"status": "ALREADY",
"scur": "CNY",
"tcur": "EUR",
"ratenm": "人民幣/歐元",
"rate": "0.127839",
"update": "2018-07-13 23:28:01"
}
*/
///
class ExchangeResult {
final String status;
final String scur;
final String tcur;
final String ratem;
final String rate;
final String update;
ExchangeResult(this.status, this.scur, this.tcur, this.ratem, this.rate,
this.update,);
ExchangeResult.fromJson(Map<String, dynamic> json)
: status = json['status'],
scur = json['scur'],
tcur = json['tcur'],
ratem = json['ratem'],
rate = json['rate'],
update = json['update'];
}
Map exchangeMap = json.decode(Utf8Codec().decode(response.bodyBytes));
var resultModel = new ExchangeResult.fromJson(exchangeMap);
Dart-langhttp請求response解碼問題
Http請求返回的response中Header會包含編碼格式charset=utf-8,官方給出的Demo如下:
var dataURL = "http://api.k780.com?app=finance.rate&scur=CNY&tcur=GBP&appkey=35134&sign=fb020c3129435bb5ff21b7113e9cb1c1&format=json";
var response = await http.get(dataURL);
print(response.body);
看起來是非常簡單的實現了非同步請求服務,但是如果返回的charset後面多加了一個";"的話 (charset=utf-8;),http client就不會自動根據header中的charset解析,會返回錯誤:
[ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
Error on line 1, column 33: Invalid media type: expected
所以,如果要解析返回的json string,必須要指定UTF8字元解析response才可以:
print(Utf8Codec().decode(response.bodyBytes));
Flutter並不能指定Dart lang version
安裝Flutter的同時也會安裝Dart lang SDK,整合在Flutter的SDK中的$FLUTTER_SDK/bin/cache/dart-sdk。假如你發現一個Dart lang bug,那就需要更改DartSDK的程式碼,但是這個修正並不能讓你馬上使用。因為Flutter與Dart lang SDK 的version是一一繫結好的。
五、總結
Flutter雖然在現階段問題比較多,但是相對於RN也有自身的優勢。
在效能方面,Flutter的表現比RN更為優秀。Flutter也可以與原生混編,不過Flutter專案在編譯過後生成的安裝包相對於原生開發的專案來說會有所增大,相信這是Flutter團隊今後要解決的一大難題。
不過隨著google與開源社群的不斷支援,相信Flutter在跨平臺移動應用開發中將成為一種新趨勢。