iOSer 的 Flutter 快速入坑之道(二)
前言
本文是繼上一篇的有關 flutter 的第二篇文章,由于合起來篇幅太長,所以考慮分開成多篇文章上傳啦。
Flutter UI
a. Widget
在 iOS 中,大部分控件都是基于 UIView 的實例,所有的控件實例在添加到視圖中后,會被作為節點添加到 UI 渲染樹中。其實在 Flutter 中的 Widget 概念也是類似,都會由一棵渲染樹來維護所有的 Widget 子節點。
但是不同之處在于,第一,Flutter 中所有的 widget 在它的生命周期中都是不可變的。在 iOS 中,假如我們要修改并重新渲染一個控件,可以調用 setNeedDisplay 方法,然后在這個控件的節點開始,重新跑一邊所有子節點,渲染生成新的視圖,但是在這個過程中,控件只是產生的屬性的修改,實例卻還是那個實例。然而在 Flutter 中就不同了,由于 Flutter 中的 Widget 都是不可變的,所以在當某個 Widget 需要修改的時候,Flutter 會創建一個新的 Widget 來做替換。
第二,其實在 Flutter 中,Widget 不同于 View 的另一個區別就是 Widget 并不負責任何渲染相關的事情。那么是誰在負責渲染呢?Flutter 是怎么樣通過 Widget 來渲染出界面的?在這里我們就要了解 Flutter 中的 Widget、Element、RenderObject 之間的聯系啦。
Widget 中包含屬性 Element,Element 中又包含了 RenderObject 屬性。Widget 就是一個對 Element 的配置或者描述,我們開發者也是只需要和 Widget 打交道,并不需要去維護更新渲染樹。
Element 就是負責維護渲染樹的實例,主要作用在 Flutter 的渲染管線 build 階段,可以把 element 看作真正的渲染樹子節點。
RenderObject 是用于渲染的對象,主要作用在 Flutter 的渲染管線 布局以及繪制 階段,也就是它將我們的界面最終繪制在屏幕上。
關于 Flutter 的底層渲染機制,其實我認為作為每個初學 Flutter 的同學都需要了解,只有更深入的了解它的渲染機制,才能更好地去使用它。
b.如何使用 Widget 構建界面?
在上面我們大概了解了 Widget 是什么東西,以及 flutter 的渲染機制,那么實際開發中如何使用 Widget呢?
還是從 hello world 開始吧!
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}
所有的 Widget 都是通過這種嵌套的方式來表示其相互之間的關系以及配置,其實很容易懂,不懂的話多看幾種 Widget 的例子就知道啦。
Flutter 中文網
c.StatelessWidget 和 StatefulWidget
StatelessWidget 是不存在中間狀態的控件,也就是說當它被 new 出來之后,就不會被改變了,也無法被改變。如果需要更新展示的內容,就只能銷毀掉重新 new 一個。
StatefulWidget 是可以保存中間狀態的控件,控件的中間狀態保存在 State 類中,通過調用 setState(){} 方法來觸發更新機制,將此節點將其以下的整個子樹重新更新繪制。
d.setState 機制介紹
setState 的作用是告訴框架這個 Object 的 state 已經被修改了,那么這個修改它可能會對 UI 有影響,所以需要重新 build 一遍該 Object。
首先我們來了解一下一個 [State]Object 的生命周期狀態。
enum _StateLifeCycle {
created,
initialized,
ready,
defunct,
}
當這個[State]Object 剛被創建時,它的狀態就是 created 的狀態。緊接著 Object 會調用 didChangeDependencies 方法,這時候 Object 的狀態會轉變為 initialized,但是此時,object 并沒有做好被 build 的準備。然后接下來,object 會轉變為 ready 的狀態,表示現在已經做好 build 的準備了。object 會保持這個狀態,直到 dispose 方法被調用。一旦 object 調用了 dispose 方法,那么它的狀態就會轉變成 defunct 的狀態。
然后我們可以看到 setState 的源碼。首先 flutter 會判斷 setState 是否在 Object 調用 dispose 銷毀后再調用,若 state 為 defunct ,說明該 Object 已經被銷毀了,報錯。
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw error;
}
接下來我們看到 flutter 還判斷了 object 的狀態是否為 created 以及 是否非 mounted。那么 mounted 屬性是什么呢?
mounted 屬性代表的意思是這個 object 是否有被 buildContext 所持有。我們知道 mounted 有安裝、加入的意思。在一個[state]Object 調用 initState 方法之前,flutter 會把該[state]Object 通過與 buildContext 關聯的方式嵌入,直至 Object 調用了 dispose,也就是不會再調用 build 了,這個 Object 才不被繼續持有。
所以這兩個條件說明該 [state]Object 還未被加入到 widget 樹中,有可能是在構造函數的時候調用了 setState,這個是沒有必要的,因為 object 剛被創建的時候是被標記為 dirty 的,需要調用 build。
final dynamic result = fn() as dynamic;
接下來是判斷如果 state 中的更新函數為一個異步函數,返回值是 Future 類型的,那么也報錯。
最后,才是調用了 _element.markNeedsBuild 方法。那么 markNeedsBuild 函數做了什么事情呢。首先,他會判斷 state 是否為 defunct,elementLifeStyle 是否為 active 以及 owner 是否為 nil。之后,將這些節點都標記為 dirty,然后加入到全局的 widget 隊列中,在下一幀的時候進行重建。
那么什么是 owner,owner 的作用是什么呢?
我們可以看到 owner 其實是一個 BuildOwner 的實例,它主要負責跟蹤需要 rebuild 的 widgets。 然后在 scheduleBuildFor 方法中,首先是判斷 element 的 _inDirtyList 是否為 true,如果是,就說明它已經在 dirtyList 中了,于是把它的 dirtyElementsNeedsResorting 屬性設置為yes,返回。
如果一切正常,那么接下來會調用到 onBuildScheduled()方法,這是一個方法回調,是 VoidCallback 類型。
那么接著,onBuildScheduled 的回調是在哪里呢。通過搜索我們發現在 WigetsBinding 的 initInstances 方法中有賦值 onBuildScheduled 代碼塊。
WigetsBinding 又是什么?! 從文檔中我們可以了解到,文檔說 WidgetsBinding 是 wigetsLayer 和 flutter 引擎的粘合劑。。
好的,不管他,先繼續看內部實現,我們繼續往下點追蹤到一個叫
ensureVisualUpdate 的函數,在其中的一個 case 中,我們看到了 scheduleFrame 的方法。然后再往這個方法里面看,可以看到最終它調用了 window.scheduleFrame 方法。 window 的 scheduleFrame 方法會生成一個新的幀,在這個方法調用之后,flutter 引擎會最終調用 handleBeginFrame 強制刷新一個新幀,無論之前那一幀是否已經渲染結束了。
所以到這里差不多就是 setState 調用到屏幕渲染刷新的大致過程了。整理一下整個過程中調用的函數
State.setState() ->
Element.markNeedsBuild() ->
BuildOwner.scheduleBuildFor() ->
WidgetBinding._handleBuildScheduled() ->
SchedulerBinding.ensureVisualUpdate() ->
SchedulerBinding.scheduleFrame() ->
Window.scheduleFrame() ->
WidgetBinding.drawFrame()
e.如何做控件的顯示和隱藏?
我們已經了解了 flutter 中 setState 的機制以及 widget 的一些特性了,那么實際開發中碰到對 widget 進行顯示隱藏控制等需求的時候,應該怎么處理呢?
大概的簡單思路就是通過一個變量 t 去表示 widget 的顯示和隱藏,然后再封裝一個函數根據這個變量來返回需要顯示的 widget 或 Container()。在 flutter 中,如果需要表示返回空的一個 widget 的時候,不要返回 null,而是返回 Container()。最后在需要修改變量 t 的時候加上 setState 代碼塊,表示需要重新繪制。
class _TestState extends State<Test> {
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('Text');
} else {
return Container();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test App"),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: Icon(Icons.update),
),
);
}
}
以上是通過一個按鈕點擊控制控件顯示與否的簡單代碼,蠻看下~
最后
這邊主要講有關 flutter UI 方面的東西,當然 flutter UI 相關的東西也遠遠不止這些,所以如果后續有補充的地方,我也會在本文中修改跟進~