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 正在開發(fā)的新系統(tǒng) Fuchsia 核心 Widgets 中對(duì) Model 類的簡(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。 - 使用
ScopedModelWidget 包裹需要用到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ài)。
我們首先從 ScopedModel 入手,通過(guò)源碼我們不難發(fā)現(xiàn),ScopedModel 是一個(gè) StatelessWidget 最后返回一個(gè) AnimatedBuilder,在 AnimatedBuilder 中在通過(guò) builder 返回 _InheritedModel。

我們?cè)購(gòu)?Model 入手,可以看出 Model 是一個(gè) 繼承自 Listenable 的抽象類,主要有一個(gè) _listeners 變量用 Set 來(lái)進(jìn)行存儲(chǔ),復(fù)寫了 addListener、removeListener、notifyListeners 方法。在這里不知道大家有沒有想過(guò) Model 為什么要繼承 Listenable? 在這里先賣個(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)聽,而 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)沒有,講了這么多,還沒有講到 ScopedModelDescendant 到底是干什么的?那我就不得不先說(shuō)起 InheritedWidget 了。
InheritedWidget 是 Flutter 中非常重要的一個(gè)功能型組件,它提供了一種在 Widget 樹中從上到下共享數(shù)據(jù)的方式,比如我們?cè)趹?yīng)用的根 Widget 中通過(guò) InheritedWidget 共享了一個(gè)數(shù)據(jù),那么我們便可以在任意子 Widget 中來(lái)獲取該共享的數(shù)據(jù)。它主要有以下兩個(gè)作用:
- 子 Widget 可以通過(guò)
InheritedWidgets 提供的靜態(tài) of 方法拿到離他最近的父Inheritedwidgets 實(shí)例。 - 當(dāng)
InheritedWidgets 改變 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 的主題,不知道大家有沒有嘗試過(guò),不用 ScopedModelDescendant 來(lái)獲取 model 會(huì)發(fā)生什么樣的情況?通過(guò)窺探源碼我們發(fā)現(xiàn)有 ScopedModelError 這樣一個(gè)異常類,說(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實(shí)戰(zhàn)教程第四十篇之Chronometer實(shí)現(xiàn)倒計(jì)時(shí)
這篇文章主要介紹了Android實(shí)戰(zhàn)教程第四十篇之Chronometer實(shí)現(xiàn)倒計(jì)時(shí),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android之自定義實(shí)現(xiàn)BaseAdapter(通用適配器三)
這篇文章主要為大家詳細(xì)介紹了Android之自定義實(shí)現(xiàn)BaseAdapter通用適配器第三篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
Android 詳解沉浸式狀態(tài)欄的實(shí)現(xiàn)流程
沉浸式就是要給用戶提供完全沉浸的體驗(yàn),使用戶有一種置身于虛擬世界之中的感覺。沉浸式模式就是整個(gè)屏幕中顯示都是應(yīng)用的內(nèi)容,沒有狀態(tài)欄也沒有導(dǎo)航欄,用戶不會(huì)被一些系統(tǒng)的界面元素所打擾,讓我們來(lái)實(shí)現(xiàn)下網(wǎng)上傳的沸沸揚(yáng)揚(yáng)的安卓沉浸式狀態(tài)欄2021-11-11

