---
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`)
> 
:::
### 普遍方案:匿名函數實例監聽
* **使用介面(`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 變得複雜後,需要不斷地傳遞監聽數據,那程式就會變得相當難看
:::
> 
### 普遍方案: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 的刷新時機交由開發者處理,所以在單例數據被修改時,還需要去適時的刷新畫面
:::
> 
## 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 才會賦予這個屬性數據
> 
* **`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:
:::
> 
## Appendix & FAQ
:::info
:::
###### tags: `Flutter`