Overview of Content
Flutter 中的每個 Widget 都是獨立的,這意味著它們之間的數據無法直接共享… 這篇文章將探討幾種常見的消息傳遞方案,如匿名函數實例監聽和 Singleton 類的使用,並深入解析 Flutter 的 InheritedWidget,這是一種專為數據共享而設計的強大工具
無論是要實現全局數據管理還是局部數據共享,本指南將幫助你找到最佳解決方案,並深入理解 MediaQuery 等常用的 InheritedWidget 範例。快來學習如何在 Flutter 中更有效地進行數據共享,提升你的開發效率和應用性能
以下使用的 Flutter 版本為
3.22.2
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看
共享數據普遍方案
接下來會使用一些普遍(傳統)的方法來分享不同 Widget 之間的共享數據方式,並分析一下它們的優缺點
無法共享數據的問題
● 以下是一段有問題的程式,兩者將數據分開做管理,分為兩個 Widget (重點是每個 Widget 的 State),Widget 關係如下圖所示
A. MvpInterfacePage
的 State:MvpInterfacePage
是個主頁面
它會保存 _showCount
數據在 _MvpInterfaceState
類中,並在其中呼叫 CounterView
(這是另一個 Widget),預計透過 _showCount
成員來顯示 CounterView
的數據
// 主畫面
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),
)
);
}
}
B. CounterView 的 State:滑動計數器
CounterView 的內部使用 _counter
成員紀錄 Slider 滑動後的數據
// 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);
},
),
);
}
}
● 現在的問題點是… 拖動內部的 CounterView 中的 Slider 後,無法將 Slider 的數據網外部傳送(無法把數據傳給
_MvpInterfaceState
)
普遍方案:匿名函數實例監聽
● 使用介面(interface
)實例監聽:
透過 _MvpInterfaceState
把介面實例傳入 CounterView
的方式,來監聽子 Widget 的數據是否改動,若子 Widget 數據有所改動,那需要子 Widget 自己去呼叫這個介面實例,以下先以 Java 為例,來展現匿名介面實例傳遞的方式
A. 介面:傳遞數據的方法
這個介面也會作為監聽工具
interface IMessageReceiver{
void onMessage(String msg);
}
B. 創建介面實例(instance
)者:在這裡會創建 IMessageReceiver 匿名介面實例,並將這個實例傳給 MessageItem
物件
class MessageLoop {
void start() {
MessageItem item = new MessageItem(new IMessageReceiver() {
@Override
public void onMessage(String msg) {
System.out.println("Get message: " + msg);
}
});
}
}
C. 使用 IMessageReceiver 命名介面實例:
在符合呼叫時機時,透過傳入的 IMessageReceiver 實例,主動將訊息傳遞給上層
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
匿名函數,該函數的原型如下
typedef ValueChanged<T> = void Function(T value);
接著我們把上面的案例改為透過匿名函數實例的方式監聽…
A. CounterView
子畫面:
在建構 CounterView 時設定需要一個 ValueChanged<double>
的函數實例(也就是呼叫者一定要傳入這個函數實例)
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); // 數據響應到主畫面函數
});
},
),
);
}
}
B. MvpInterfacePage
主畫面:
在創建 CounterView
Widget 時,同時創建匿名函數,當該函數收到事件時主動刷新畫面
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),
)
),
);
}
}
● 其實使用介面實例來做監聽是很常見的手法,但是…
在這個範例中只有一個數值改變所以只有一個監聽接口,若是 View 變得複雜後,需要不斷地傳遞監聽數據,那程式就會變得相當難看
普遍方案:Singleton 類、static 成員
● 另外一個常見的手法是使用 singleton
、static
類:讓全局單例,或是局部單例來共享數據
● 單例雖然可以間單的處理數據問題,但不能好很的解決 狀態管理 問題
狀態管理:不同狀態下顯示不同的樣式,而這個需要全局共同操控,對於物件導向設計而言會盡量避免或是限制這樣的設計存在
● 以下使用 Singleton 單例的手法示範如何實現數據共享,首先我們先創建單例類(DataManager
),之後會使用該類來設定、取得數據
// DataManager 單例
class DataManager {
// 在虛擬機中只會有一個 DataManager 靜態物件
static final DataManager instance = DataManager._();
// 私有化建構函數
DataManager._();
factory DataManager.getInstance() {
// 取得 DataManager 靜態物件
return instance;
}
double counter = 0;
}
● CounterView
子畫面:在數據改變時設定 DataManager 單例的 counter
屬性
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
成員中,並刷新畫面
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),
)
),
);
}
}
● 這樣的方案除了要注意狀態管理之外,同時還要注意 Flutter View 的刷新時機,由於 Flutter View 的刷新時機交由開發者處理,所以在單例數據被修改時,還需要去適時的刷新畫面
InheritedWidget 實現數據共享
同上面的例子,但現在使用 Flutter 提供的 InheritedWidget 實現數據共享
MediaQuery 的 InheritedWidget 分析
● 這邊以常用的的 MediaQuery.of(context).size
來說明,為啥我們透過 MediaQuery.of(context).size
就可以取得螢幕尺寸 ? 這說明螢幕尺寸是所有組件共享
A. 首先先看 debugCheckHasMediaQuery
函數:
該函數會檢查 BuildContext#widget
是否是 MediaQuery,或檢查是否有 MediaQuery 類,其目的是為了檢查該 BuildContext 中是否有「InheritedElement 物件」
如果條件都不符合則不能使用
MediaQuery.of
方法
// 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;
}
}
B. 接著查看 inheritFrom
函數:
該函數會取得父 Widget 的 InheritedModel 物件並返回(如果有的話)
由於當前函數並不會帶入 aspect
參數,所以我們接著直接看 dependOnInheritedWidgetOfExactType
方法:在該方法中,會從 _inheritedElements
中取出 InheritedElement
物件
// 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>();
}
... 省略部分
}
C. 查看 dependOnInheritedWidgetOfExactType
方法:
該方法會取得父 Widget 的 InheritedElement 物件(而為何是父 Widget,需要從 Flutter 三棵樹的加載來了解,這裡我們先略過這個細節… 之後會提及)
// 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;
}
}
● 一般的 Element 物件沒有
_inheritedElements
屬性嗎?是的,一般的 Element 物件的
_inheritedElements
屬性會為 null,只有 InheritedElement 才會賦予這個屬性數據
●
dependOnInheritedWidgetOfExactType
函數用意這可以看出這個函數就是,透過暫存 Map 找到 Element 中的 InheritedElement 類,並將其返回,而這返回的 Widget 中就存有需要的數據
接著最後我們會看到 dependOnInheritedElement
方法,該方法會把 InheritedElement 物件保存到 _dependencies
成員中
// 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 了 (而這裡面就紀錄在上層提供的數據)
// 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;
}
}
● 這裡我們可以解答
_inheritedWidgets
是由父 Widget 的_inheritedWidgets
成員賦予的,所以當前 Widget 的_inheritedWidgets
是保存父 Widget 的資料
InheritedWidget 使用
● 這裡我們簡單的做個 Data Class,並試圖把這個類的物件網內層的子 Widget 傳遞,範例實作如下…
● 創建 Data class
class CounterData {
double _value = 0;
double get value => _value;
void updateValue(double value) => _value = value;
}
● 創建一個 Widget,並讓該 Widget 繼承 InheritedWidget
:
把儲存的資料放進 這個 Widget 而之後在這個底下的 Widget 的 BuildContext 都可以取得資料
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 數據,套用關係圖如下
A. MvpInterfacePage
主頁面:
主頁面主要就是在創建子 Widget CounterView
之前,先創建 CounterStore
將其包裹起來,並創建需要的 CounterData 共用資料
// 主畫面
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')
],
)
)
),
);
}
}
B. CounterView
子頁面:
內部的 Widget 就可以使用 BuildContext 取得上層 Widget 創建出的 CounterData 實例並使用
// 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(() {});
},
),
);
}
}
● 為何使用 Builder 包裝起來 ? 用原來的 context 不行 ?
因為外部的 context(Element) 找不到相對的 InheritedWidget ! 在上面的分析可以看得出來 😀
更多的 Flutter/Dart 語言相關文章
了解 Flutter 如何在跨平台開發中佔據重要地位,掌握快速上手的技巧與項目建置流程,開啟你的跨平台開發之旅!
探索跨平台與 Flutter 技術的未來:從認識到 Flutter 專案建置 | 3 種跨平台
Dart 語言基礎
● 探討 Dart 語言:宣告、數據類型、操作符 | 從基礎到應用指南
快速掌握 Dart 語言的核心概念,包括變數宣告、數據類型及操作符,為 Flutter 開發奠定扎實基礎。
● Dart 函數與方法、異常處理、引用庫 | Java 比較
深入了解 Dart 的函數與異常處理特性,並與 Java 的處理方式進行比較,幫助你跨語言切換更加順暢。
● 深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性
學習 Dart 類的設計邏輯及命名慣例,深入探索抽象類與 Mixin 的強大應用場景。
● 深入探索 Dart 的併發與異步處理:從 Isolate 到 Event Loop 的全面指南 | Future、Stream
徹底搞懂 Dart 的併發與異步處理,掌握 Isolate 與 Event Loop 的運行機制,助你提升應用效能!
深入 Flutter 框架
● 深入解析 Flutter Navigator:常見錯誤、解決方法與路由跳轉技巧、動畫
從常見問題到自定義解決方案,學會如何利用 Navigator 實現路由跳轉與流暢動畫效果。
● 深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案
探討數據共享的最佳實踐,了解 InheritedWidget 等三種主要方案,幫助你優化應用結構。
● 深入解析 Flutter 三顆樹:Widget、Element 與 RenderObject 完整指南
拆解 Flutter 的內部結構,全面了解 Widget、Element 和 RenderObject 之間的關係,提升你的 Flutter 開發技能!