Flutter 狀態(tài)管理scoped model源碼解讀
一、什么是 scoped_model
本文主要從 scoped_model 的簡(jiǎn)單使用說(shuō)起,然后再深入源碼進(jìn)行剖析(InheritedWidget、Listenable、AnimatedBuilder),不會(huì)探討 Flutter 狀態(tài)管理的優(yōu)劣,單純?yōu)榱藢W(xué)習(xí)作者的設(shè)計(jì)思想。
scoped_model 是一個(gè)第三方 Dart 庫(kù),可以讓您輕松的將數(shù)據(jù)模型從父 Widget 傳遞到子 Widget。此外,它還會(huì)在模型更新時(shí)重新構(gòu)建所有使用該模型的子 Widget。
它直接來(lái)自于 Google 正在開(kāi)發(fā)的新系統(tǒng) Fuchsia 核心 Widgets 中對(duì) Model 類(lèi)的簡(jiǎn)單提取,作為獨(dú)立使用的獨(dú)立 Flutter 插件發(fā)布。
二、用法
class CounterModel extends Model { int _counter = 0; int get counter => _counter; static CounterModel of(BuildContext context) => ScopedModel.of<CounterModel>(context, rebuildOnChange: true); void increment() { _counter++; notifyListeners(); } } class ScopedModelDemoPage extends StatelessWidget { const ScopedModelDemoPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('scopedModel'), centerTitle: true, ), body: ScopedModel( model: CounterModel(), child: ScopedModelDescendant<CounterModel>( builder: (context, child, model) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('${model.counter}'), OutlinedButton( onPressed: ScopedModel.of<CounterModel>(context).increment, // onPressed: model.increment, child: Text('add'), ), ], ), ), ), ), ); } }
從上面的代碼可以看出 scoped_model 的使用非常簡(jiǎn)單,只需要以下三步:
- 定義
Model
的實(shí)現(xiàn),如 CounterModel,這里需要注意的是 CounterModel 一定要繼承自Model
(為什么一定要繼承 Model 我們后面細(xì)說(shuō))并且在狀態(tài)改變的時(shí)候執(zhí)行 notifyListeners。 - 使用
ScopedModel
Widget 包裹需要用到Model
的 Widget。 - 使用
ScopedModelDescendant
或者ScopedModel.of<CounterModel>(context)
來(lái)進(jìn)行獲取數(shù)據(jù)。
三、實(shí)現(xiàn)原理
在 scoped_model 中的整個(gè)實(shí)現(xiàn)中,它很巧妙的借助了 AnimatedBuilder、Listenable、InheritedWidget 等 Flutter 的基礎(chǔ)特性。
scoped_model 使用了觀察者模式,將數(shù)據(jù)放在父 Widget,子 Widget 通過(guò)找到父 Widget 的 model 進(jìn)行數(shù)據(jù)渲染,最后改變數(shù)據(jù)的時(shí)候再將數(shù)據(jù)傳回,父 Widget 再通知所有用到了該 model 的子 Widget 去更新?tīng)顟B(tài)。
我們首先從 ScopedModel
入手,通過(guò)源碼我們不難發(fā)現(xiàn),ScopedModel 是一個(gè) StatelessWidget
最后返回一個(gè) AnimatedBuilder
,在 AnimatedBuilder
中在通過(guò) builder 返回 _InheritedModel
。
我們?cè)購(gòu)?Model
入手,可以看出 Model
是一個(gè) 繼承自 Listenable
的抽象類(lèi),主要有一個(gè) _listeners 變量用 Set 來(lái)進(jìn)行存儲(chǔ),復(fù)寫(xiě)了 addListener
、removeListener
、notifyListeners
方法。在這里不知道大家有沒(méi)有想過(guò) Model
為什么要繼承 Listenable
? 在這里先賣(mài)個(gè)關(guān)子,在后面會(huì)詳細(xì)講解。
如果只是單單看 ScopedModel
和 Model
好像也看不出來(lái)什么巧妙之處,但是如果把 ScopedModel
中返回的 AnimatedBuilder
和 Model
所繼承的 Listenable
結(jié)合起來(lái)進(jìn)行思考就會(huì)發(fā)現(xiàn),AnimatedBuilder
繼承自 AnimatedWidget
,在 AnimatedWidget
的生命周期中會(huì)對(duì) Listenable
添加監(jiān)聽(tīng),而 Model
正好就實(shí)現(xiàn)了 Listenable
接口。
Model
實(shí)現(xiàn)了 Listenable
接口,內(nèi)部剛好有一個(gè) Set<VoidCallback> _listeners
用來(lái)保存接收者。當(dāng) Model
賦值給 AnimatedBuilder
中的 animation 時(shí),Listenable
的 addListener 就會(huì)被調(diào)用,然后添加一個(gè) _handleChange
方法,_handleChange
內(nèi)部只有一行代碼 setState((){})
,當(dāng)調(diào)用 notifyListeners
時(shí),會(huì)從創(chuàng)建一個(gè) Microtask,去執(zhí)行一遍 _listeners
中的 _handleChange
,當(dāng) _handleChange
被調(diào)用時(shí)就會(huì)進(jìn)行更新 UI 界面。其實(shí)這里也就解釋了 Model
為什么要繼承 Listenable
。
不知道大家發(fā)現(xiàn)沒(méi)有,講了這么多,還沒(méi)有講到 ScopedModelDescendant
到底是干什么的?那我就不得不先說(shuō)起 InheritedWidget
了。
InheritedWidget
是 Flutter 中非常重要的一個(gè)功能型組件,它提供了一種在 Widget 樹(shù)中從上到下共享數(shù)據(jù)的方式,比如我們?cè)趹?yīng)用的根 Widget 中通過(guò) InheritedWidget
共享了一個(gè)數(shù)據(jù),那么我們便可以在任意子 Widget 中來(lái)獲取該共享的數(shù)據(jù)。它主要有以下兩個(gè)作用:
- 子 Widget 可以通過(guò)
Inherited
Widgets 提供的靜態(tài) of 方法拿到離他最近的父Inherited
widgets 實(shí)例。 - 當(dāng)
Inherited
Widgets 改變 state 之后,會(huì)自動(dòng)觸發(fā) state 消費(fèi)者的 rebuild 行為。
在 scoped_model
中我們可以通過(guò) ScopedModel.of<CountModel>(context)
來(lái)獲取 我們的 model,最主要的就是在 ScopedModel
中返回了 AnimatedBuilder
,而 AnimatedBuilder
中 builder 又返回了 _InheritedModel
, _InheritedModel
又繼承了 InheritedWidget
。
言歸正傳,我們一起回到 ScopedModelDescendant
的主題,不知道大家有沒(méi)有嘗試過(guò),不用 ScopedModelDescendant
來(lái)獲取 model 會(huì)發(fā)生什么樣的情況?通過(guò)窺探源碼我們發(fā)現(xiàn)有 ScopedModelError
這樣一個(gè)異常類(lèi),說(shuō)的已經(jīng)很明確了,必須要提供 ScopedModelDescendant
。what ?其實(shí)它主要做了以下 2 件事情:
- 隱式調(diào)用
ScopedModel.of<T>(context)
來(lái)獲取 model。 - 明確語(yǔ)義化,不然我們每次都需要用
Builder
來(lái)進(jìn)行構(gòu)建,不然將獲取不到 model,還會(huì)拋出異常。
// 不使用 ScopedModelDescendant 使用 Builder 的用法 class ScopedModelDemoPage extends StatelessWidget { const ScopedModelDemoPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('scopedModel'), centerTitle: true, ), body: ScopedModel( model: CounterModel(), child: Builder( builder: (context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('${CounterModel.of(context).counter}'), OutlinedButton( onPressed: CounterModel.of(context).increment, child: Text('add1'), ), ], ); }, ), // child: ScopedModelDescendant<CounterModel>( // builder: (context, child, model) => Center( // child: Column( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // Text('${model.counter}'), // OutlinedButton( // onPressed: ScopedModel.of<CounterModel>(context).increment, // // onPressed: model.increment, // child: Text('add'), // ), // ], // ), // ), // ), ), ); } }
除了上面這種使用 Builder 的方式,當(dāng)然我們還可以使用下面的方法,把它單獨(dú)提取出一個(gè) Widget,代碼如下:
// 單獨(dú)提取 Widget 的方式 class ScopedModelDemoPage extends StatelessWidget { const ScopedModelDemoPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('scopedModel'), centerTitle: true, ), body: ScopedModel<CounterModel>( model: CounterModel(), // 不使用 ScopedModelDescendant 的用法 // child: Builder( // builder: (context) { // return Column( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // Text('${CounterModel.of(context).counter}'), // OutlinedButton( // onPressed: CounterModel.of(context).increment, // child: Text('add1'), // ), // ], // ); // }, // ), child: NewWidget(), ), ); } } class NewWidget extends StatelessWidget { const NewWidget({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('${CounterModel.of(context).counter}'), OutlinedButton( onPressed: CounterModel.of(context).increment, // onPressed: model.increment, child: Text('add'), ), ], ), ); } } class CounterModel extends Model { int _counter = 0; int get counter => _counter; static CounterModel of(BuildContext context) => ScopedModel.of<CounterModel>(context, rebuildOnChange: true); void increment() { _counter++; notifyListeners(); } }
是不是很意外?主要起作用的是下面這一段代碼:
單獨(dú)提取出來(lái) Widget,可以獲取到正確的 context,從而可以獲取到離他最近的父 Inherited
widgets 實(shí)例。
四、結(jié)束
以上就是我對(duì) scoped_model 的使用以及部分源碼的解讀,如果有不足之處,還請(qǐng)指教。 最后,大家也可以思考一下,我們是如何通過(guò) context
就能獲取到共享的 Model 呢?
以上就是Flutter 狀態(tài)管理scoped model源碼解讀的詳細(xì)內(nèi)容,更多關(guān)于Flutter狀態(tài)管理 scoped model的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義View控件實(shí)現(xiàn)多種水波紋漣漪擴(kuò)散效果
這篇文章主要給大家介紹了關(guān)于Android自定義View控件實(shí)現(xiàn)多種水波紋漣漪擴(kuò)散效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03Android ViewPager撤消左右滑動(dòng)切換功能實(shí)現(xiàn)代碼
這篇文章主要介紹了Android ViewPager撤消左右滑動(dòng)切換功能實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-04-04Android重寫(xiě)View并自定義屬性實(shí)例分析
這篇文章主要介紹了Android重寫(xiě)View并自定義屬性的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android基于重寫(xiě)View實(shí)現(xiàn)自定義屬性的相關(guān)布局與具體技巧,需要的朋友可以參考下2016-02-02Android ContentResolver使用說(shuō)明
這篇文章主要介紹了Android ContentResolver使用說(shuō)明,需要的朋友可以參考下2016-01-01Android實(shí)現(xiàn)自定義圓角對(duì)話框Dialog的示例代碼
項(xiàng)目中多處用到對(duì)話框,本篇文章主要介紹了Android實(shí)現(xiàn)圓角對(duì)話框Dialog的示例代碼,有興趣的可以了解一下。2017-03-03基于Flutter制作一個(gè)心碎動(dòng)畫(huà)特效
這篇文章主要為大家介紹了如何利用Flutter制作一個(gè)心碎動(dòng)畫(huà)特效,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Flutter有一定幫助,感興趣的可以了解一下2022-04-04Android開(kāi)發(fā)之ContentProvider的使用詳解
本篇文章介紹了Android開(kāi)發(fā)之ContentProvider的使用詳解。需要的朋友參考下2013-04-04