--- title: 'InheritedWidget 數據共享' disqus: kyleAlien --- InheritedWidget 數據共享 === ## Overview of Content Flutter 有許多的 Widget,但是對於「消息傳遞」來說,每個 Widget 都是獨立的,無法共享數據,而這裡會提及幾個普遍常見的消息傳遞方案,以及介紹 Flutter 的專屬 InheritedWidget(同樣也可以用來共享數據) :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案**](https://devtechascendancy.com/flutter-data-sharing-inheritedwidget/) ::: [TOC] ## 共享數據普遍方案 接下來會使用一些普遍(傳統)的方法來分享不同 Widget 之間的共享數據方式,並分析一下它們的優缺點 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案**](https://devtechascendancy.com/flutter-data-sharing-inheritedwidget//) ::: ### 無法共享數據的問題 * 以下是一段有問題的程式,兩者將數據分開做管理,分為兩個 Widget (重點是每個 Widget 的 State),Widget 關係如下圖所示 ```mermaid graph LR; subgraph MvpInterfacePage CounterView end ``` 1. **`MvpInterfacePage` 的 State**:`MvpInterfacePage` 是個主頁面 它會保存 `_showCount` 數據在 `_MvpInterfaceState` 類中,並在其中呼叫 `CounterView`(這是另一個 Widget),預計透過 `_showCount` 成員來顯示 `CounterView` 的數據 ```dart= // 主畫面 class MvpInterfacePage extends StatefulWidget { MvpInterfacePage({super.key}); @override State<StatefulWidget> createState() => _MvpInterfaceState(); } class _MvpInterfaceState extends State<MvpInterfacePage> { double _showCount = 0; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('MvpInterface'), ), body: Column( children: [ // 呼叫另一個 Widget CounterView(), Text('$_showCount') ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState((){}); }, tooltip: 'Sync', child: Icon(Icons.add), ) ); } } ``` 2. **CounterView 的 State**:滑動計數器 CounterView 的內部使用 `_counter` 成員紀錄 Slider 滑動後的數據 ```dart= // CounterView.dart class CounterView extends StatefulWidget { CounterView({super.key}); @override State<StatefulWidget> createState() => _CounterViewState(); } class _CounterViewState extends State<CounterView> { double _counter = 0; // child widget 自己管理 @override Widget build(BuildContext context) { return ListTile( title: Text('Counter'), subtitle: Slider( // 滑動 Widget max: 10, min: 0, value: _counter, onChanged: (double value) { setState(() => _counter = value); }, ), ); } } ``` :::warning * 現在的問題點是… 拖動內部的 CounterView 中的 Slider 後,無法將 Slider 的數據網外部傳送(無法把數據傳給 `_MvpInterfaceState`) > ![](https://i.imgur.com/AEzWQyX.png) ::: ### 普遍方案:匿名函數實例監聽 * **使用介面(`interface`)實例監聽**: 透過 `_MvpInterfaceState` 把介面實例傳入 `CounterView` 的方式,來監聽子 Widget 的數據是否改動,若子 Widget 數據有所改動,那需要子 Widget 自己去呼叫這個介面實例,以下先以 Java 為例,來展現匿名介面實例傳遞的方式 1. 介面:傳遞數據的方法 ```java= interface IMessageReceiver{ void onMessage(String msg); } ``` 2. 創建介面實例(`instance`)者:在這裡會創建 IMessageReceiver 匿名介面實例,並將這個實例傳給 `MessageItem` 物件 > 這個介面也會作為監聽工具 ```java= class MessageLoop { void start() { MessageItem item = new MessageItem(new IMessageReceiver() { @Override public void onMessage(String msg) { System.out.println("Get message: " + msg); } }); } } ``` 3. 使用 IMessageReceiver 命名介面實例: 在符合呼叫時機時,透過傳入的 IMessageReceiver 實例,主動將訊息傳遞給上層 ```java= class MessageItem { final IMessageReceiver messageReceiver; MessageItem(IMessageReceiver messageReceiver) { this.messageReceiver = messageReceiver; try { start(); } catch (InterruptedException e) { throw new RuntimeException(e); } } void start() throws InterruptedException { Thread.sleep(1000); messageReceiver.onMessage("item start"); } } ``` * 當然,這種匿名介面的手法我們在 Dart 中並不能使用(因為 Dart 沒有匿名介面類),但我們可以使用匿名函數的方式來實現匿名介面實例傳遞的目的… 以下將會使用 `ValueChanged` 匿名函數,該函數的原型如下 ```dart= typedef ValueChanged<T> = void Function(T value); ``` 接著我們把上面的案例改為透過匿名函數實例的方式監聽… 1. `CounterView` 子畫面: 在建構 CounterView 時設定需要一個 `ValueChanged<double>` 的函數實例(也就是呼叫者一定要傳入這個函數實例) ```dart= class CounterView extends StatefulWidget { final ValueChanged<double> _valueChanged; // 要求建構該類時,一定要傳入 ValueChanged<double> 實例 const CounterView(this._valueChanged); @override State<StatefulWidget> createState() => _CounterViewState(); } class _CounterViewState extends State<CounterView> { double _counter = 0; @override Widget build(BuildContext context) { return ListTile( title: Text('Counter: $_counter'), subtitle: Slider( max: 10, min: 0, value: _counter, onChanged: (double value) { setState(() { // 刷新子畫面 _counter = value; // 同時透過傳入瘩函數實例把數據回傳到主畫 widget._valueChanged(value); // 數據響應到主畫面函數 }); }, ), ); } } ``` 2. `MvpInterfacePage` 主畫面: 在創建 `CounterView` Widget 時,同時創建匿名函數,當該函數收到事件時主動刷新畫面 ```dart= class MvpInterfacePage extends StatefulWidget { MvpInterfacePage({super.key}); @override State<StatefulWidget> createState() => _MvpInterfaceState(); } class _MvpInterfaceState extends State<MvpInterface> { double _showCount = 0; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('MvpInterface'), ), body: Column( children: [ // 創建 CounterView Widget CounterView((double value) { // 傳入匿名函數實例 setState((){ // 收到數據後響應,畫面刷新 _showCount = value; }); }), Text('$_showCount') ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState((){}); // 刷新主畫面 }, tooltip: 'Sync', child: Icon(Icons.add), ) ), ); } } ``` :::warning * 其實使用介面實例來做監聽是很常見的手法,但是… 在這個範例中只有一個數值改變所以只有一個監聽接口,若是 View 變得複雜後,需要不斷地傳遞監聽數據,那程式就會變得相當難看 ::: > ![](https://i.imgur.com/rcdh8S5.png) ### 普遍方案:Singleton 類、static 成員 * 另外一個常見的手法是使用 `singleton`、`static` 類:讓全局單例,或是局部單例來共享數據 :::warning * 單例雖然可以間單的處理數據問題,**但不能好很的解決 ++狀態管理++ 問題** 狀態管理:不同狀態下顯示不同的樣式,而這個需要全局共同操控,對於物件導向設計而言會盡量避免或是限制這樣的設計存在 ::: * 以下使用 Singleton 單例的手法示範如何實現數據共享,首先我們先創建單例類(`DataManager`),之後會使用該類來設定、取得數據 ```dart= // DataManager 單例 class DataManager { // 在虛擬機中只會有一個 DataManager 靜態物件 static final DataManager instance = DataManager._(); // 私有化建構函數 DataManager._(); factory DataManager.getInstance() { // 取得 DataManager 靜態物件 return instance; } double counter = 0; } ``` * `CounterView` 子畫面:在數據改變時設定 DataManager 單例的 `counter` 屬性 ```dart= class CounterView extends StatefulWidget { const CounterView(); @override State<StatefulWidget> createState() => _CounterViewState(); } class _CounterViewState extends State<CounterView> { double _counter = DataManager.getInstance().counter; @override Widget build(BuildContext context) { return ListTile( title: Text('Counter: $_counter'), subtitle: Slider( max: 10, min: 0, value: _counter, onChanged: (double value) { _counter = value; // 設定全局單例 DataManager.getInstance().counter = value; setState(() {}); }, ), ); } } ``` * `MvpInterfacePage` 主畫面: 更新畫面時取 DataManager 單例中的 `counter` 屬性,將該屬性賦予到自身的 `_showCount` 成員中,並刷新畫面 ```java= class MvpInterfacePage extends StatefulWidget { MvpInterfacePage({super.key}); @override State<StatefulWidget> createState() => _MvpInterfaceState(); } class _MvpInterfaceState extends State<MvpInterface> { double _showCount = 0; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('MvpInterface'), ), body: Column( children: [ CounterView(), Text('$_showCount') ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState((){ // 取全局單例 _showCount = DataManager.getInstance().counter; }); }, tooltip: 'Sync', child: Icon(Icons.add), ) ), ); } } ``` :::warning * 這樣的方案除了要注意狀態管理之外,同時還要注意 Flutter View 的刷新時機,由於 Flutter View 的刷新時機交由開發者處理,所以在單例數據被修改時,還需要去適時的刷新畫面 ::: > ![](https://i.imgur.com/4ULjmAU.png) ## InheritedWidget 實現數據共享 同上面的例子,但現在使用 Flutter 提供的 InheritedWidget 實現數據共享 ### MediaQuery 的 InheritedWidget 分析 * 這邊以常用的的 `MediaQuery.of(context).size` 來說明,為啥我們透過 `MediaQuery.of(context).size` 就可以取得螢幕尺寸 ? 這說明螢幕尺寸是所有組件共享 1. **首先先看 `debugCheckHasMediaQuery` 函數**: 該函數會檢查 BuildContext#`widget` 是否是 MediaQuery,或檢查是否有 MediaQuery 類,其目的是為了檢查該 BuildContext 中是否有「**InheritedElement 物件**」 > 如果條件都不符合則不能使用 `MediaQuery.of` 方法 ```dart= // media_query.dart static MediaQueryData of(BuildContext context) { // 查看 _of 函數 return _of(context); } static MediaQueryData _of(BuildContext context, [_MediaQueryAspect? aspect]) { // 查看 debugCheckHasMediaQuery 函數 assert(debugCheckHasMediaQuery(context)); return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)!.data; } // ------------------------------------------------------- // debug.dart bool debugCheckHasMediaQuery(BuildContext context) { assert(() { // 查看 getElementForInheritedWidgetOfExactType 方法 if (context.widget is! MediaQuery && context.getElementForInheritedWidgetOfExactType<MediaQuery>() == null) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary('No MediaQuery widget ancestor found.'), ErrorDescription('${context.widget.runtimeType} widgets require a MediaQuery widget ancestor.'), context.describeWidget('The specific widget that could not find a MediaQuery ancestor was'), context.describeOwnershipChain('The ownership chain for the affected widget is'), ErrorHint( 'No MediaQuery ancestor could be found starting from the context ' 'that was passed to MediaQuery.of(). This can happen because the ' 'context used is not a descendant of a View widget, which introduces ' 'a MediaQuery.' ), ]); } return true; }()); return true; } // ------------------------------------------------------- // framework.dart abstract class Element extends DiagnosticableTree implements BuildContext { // 保存 InheritedElement 物件 PersistentHashMap<Type, InheritedElement>? _inheritedElements; @override InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() { // 確保 context 可生命週期狀態為 active assert(_debugCheckStateIsActiveForAncestorLookup()); // 從 _inheritedElements 變量中取得 InheritedElement 物件 final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T]; return ancestor; } } ``` 2. 接著查看 `inheritFrom` 函數: 該函數會取得父 Widget 的 InheritedModel 物件並返回(如果有的話) 由於當前函數並不會帶入 `aspect` 參數,所以我們接著直接看 `dependOnInheritedWidgetOfExactType` 方法:在該方法中,會從 `_inheritedElements` 中取出 `InheritedElement` 物件 ```dart= // media_query.dart static MediaQueryData of(BuildContext context) { // 查看 _of 函數 return _of(context); } static MediaQueryData _of(BuildContext context, [_MediaQueryAspect? aspect]) { ... // 查看 inheritFrom 函數 return InheritedModel.inheritFrom<MediaQuery>(context, aspect: aspect)!.data; } // ------------------------------------------------------- // inherited_model.dart static T? inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object? aspect }) { if (aspect == null) { // 查看 dependOnInheritedWidgetOfExactType 方法 return context.dependOnInheritedWidgetOfExactType<T>(); } ... 省略部分 } ``` 3. **查看 `dependOnInheritedWidgetOfExactType` 方法**: 該方法會取得父 Widget 的 InheritedElement 物件(而為何是父 Widget,需要從 Flutter 三棵樹的加載來了解,這裡我們先略過這個細節… 之後會提及) ```dart= // framework.dart abstract class Element extends DiagnosticableTree implements BuildContext { // 保存 InheritedElement 物件 PersistentHashMap<Type, InheritedElement>? _inheritedElements; @override T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) { // 確保 Context 生命週期為 active 的狀態 assert(_debugCheckStateIsActiveForAncestorLookup()); // InheritedElement 會賦予 _inheritedWidgets 值 final InheritedElement? ancestor = _inheritedElements == null ? null : _inheritedElements![T]; if (ancestor != null) { // 查看 dependOnInheritedElement 方法 return dependOnInheritedElement(ancestor, aspect: aspect) as T; } _hadUnsatisfiedDependencies = true; return null; } } ``` :::success * 一般的 Element 物件沒有 `_inheritedElements` 屬性嗎? 是的,一般的 Element 物件的 `_inheritedElements` 屬性會為 null,只有 InheritedElement 才會賦予這個屬性數據 > ![image](https://hackmd.io/_uploads/BJg6Bie2C.png) * **`dependOnInheritedWidgetOfExactType` 函數用意** 這可以看出這個函數就是,透過暫存 Map 找到 Element 中的 InheritedElement 類,並將其返回,而這返回的 Widget 中就存有需要的數據 ::: 接著最後我們會看到 `dependOnInheritedElement` 方法,該方法會把 InheritedElement 物件保存到 `_dependencies` 成員中 ```dart= // framework.dart abstract class Element extends DiagnosticableTree implements BuildContext { Set<InheritedElement>? _dependencies; @override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) { assert(ancestor != null); _dependencies ??= HashSet<InheritedElement>(); _dependencies!.add(ancestor); ancestor.updateDependencies(this, aspect); // 返回 Widget return ancestor.widget; } } ``` ### InheritedWidget 賦予的時機:mount * **在 ==Element#mount 加載== view 時會更新 `_inheritedWidgets` Map**,這時就可以取得當前 BuildContext 中的 InheritedWidget 了 (而這裡面就紀錄在上層提供的數據) ```dart= // framework.dart abstract class Element extends DiagnosticableTree implements BuildContext { Element(Widget widget) : assert(widget != null), _widget = widget; // mount 函數中會賦予 _parent 值 Element? _parent; // 這個 Map 會不斷地更新 ! Map<Type, InheritedElement>? _inheritedWidgets; void mount(Element? parent, Object? newSlot) { ... 省略部分 _updateInheritance(); } void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); // 更新當前 BuildContext 的 InheritedWidget 紀錄 Map ! _inheritedWidgets = _parent?._inheritedWidgets; } } ``` :::success * 這裡我們可以解答 `_inheritedWidgets` 是由父 Widget 的 `_inheritedWidgets` 成員賦予的,所以當前 Widget 的 `_inheritedWidgets` 是保存父 Widget 的資料 ::: ### InheritedWidget 使用 * 這裡我們簡單的做個 Data Class,並試圖把這個類的物件網內層的子 Widget 傳遞,範例實作如下… * 創建 Data class ```dart= class CounterData { double _value = 0; double get value => _value; void updateValue(double value) => _value = value; } ``` * 創建一個 Widget,並讓該 Widget 繼承 `InheritedWidget`: 把儲存的資料放進 這個 Widget 而之後在這個底下的 Widget 的 BuildContext 都可以取得資料 ```dart= class CounterStore extends InheritedWidget { final CounterData counterData; CounterStore({required this.counterData, required Widget widget}) : super(child: widget); // 返回 true 代表需要更新,false 則不更新 @override bool updateShouldNotify(covariant CounterStore oldWidget) { return counterData.value != oldWidget.counterData.value; } // 創建一個語法糖函數 of static CounterStore? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<CounterStore>(); } ``` * 使用 InheritedWidget 功能來取得 CounterData 數據,套用關係圖如下 ```mermaid graph LR; subgraph MvpInterfacePage subgraph CounterStore CounterView end end ``` 1. **`MvpInterfacePage` 主頁面**: 主頁面主要就是在創建子 Widget `CounterView` 之前,先創建 `CounterStore` 將其包裹起來,並創建需要的 CounterData 共用資料 ```dart= // 主畫面 class MvpInterfacePage extends StatefulWidget { MvpInterfacePage({super.key}); @override State<StatefulWidget> createState() => _MvpInterfaceState(); } class _MvpInterfaceState extends State<FlutterType> { double _showCount = 0; @override Widget build(BuildContext context) { return MaterialApp( // 包裹 CounterView Widget home: CounterStore( counterData: CounterData(), // 創建共用資料 widget: Scaffold( appBar: AppBar( title: Text('MvpInterface'), ), body: Column( children: [ CounterView(), Text('$_showCount') ], ) ) ), ); } } ``` 2. **`CounterView` 子頁面**: 內部的 Widget 就可以使用 BuildContext 取得上層 Widget 創建出的 CounterData 實例並使用 ```dart= // CounterView.dart class CounterView extends StatefulWidget { CounterView({super.key}); @override State<StatefulWidget> createState() => _CounterViewState(); } class _CounterViewState extends State<CounterView> { double _counter = 0; @override Widget build(BuildContext context) { return ListTile( title: Text('Counter: $_counter'), subtitle: Slider( max: 10, min: 0, value: _counter, onChanged: (double value) { _counter = value; // 自己使用 // 更新到 InheritedWidget CounterStore.of(context)!.counterData.updateValue(value); setState(() {}); }, ), ); } } ``` :::info * 為何使用 Builder 包裝起來 ? 用原來的 context 不行 ? 因為外部的 context(Element) 找不到相對的 InheritedWidget ! 在上面的分析可以看得出來 :+1: ::: > ![](https://i.imgur.com/nthgYo8.png) ## Appendix & FAQ :::info ::: ###### tags: `Flutter`