---
# System prepended metadata

title: InheritedWidget 數據共享
tags: [Flutter]

---

---
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`
