# 使用InheritedWidget傳遞數據
- 2019 年 10 月 6 日
- 筆記
# 使用InheritedWidget傳遞數據
除了StatefulWidget、StatelessWidget之外flutter還提供了另外一個用的Widget組件即InheritedWidget。那麼該組件的作用時什麼呢?
# 我們來看一下數據是如何從父widget傳遞到子widget的
下面我們定義一個嵌套三層的數據傳遞例子:
class DataTransferAWidget extends StatelessWidget { final int data; DataTransferAWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Expanded(child: Text("A:${data.toString()}")), Expanded( //子組件依賴自己的data數據 child: DataTransferBWidget(data), ) ], ), ); } } class DataTransferBWidget extends StatelessWidget { final int data; DataTransferBWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Expanded(child: Text("B:${data.toString()}")), //子組件依賴自己的data數據 Expanded(child: DataTransferCWidget(data)) ])); } } class DataTransferCWidget extends StatelessWidget { final int data; DataTransferCWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Text("C:${data.toString()}")); } } class DataTransferDemoPage extends StatefulWidget { @override State<StatefulWidget> createState() { return _DataTransferDemoPageState(); } } class _DataTransferDemoPageState extends State<DataTransferDemoPage> { //定義一個待向子widget傳遞的數據 int data = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("數據傳遞"), ), body: Container( alignment: Alignment.center, //向子傳遞data child: DataTransferAWidget(data)), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { setState(() { data++; }); }), ); }
上面例子你看到,每個DataTransferWidget
的構造函數都依賴父widget的data,如果還有第4層,第5層…等嵌套的話,data要不停的通過構造函數傳遞,甚是麻煩。
既然每層widget依賴的都是根的data,那麼為什麼不定義一個全局靜態變數來做呢?(好想法,我們試一下)
# 使用static代替構造函數傳遞數據
typedef ChildWidgetBuilder =DataTransferCWidget Function(int); class DataTransferAWidget extends StatelessWidget { //下面程式碼已經沒有意義了 //final int data; //DataTransferAWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Expanded(child: Text("A:${DataModel.data.toString()}")), Expanded( child: DataTransferBWidget(), ) ], ), ); } } class DataTransferBWidget extends StatelessWidget { //下面程式碼已經沒有意義了 //final int data; //DataTransferBWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Expanded(child: Text("B:${DataModel.data.toString()}")), Expanded(child: DataTransferCWidget()) ])); } } class DataTransferCWidget extends StatelessWidget { //下面程式碼已經沒有意義了 //final int data; //DataTransferCWidget(this.data); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Text("C:${DataModel.data.toString()}")); } } //靜態變數 class DataModel{ static int data=0; } class DataTransferDemoPage extends StatefulWidget { @override State<StatefulWidget> createState() { return _DataTransferDemoPageState(); } } class _DataTransferDemoPageState extends State<DataTransferDemoPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("數據傳遞"), ), body: Container( alignment: Alignment.center, child: DataTransferAWidget()), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { setState(() { DataModel.data++; }); }), ); } }
全局static類型的變數是不是更方便一些呢?!
# 該是InheritedWidget出場的時候了
flutter官方api是這樣說的:有效地在樹中傳播資訊的小部件的基類,下面咱們來看一下它的定義:
//我們可以看到該類是一個抽象類 abstract class InheritedWidget { //這是一個常量構造函數,常量的作用就是一旦編譯後就不可能再被修改 const InheritedWidget({ Key key, Widget child }) : super(key: key, child: child); //這個函數不在我們的討論範圍之內 @override InheritedElement createElement() => InheritedElement(this); //這個函數可以接收舊的對象,用來判斷新舊InheritedWidget是否一樣,它又什麼作用呢?往下看... @protected bool updateShouldNotify(covariant InheritedWidget oldWidget); }
它是一個抽象類,這就意味著我們要去實現它:
class TestModel { int model = 0; void add() { model++; } } class MyInheriteWidget extends InheritedWidget { //這裡我們定義一個我們準備向後代傳遞的數據 final DataModel model; //另外InheriteWidget需要包裹一個Widget //由於InheritedWidget僅用來傳輸數據, //所以這個child或者child的後代需要是一個StatelessWidget或StatefulWidget final Widget child; MyInheriteWidget({Key key, @required this.model, @required this.child}) : super(key: key, child: child); //這裡我們要判斷新舊widget是否相同 @override bool updateShouldNotify(MyInheriteWidget oldWidget) { return model != oldWidget.model; } static TestModel of(BuildContext context, [bool onChangeBuild = true]) { var widget = onChangeBuild ? context.inheritFromWidgetOfExactType(MyInheriteWidget) : context .ancestorInheritedElementForWidgetOfExactType(MyInheriteWidget) .widget; return (widget as MyInheriteWidget).model; } }
此時,一個可以用與向後台傳遞數據的InheritedWidget就完成了,下面來看如何使用:
class _InheriteWidgetDemoPage extends State<InheriteWidgetDemoPage> { //在一個有狀態的widget中定義它需要維護的狀態 var testModel = TestModel(); @override Widget build(BuildContext context) { //使用InheritedWidget向後代傳遞數據 return MyInheriteWidget( model: testModel, child: Scaffold( appBar: AppBar(title: Text("title")), //或許你想這樣使用,這是錯誤的,我們應該獲取的是InheritedWidget的數據而不是當前widget的 //body:Text(testModel.model.toString()) //為了更清楚,我們將獲取InheritedWidget的數據放在一個單獨的widget中執行,如下 body: TestAWidget(), floatingActionButton: Builder( builder: (context) => FloatingActionButton( child: Icon(Icons.add), onPressed: () { setState(() { MyInheriteWidget.of(context).add(); }); }, ), )), ); } } class TestAWidget extends StatelessWidget { TestAWidget(); @override Widget build(BuildContext context) { //官方文檔提到我們應該這樣獲取InheritedWidget上傳遞的數據,是不是有點類似全局靜態變數呢?! //不同的是 這是從context獲取的,因為context可以貫穿整個widget依賴樹,像android的context //官方給的demo不是下面這樣 //var myInheritedWidget = context.inheritFromWidgetOfExactType(MyInheriteWidget) as MyInheriteWidget; //return Center(child: Text(myInheritedWidget.model.model.toString())); //應該是這樣,將獲取傳遞對象的程式碼放在MyInheritedWidget中,這僅是為了程式碼可讀性 var model = MyInheriteWidget.of(context).model.toString(); return Center(child: Text(model)); } }
此時InheritedWidget向後代傳遞數據的方式已經完了,從現有的實現來說並沒有比全局靜態變數有什麼優略之處,如果僅是這樣也就沒有它存在的價值了,請往下看…
# updateShouldNotify的作用
要弄清楚updateShouldNotify的作用我們還要翻一翻源碼了,不然只看api文檔還是一頭霧水,上源碼:
//我們知道一個widget由:widget,element,ReanderObject三部分組成 class InheritedElement extends ProxyElement { ... @override void updated(InheritedWidget oldWidget) { //通過字元串搜索你會找到該方法是在此處調用的 if (widget.updateShouldNotify(oldWidget)) super.updated(oldWidget); } @override void notifyClients(InheritedWidget oldWidget) { ... notifyDependent(oldWidget, dependent); } } @protected void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) { dependent.didChangeDependencies(); } ... } class StatefulElement extends ComponentElement { @override void didChangeDependencies() { super.didChangeDependencies(); //調了這麼一圈最後是為了調用didChangeDependencies方法 _state.didChangeDependencies(); } } abstract class ProxyElement extends ComponentElement{ @protected void updated(covariant ProxyWidget oldWidget) { notifyClients(oldWidget); } }
從上面重要的源碼可以看到最後updateShouldNotify決定了didChangeDependencie()會不會被調用。
另外,分析源碼的調用過程,也可以藉助idea的debugger模式,在Frames裡面我們可以看到程式碼是如何一步步被調用的。
# didChangeDependencie又是什麼
didChangeDependencie是State中定義的一個回調函數,而State正是暴漏StatefulWidget生命周期的地方,我們可以同步實現State的不同方法,來對widget的生命周期變化時做出一些響應。
上面例子我們定義了一個無狀態的TestAWidget來演示如果獲取InheritedWidget要向子傳遞的數據,下面我們通過一個有狀態的控制項來展示在獲取數據的同時響應didChangeDependencie的調用。
lass TestBWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return _TestBWidgetState(); } } class _TestBWidgetState extends State<TestBWidget> { //這裡就是要被調用的地方 @override void didChangeDependencies() { super.didChangeDependencies(); print('TestBWidget'); } @override Widget build(BuildContext context) { var model = MyInheriteWidget.of(context).model.toString(); return Center(child: Text(model)); } }
現在我們已經知道了updateShouldNotify返回true時didChangeDependencies()就會被調用。
InheritedWidget是為了向後代傳遞數據,如果InheritedWidget發生了嵌套呢?可能你只想響應某一個先輩的didChangeDependencies()調用,這也是可以,通過ancestorInheritedElementForWidgetOfExactType來獲取先輩的數據,不會將自己的didChangeDependencies註冊進該先輩的調用集合。
# 有選擇性的註冊didChangeDependencies調用
前面的程式碼中有一段是這樣的:
static TestModel of(BuildContext context, [bool onChangeBuild = true]) { //通過一個變數來控制調用哪個方法獲取先輩的數據 var widget = onChangeBuild ? context.inheritFromWidgetOfExactType(MyInheriteWidget) : context .ancestorInheritedElementForWidgetOfExactType(MyInheriteWidget) .widget; return (widget as MyInheriteWidget).model; }
要搞清楚inheritFromWidgetOfExactType與ancestorInheritedElementForWidgetOfExactType的區別,我認為看源程式碼是最直接的:
... @override InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) { final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType]; //重點再這裡 if (ancestor != null) { return inheritFromElement(ancestor, aspect: aspect); } _hadUnsatisfiedDependencies = true; return null; } @override InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) { final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType]; return ancestor; } @override InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) { _dependencies ??= HashSet<InheritedElement>(); //將widget添加到依賴集合後就會收到didChangeDependencies調用 _dependencies.add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; } ...
下面配上一張流程圖來加深一下印象:

# 總結
- InheritedWidget用與向後代傳遞/共享數據
- 通過updateShouldNotify方法可以控制是否要調用後代的didChangeDependencies方法
- didChangeDependencies被調用的前提是這個後代是一個StatefulWidget,且是通過inheritFromWidgetOfExactType方法來獲取先輩的數據。