Flutter漫说:组件生命周期、State状态管理及局部重绘的实现(Inherit)
?
目錄
生命周期
State改變時組件如何刷新
InheritedWidget
InheritedModel
InheritedNotifier
Notifier
生命周期
flutter的生命周期其實有兩種:StatefulWidget和StatelessWidget。
這兩個是flutter的兩個基本組件,名稱已經(jīng)很好表明了這兩個組件的功能:有狀態(tài)和無狀態(tài)。
(1)StatelessWidget
StatelessWidget是無狀態(tài)組件,它的生命周期非常簡單,只有一個build,如下:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {return ...;} }對于StatelessWidget來說只渲染一次,之后它就不再有任何改變。
由于無狀態(tài)組件在執(zhí)行過程中只有一個 build 階段,在執(zhí)行期間只會執(zhí)行一個 build 函數(shù),沒有其他生命周期函數(shù),因此在執(zhí)行速度和效率方面比有狀態(tài)組件更好。所以在設計組件時,要考慮業(yè)務情況,盡量使用無狀態(tài)組件。
(2)StatefulWidget
StatelessWidget是有狀態(tài)組件,我們討論的生命周期也基本指它的周期,如圖:
包含以下幾個階段:
-
createState?
該函數(shù)為 StatefulWidget 中創(chuàng)建 State 的方法,當 StatefulWidget 被調(diào)用時會立即執(zhí)行 createState 。
-
initState?
該函數(shù)為 State 初始化調(diào)用,因此可以在此期間執(zhí)行 State 各變量的初始賦值,同時也可以在此期間與服務端交互,獲取服務端數(shù)據(jù)后調(diào)用 setState 來設置 State。
-
didChangeDependencies
該函數(shù)是在該組件依賴的 State 發(fā)生變化時,這里說的 State 為全局 State ,例如語言或者主題等,類似于前端 Redux 存儲的 State 。
-
build?
主要是返回需要渲染的 Widget ,由于 build 會被調(diào)用多次,因此在該函數(shù)中只能做返回 Widget 相關(guān)邏輯,避免因為執(zhí)行多次導致狀態(tài)異常,注意這里的性能問題。 -
reassemble
主要是提供開發(fā)階段使用,在 debug 模式下,每次熱重載都會調(diào)用該函數(shù),因此在 debug 階段可以在此期間增加一些 debug 代碼,來檢查代碼問題。
-
didUpdateWidget
該函數(shù)主要是在組件重新構(gòu)建,比如說熱重載,父組件發(fā)生 build 的情況下,子組件該方法才會被調(diào)用,其次該方法調(diào)用之后一定會再調(diào)用本組件中的 build 方法。
-
deactivate
在組件被移除節(jié)點后會被調(diào)用,如果該組件被移除節(jié)點,然后未被插入到其他節(jié)點時,則會繼續(xù)調(diào)用 dispose 永久移除。
-
dispose
永久移除組件,并釋放組件資源。
在StatelessWidget中,只要我們調(diào)用setState,就會執(zhí)行重繪,也就是說重新執(zhí)行build函數(shù),這樣就可以改變ui。
State改變時組件如何刷新
先來看看下方的代碼:
class MyHomePage extends StatefulWidget {@override_MyHomePageState createState() => _MyHomePageState(); }class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(_counter),WidgetB(),WidgetC(_incrementCounter)],),),);} }class WidgetA extends StatelessWidget {final int counter;WidgetA(this.counter);@overrideWidget build(BuildContext context) {return Center(child: Text(counter.toString()),);} }class WidgetB extends StatelessWidget {@overrideWidget build(BuildContext context) {return Text('I am a widget that will not be rebuilt.');} }class WidgetC extends StatelessWidget {final void Function() incrementCounter;WidgetC(this.incrementCounter);@overrideWidget build(BuildContext context) {return RaisedButton(onPressed: () {incrementCounter();},child: Icon(Icons.add),);} }我們有三個Widget,一個負責顯示count,一個按鈕改變count,一個則是靜態(tài)顯示文字,通過這三個Widget來對比比較頁面的刷新邏輯。
上面代碼中,三個Widget是在_MyHomePageState的build中創(chuàng)建的,執(zhí)行后點擊按鈕可以發(fā)現(xiàn)三個Widget都刷新了。
在Flutter Performance面板上選中Track Widget Rebuilds即可看到
雖然三個Widget都是無狀態(tài)的StatelessWidget,但是因為_MyHomePageState的State改變時會重新執(zhí)行build函數(shù),所以三個Widget會重新創(chuàng)建,這也是為什么WidgetA雖然是無狀態(tài)的StatelessWidget卻依然可以動態(tài)改變的原因。
所以:無狀態(tài)的StatelessWidget并不是不能動態(tài)改變,只是在其內(nèi)部無法通過State改變,但是其父Widget的State改變時可以改變其構(gòu)造參數(shù)使其改變。實際上確實不能改變,因為是一個新的實例。
下面我們將三個組件提前創(chuàng)建,可以在_MyHomePageState的構(gòu)造函數(shù)中創(chuàng)建,修改后代碼如下:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;List<Widget> children;_MyHomePageState(){children = [WidgetA(_counter),WidgetB(),WidgetC(_incrementCounter)];}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: children,),),);} }再次執(zhí)行,發(fā)現(xiàn)點擊沒有任何效果,Flutter Performance上可以看到?jīng)]有Widget刷新(這里指三個Widget,當然Scaffold還是刷新了)。
這是因為組件都提前創(chuàng)建了,所以執(zhí)行build時沒有重新創(chuàng)建三個Widget,所以WidgetA顯示的內(nèi)容并沒有改變,因為它的counter沒有重新傳入。
所以,不需要動態(tài)改變的組件可以提前創(chuàng)建,build時直接使用即可,而需要動態(tài)改變的組件實時創(chuàng)建。
這樣就可以實現(xiàn)局部刷新了么?我們繼續(xù)改動代碼如下:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;Widget b = WidgetB();Widget c ;_MyHomePageState(){c = WidgetC(_incrementCounter);}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(_counter),b,c],),),);} }我們只將WidgetB和WidgetC重新創(chuàng)建,而WidgetA則在build中創(chuàng)建。執(zhí)行后,點擊按鈕WidgetA的內(nèi)容改變了,查看Flutter Performance可以看到只有WidgetA刷新了,WidgetB和WidgetC沒有刷新。
所以:通過提前創(chuàng)建靜態(tài)組件build時直接使用,而build時直接創(chuàng)建動態(tài)Widget 這種方式可以實現(xiàn)局部刷新。
注意:
只要setState,_MyHomePageState就會刷新,所以WidgetA就會跟著刷新,即使count沒有改變。比如上面代碼中將setState中的_count++代碼注釋掉,再點擊按鈕雖然內(nèi)容沒有改變,但是WidgetA依然刷新。
這種情況可以通過InheritedWidget來進行優(yōu)化。
InheritedWidget
InheritedWidget的作用什么?網(wǎng)上有人說是數(shù)據(jù)共享,有人說是用于局部刷新。我們看官方的描述:
Base?class?for?widgets?that?efficiently?propagate?information?down?the?tree.
可以看到它的作用是Widget樹從上到下有效的傳遞消息,所以很多人理解為數(shù)據(jù)共享,但是注意這個“有效的”,這個才是它的關(guān)鍵,而這個有效的其實就是解決上面提到的問題。
那么它怎么使用?
先創(chuàng)建一個繼承至InheritedWidget的類:
class MyInheriteWidget extends InheritedWidget{final int count;MyInheriteWidget({@required this.count, Widget child}) : super(child: child);static MyInheriteWidget of(BuildContext context){return context.dependOnInheritedWidgetOfExactType<MyInheriteWidget>();}@overridebool updateShouldNotify(MyInheriteWidget oldWidget) {return oldWidget.count != count;} }這里將count傳入。重點注意要實現(xiàn)updateShouldNotify函數(shù),通過名字可以知道這個函數(shù)決定InheritedWidget的Child Widget是否需要刷新,這里我們判斷如果與之前改變了才刷新。這樣就解決了上面提到的問題。
然后還要實現(xiàn)一個static的of方法,用于Child Widget中獲取這個InheritedWidget,這樣就可以訪問它的count屬性了,這就是消息傳遞,即所謂的數(shù)據(jù)共享(因為InheritedWidget的child可以是一個layout,里面有多個widget,這些widget都可以使用這個InheritedWidget中的數(shù)據(jù))。
然后我們改造一下WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteWidget myInheriteWidget = MyInheriteWidget.of(context);return Center(child: Text(myInheriteWidget.count.toString()),);} }這次不用在構(gòu)造函數(shù)中傳遞count了,直接通過of獲取MyInheriteWidget,使用它的count即可。
最后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;Widget a = WidgetA();Widget b = WidgetB();Widget c ;_MyHomePageState(){c = WidgetC(_incrementCounter);}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteWidget(count: _counter,child: a,),b,c],),),);} }注意,這里用MyInheriteWidget包裝一下WidgetA,而且WidgetA必須提前創(chuàng)建,如果在build中創(chuàng)建則每次MyInheriteWidget刷新都會跟著刷新,這樣updateShouldNotify函數(shù)的效果就無法達到。
執(zhí)行,點擊按鈕,可以發(fā)現(xiàn)只有WidgetA刷新了(當然MyInheriteWidget也刷新了)。如果注釋掉setState中的_count++代碼,再執(zhí)行并點擊發(fā)現(xiàn)雖然MyInheriteWidget刷新了,但是WidgetA并不刷新,因為MyInheriteWidget的count并未改變。
下面我們改動一下代碼,將WidgetB和C都放入MyInheriteWidget會怎樣?
@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteWidget(count: _counter,child: Column(children: [a,b,c],),),],),),);} }MyInheriteWidget的child是一個Column,將a、b、c都放在這下面。執(zhí)行會發(fā)現(xiàn)依然是WidgetA刷新,B和C都不刷新。這是因為在B和C中沒有執(zhí)行MyInheriteWidget的of函數(shù),就沒有執(zhí)行dependOnInheritedWidgetOfExactType,這樣其實就沒構(gòu)成依賴,MyInheriteWidget就不會通知它們。
如果我們修改WidgetC,在build函數(shù)中添加一行MyInheriteWidget.of(context);那么雖然沒有任何使用,依然能會跟著刷新,因為建立了依賴關(guān)系就會被通知。
InheritedWidget會解決多余的刷新問題,比如在一個頁面中有多個屬性,同樣有多個Widget來使用這些屬性,但是并不是每個Widget都使用所有屬性。如果用最普通的實現(xiàn)方式,那么每次setState(無論改變哪個屬性)都需要刷新這些Widget。但是如果我們用多個InheritedWidget來為這些Widget分類,使用相同屬性的用同一個InheritedWidget來包裝,并實現(xiàn)updateShouldNotify,這樣當改變其中一個屬性時,只有該屬性相關(guān)的InheritedWidget才會刷新它的child,這樣就提高了性能。
InheritedModel
InheritedModel是繼承至InheritedWidget的,擴充了它的功能,所以它的功能更加強大。具體提現(xiàn)在哪里呢?
通過上面我們知道,InheritedWidget可以通過判斷它的data是否變化來決定是否刷新child,但是實際情況下這個data可以是多個變量或者一個復雜的對象,而child也不是單一widget,而是一系列widget組合。比如展示一本書,數(shù)據(jù)可能有書名、序列號、日期等等,但是每個數(shù)據(jù)可能單獨變化,如果用InheritedWidget,就需要每種數(shù)據(jù)需要一個InheritedWidget類,然后將使用該數(shù)據(jù)的widget包裝,這樣才能包裝改變某個數(shù)據(jù)時其他widget不刷新。
但是這樣的問題就是widget層級更加復雜混亂,InheritedModel就可以解決這個問題。InheritedModel最大的功能就是根據(jù)不同數(shù)據(jù)的變化刷新不同的widget。下面來看看如何實現(xiàn)。
首先創(chuàng)建一個InheritedModel:
class MyInheriteModel extends InheritedModel<String>{final int count1;final int count2;MyInheriteModel({@required this.count1, @required this.count2, Widget child}) : super(child: child);static MyInheriteModel of(BuildContext context, String aspect){return InheritedModel.inheritFrom(context, aspect: aspect);}@overridebool updateShouldNotify(MyInheriteModel oldWidget) {return count1 != oldWidget.count1 || count2 != oldWidget.count2;}@overridebool updateShouldNotifyDependent(MyInheriteModel oldWidget, Set<String> dependencies) {return (count1 != oldWidget.count1 && dependencies.contains("count1")) ||(count2 != oldWidget.count2 && dependencies.contains("count2"));} }這里我們傳入兩個count,除了實現(xiàn)updateShouldNotify方法,還需要實現(xiàn)updateShouldNotifyDependent方法。這個函數(shù)就是關(guān)鍵,可以看到我們判斷某個數(shù)據(jù)是否變化后還判斷了dependencies中是否包含一個關(guān)鍵詞:
count1 != oldWidget.count1 && dependencies.contains("count1")這個關(guān)鍵詞是什么?從哪里來?后面會提到,這里先有個印象。
然后同樣需要實現(xiàn)一個static的of函數(shù)來獲取這個InheritedModel,不同的是這里獲取的代碼變化了:
InheritedModel.inheritFrom(context, aspect: aspect);這里的aspect就是后面用到的關(guān)鍵字,而inheritFrom會將這個關(guān)鍵字放入dependencies,以便updateShouldNotifyDependent來使用。后面會詳細解釋這個aspect完整作用。
然后我們改造WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count1");return Center(child: Text(myInheriteModel.count1.toString()),);} }可以看到,這里定義了aspect。
然后因為有兩個count,所以我們再新增兩個Widget來處理count2:
class WidgetD extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count2");return Center(child: Text(myInheriteModel.count2.toString()),);} }class WidgetE extends StatelessWidget {final void Function() incrementCounter;WidgetE(this.incrementCounter);@overrideWidget build(BuildContext context) {return RaisedButton(onPressed: () {incrementCounter();},child: Icon(Icons.add),);} }這里可以看到WidgetD的aspect與WidgetA是不同的。
最后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;int _counter2 = 0;Widget a = Row(children: [WidgetA(),WidgetD()],);Widget b = WidgetB();Widget c ;Widget e ;_MyHomePageState(){c = WidgetC(_incrementCounter);e = WidgetE(_incrementCounter2);}void _incrementCounter() {setState(() {_counter++;});}void _incrementCounter2() {setState(() {_counter2++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteModel(count1: _counter,count2: _counter2,child: a,),b,c,e],),),);} }WidgetD和E是處理count2的,A和C則是處理count。而MyInheriteModel的child不是單一Widget,而是一個Row,包含WidgetD和A。
執(zhí)行代碼,可以發(fā)現(xiàn)點擊WidgetC的時候,只有WidgetA刷新了(當然MyInheriteModel也刷新);而點擊WidgetD的時候,只有WidgetE刷新了。這樣我們就實現(xiàn)了MyInheriteModel中的局部刷新。
其實原理很簡單,aspect就相當于一個標記,當我們通過InheritedModel.inheritFrom(context, aspect: aspect);獲取MyInheriteModel時,實際上將本W(wǎng)idget依賴到MyInheriteModel,并且將這個Widget標記。這時候如果data改變,遍歷它的所有依賴時,會通過每個依賴的Widget獲取它對應的標記集dependencies,然后觸發(fā)updateShouldNotifyDependent判斷該Widget是否刷新。
所以在InheritedModel(其實是InheritedElement)中存在一個map,記錄了每個依賴的Widget對應的dependencies,所以一個Widget可以有多個標記,因為dependencies是一個Set,這樣就可以響應多個數(shù)據(jù)的變化(比如多個數(shù)據(jù)組成一個String作為文本顯示)。
上面其實可以用兩個InheritedWidget也可以實現(xiàn),但是布局越復雜,就需要越多的InheritedWidget,維護起來也費時費力。
所以可以看到InheritedModel使用更靈活,功能更強大,更適合復雜的數(shù)據(jù)和布局使用,并且通過細分細化每一個刷新區(qū)域,使得每次刷新都只更新最小區(qū)域,極大的提高了性能。
InheritedNotifier
InheritedNotifier同樣繼承至InheritedWidget,它是一個給Listenable的子類的專用工具,它的構(gòu)造函數(shù)中要傳入一個Listenable(這是一個接口,不再是之前的各種數(shù)據(jù)data),比如動畫(如AnimationController),然后其依賴的組件則根據(jù)Listenable進行更新。
首先還是先創(chuàng)建一個InheritedNotifier:
class MyInheriteNotifier extends InheritedNotifier<AnimationController>{MyInheriteNotifier({Key key,AnimationController notifier,Widget child,}) : super(key: key, notifier: notifier, child: child);static double of(BuildContext context){return context.dependOnInheritedWidgetOfExactType<MyInheriteNotifier>().notifier.value;} }這里提供的of函數(shù)則直接返回AnimationController的value即可。
然后創(chuàng)建一個Widget:
class Spinner extends StatelessWidget {@overrideWidget build(BuildContext context) {return Transform.rotate(angle: MyInheriteNotifier.of(context) * 2 * pi,child: Text("who!!"),);} }內(nèi)容會根據(jù)AnimationController進行旋轉(zhuǎn)。
修改WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {return Center(child: Text("WidgetA"),);} }然后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage6> with SingleTickerProviderStateMixin {AnimationController _controller;@overridevoid initState() {super.initState();_controller = AnimationController(vsync: this,duration: Duration(seconds: 10),)..repeat();}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(),MyInheriteNotifier(notifier: _controller,child: Spinner()),],),),);} }運行會看到Text在不停的旋轉(zhuǎn),當然如果有其他Widget可以看到并不跟著刷新。
總之InheritedNotifier是一個更細化的工具,聚焦到一個具體場景中,使用起來也更方便。
Notifier
最后再簡單介紹一下Notifier,考慮一個需求:頁面A是列表頁,而頁面B是詳情頁,兩個頁面都有點贊操作和顯示點贊數(shù)量,需要在一個頁面點贊后兩個頁面的數(shù)據(jù)同時刷新。這種情況下就可以使用flutter提供另外一種方式——Notifier。
Notifier其實就是訂閱模式的實現(xiàn),主要包含ChangeNotifier和ValueNotifier,使用起來也非常簡單。通過addListener和removeListener進行訂閱和取消訂閱(參數(shù)是無參無返回值的function),當數(shù)據(jù)改變時調(diào)用notifyListeners();通知即可。
ValueNotifier是更簡單的ChangeNotifier,只有一個數(shù)據(jù)value,可以直接進行set和get,set時自動執(zhí)行notifyListeners(),所以適合單數(shù)據(jù)的簡單場景。
當時注意Notifier只是共享數(shù)據(jù)并通知變化,并不實現(xiàn)刷新,所以還要配合其他一并實現(xiàn)。比如上面的InheritedNotifier(因為Notifier都繼承Listenable接口,所以兩個可以很簡單的配合使用),或者第三方庫Provider(web開發(fā)的習慣)等等。
源碼
關(guān)注公眾號:BennuCTech,發(fā)送“Inherite1”獲取源碼。
?
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的Flutter漫说:组件生命周期、State状态管理及局部重绘的实现(Inherit)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 12来了,支持更多设备,
- 下一篇: 细说Android apk四代签名:AP