最近做的專案有一個需求, 就是有不同的主題, 那麼在設定頁面就應該讓生效, 這就是全域性狀態的一個管理了, Flutter 系統提供了 InheritedWidget, 但是這裡我們來使用 scoped_model(基於InheritedWidget進行了封裝)
.
InheritedWidget
我們先來看一下InheritedWidget
是如何實現共享資料的.
以下是官網上的一個小例子
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data; //需要在子樹中共享的資料,儲存點選次數
//定義一個便捷方法,方便子樹中的widget獲取共享資料
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
//該回撥決定當data發生變化時,是否通知子樹中依賴data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,則子樹中依賴(build函式中有呼叫)本widget
//的子widget的`state.didChangeDependencies`會被呼叫
return old.data != data;
}
}
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => new __TestWidgetState();
}
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
//使用InheritedWidget中的共享資料
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//父或祖先widget中的InheritedWidget改變(updateShouldNotify返回true)時會被呼叫。
//如果build中沒有依賴InheritedWidget,則此回撥不會被呼叫。
print("Dependencies change");
}
}
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget( //使用ShareDataWidget
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),//子widget中依賴ShareDataWidget
),
RaisedButton(
child: Text("Increment"),
//每點選一次,將count自增,然後重新build,ShareDataWidget的data將被更新
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
複製程式碼
這裡需要注意:
context.inheritFromWidgetOfExactType(ShareDataWidget)
來獲取到指定InheritedWidget
中的資料, 實際通知的時候是父Widget往下傳遞還是子Widget往上遍歷呢?
接下lai 探索原因
InheritedWidget
的原始碼很簡單, 繼承了ProxyWidget
, 也沒有實現太多邏輯, 對createElement
進行了實現, 還定義了updateShouldNotify
, 該方法的意思就是更新的時候是否應該通知在 build
階段通過inheritFromWidgetOfExactType
查詢該 Widget
的子 Widget
.
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child);
@override
InheritedElement createElement() => InheritedElement(this);
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
複製程式碼
順藤摸瓜, 我們去就去看InheritedElement(this)
做了什麼, 進去之後我們可以發現_updateInheritance
方法.
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
複製程式碼
同時我們對比一下普通的Element
的該方法實現, 只是簡單的將父Element
的_inheritedWidgets
屬性儲存到自身(這樣就保證了父級的向子集傳遞特性).
void _updateInheritance() {
assert(_active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼
但是這個_inheritedWidgets
屬性又是在哪裡出現呢? 它定義在 Element
中, 每一個例項都有這個屬性. 它的作用是儲存上級節點Widget
和Element
之間的對映.
abstract class Element extends DiagnosticableTree implements BuildContext {
/// Creates an element that uses the given widget as its configuration.
///
/// Typically called by an override of [Widget.createElement].
Element(Widget widget)
: assert(widget != null),
_widget = widget;
Element _parent;
...
Map<Type, InheritedElement> _inheritedWidgets;
複製程式碼
現在我們回到InheritedElement
的實現.
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
// 新增
_inheritedWidgets[widget.runtimeType] = this;
}
複製程式碼
InheritedElement
會將自身的資訊新增到_inheritedWidgets
屬性中, 然後子孫都可以通過他們自身的該屬性訪問當前的InheritedElement
了.
現在我們知道如何訪問_inheritedWidgets屬性以及包含的內容了, 那麼通知機制是如何實現呢?
一開始例子中就是使用inheritFromWidgetOfExactType(Type)
方法去獲取到指定的InherientWidget
的, 那麼這個方法是怎麼實現呢?
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
複製程式碼
首先會獲取Element(Context)
的_inheritedWidgets
指定型別的Element
, 如果獲取到了, 則會新增到自身的依賴列表中, 祖先節點也有記錄這個依賴, 這樣在更新時候就可以直接通過_dependencies
屬性來進行通知了.
每一次InherientElement
更新的時候, 都會呼叫notifyClients
方法來通知子節點, 呼叫子節點的didChangeDependencies
方法
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents) {
assert(() {
// check that it really is our descendant
Element ancestor = dependent._parent;
while (ancestor != this && ancestor != null)
ancestor = ancestor._parent;
return ancestor == this;
}());
// check that it really depends on us
assert(dependent._dependencies.contains(this));
dependent.didChangeDependencies();
}
}
複製程式碼
以上就是InherientWidget
的原理, 接下來就是看看scoped_model
做了怎樣的封裝.
scoped_model
該庫主要的類有:
abstract class Model extends Listenable
abstract class Model extends Listenable {
final Set<VoidCallback> _listeners = Set<VoidCallback>();
int _version = 0;
int _microtaskVersion = 0;
void addListener(VoidCallback listener) {
debugPrint("新增監聽器");
listener();
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
print("監聽器被去除");
_listeners.remove(listener);
}
int get listenerCount => _listeners.length;
@protected
void notifyListeners() {
if (_microtaskVersion == _version) {
_microtaskVersion++;
scheduleMicrotask(() {
_version++;
_microtaskVersion = _version;
_listeners.toList().forEach((VoidCallback listener) => listener());
});
}
}
}
複製程式碼
Model
繼承了Listenable
, 值在改變的時候呼叫notifyListeners
既可以通知到所有的監聽器.
class ScopedModel<T extends Model> extends StatelessWidget
class ScopedModel<T extends Model> extends StatelessWidget {
final T model;
final Widget child;
ScopedModel({@required this.model, @required this.child})
: assert(model != null),
assert(child != null);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: model,
builder: (context, _) => _InheritedModel<T>(model: model, child: child),
);
}
// 找到 ScopeModel, 並且讓子節點和祖先節點建立依賴
static T of<T extends Model>(
BuildContext context, {
bool rebuildOnChange = false,
}) {
final Type type = _type<_InheritedModel<T>>();
Widget widget = rebuildOnChange
? context.inheritFromWidgetOfExactType(type)
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
if (widget == null) {
throw ScopedModelError();
} else {
return (widget as _InheritedModel<T>).model;
}
}
static Type _type<T>() => T;
}
複製程式碼
通知方法
作者在構造ScopedModel
的時候, 使用了AnimationBuilder
, 這裡會註冊一個監聽器到 Model
中, 然後每一次值的改變都會呼叫AnimationBuilder.builder
方法, 然後就會觸發InherientWidget
的改變, 根據updateShouldNotify
來決定是否通知子孫控制元件更新, 但是在這裡我們並沒有看到InherientWidget
的影子, 讓我們接著往下看.
class _InheritedModel<T extends Model> extends InheritedWidget
class _InheritedModel<T extends Model> extends InheritedWidget {
final T model;
final int version;
_InheritedModel({Key key, Widget child, T model})
: this.model = model,
this.version = model._version,
super(key: key, child: child);
@override
bool updateShouldNotify(_InheritedModel<T> oldWidget) =>
(oldWidget.version != version);
}
複製程式碼
看到這個類, 我們就可以發現作者的實現方式了, _InheritedModel
繼承自InheritedWidget
, 上方ScopedModel.build()
方法, 裡面就返回了該inherientWidget
物件, 但是與上方的例子還缺少子類對父類進行依賴的一步.
class ScopedModelDescendant<T extends Model> extends StatelessWidget
typedef Widget ScopedModelDescendantBuilder<T extends Model>(
BuildContext context,
Widget child,
T model,
);
class ScopedModelDescendant<T extends Model> extends StatelessWidget {
final ScopedModelDescendantBuilder<T> builder;
final Widget child;
final bool rebuildOnChange;
ScopedModelDescendant({
@required this.builder,
this.child,
this.rebuildOnChange = true,
});
@override
Widget build(BuildContext context) {
return builder(
context,
child,
ScopedModel.of<T>(context, rebuildOnChange: rebuildOnChange),
);
}
}
複製程式碼
ScopedModelDescendant
就是ScopedModel
的子孫, 那麼可以看到它的 Build
方法, 裡面有呼叫ScopedModel.of<T>(context, rebuildOnChange: rebuildOnChange)
, 我們把這個方法在拿出來看一下:
static T of<T extends Model>(
BuildContext context, {
bool rebuildOnChange = false,
}) {
final Type type = _type<_InheritedModel<T>>();
Widget widget = rebuildOnChange
? context.inheritFromWidgetOfExactType(type)
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
if (widget == null) {
throw ScopedModelError();
} else {
return (widget as _InheritedModel<T>).model;
}
}
static Type _type<T>() => T;
複製程式碼
到這裡, 就可以發現它和inherient
做法相同了, 首先獲取到InheritedWidget
的型別, _type<_InheritedModel<T>>()
得到_InheritedModel<T>
, 然後判斷是否需要在改變的時候重繪, 預設是True
, 如果需要重繪就會呼叫inheritFromWidgetOfExactType
去建立依賴, 如果為false
, 則會呼叫ancestorInheritedElementForWidgetOfExactType
, 這個方法不會建立依賴, 所以在改變的時候不會收到通知並重繪, 官方的註釋有這麼一句:
This method does not establish a relationship with the target in the way that [inheritFromWidgetOfExactType] does.
最後貼一下作者提供的小例子
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() {
CounterModel model = CounterModel();
runApp(MyApp(
model: model,
));
}
class MyApp extends StatelessWidget {
final CounterModel model;
const MyApp({Key key, @required this.model}) : super(key: key);
@override
Widget build(BuildContext context) {
// At the top level of our app, we'll, create a ScopedModel Widget. This
// will provide the CounterModel to all children in the app that request it
// using a ScopedModelDescendant.
return ScopedModel<CounterModel>(
model: model,
child: MaterialApp(
title: 'Scoped Model Demo',
home: CounterHome('Scoped Model Demo'),
),
);
}
}
// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
// First, increment the counter
_counter++;
// Then notify all the listeners.
notifyListeners();
}
}
class CounterHome extends StatelessWidget {
final String title;
CounterHome(this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
// Create a ScopedModelDescendant. This widget will get the
// CounterModel from the nearest parent ScopedModel<CounterModel>.
// It will hand that CounterModel to our builder method, and
// rebuild any time the CounterModel changes (i.e. after we
// `notifyListeners` in the Model).
ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
model.counter.toString(),
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
// Use the ScopedModelDescendant again in order to use the increment
// method from the CounterModel
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return FloatingActionButton(
onPressed: model.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
}
}
複製程式碼
這裡也能驗證我們上方所分析的, 首先需要構建 ScopedModel
, 然後共享狀態的子孫節點通過ScopedModelDescendant
來新增.