深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案

深入理解 Flutter 中的數據共享:從普遍方案到 InheritedWidget | 3 種方案

Overview of Content

Flutter 中的每個 Widget 都是獨立的,這意味著它們之間的數據無法直接共享… 這篇文章將探討幾種常見的消息傳遞方案,如匿名函數實例監聽和 Singleton 類的使用,並深入解析 Flutter 的 InheritedWidget,這是一種專為數據共享而設計的強大工具

無論是要實現全局數據管理還是局部數據共享,本指南將幫助你找到最佳解決方案,並深入理解 MediaQuery 等常用的 InheritedWidget 範例。快來學習如何在 Flutter 中更有效地進行數據共享,提升你的開發效率和應用性能

以下使用的 Flutter 版本為 3.22.2

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


共享數據普遍方案

接下來會使用一些普遍(傳統)的方法來分享不同 Widget 之間的共享數據方式,並分析一下它們的優缺點

無法共享數據的問題

● 以下是一段有問題的程式,兩者將數據分開做管理,分為兩個 Widget (重點是每個 Widget 的 State),Widget 關係如下圖所示

graph LR; subgraph MvpInterfacePage CounterView end

A. MvpInterfacePage 的 StateMvpInterfacePage 是個主頁面

它會保存 _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 成員

● 另外一個常見的手法是使用 singletonstatic 類:讓全局單例,或是局部單例來共享數據

● 單例雖然可以間單的處理數據問題,但不能好很的解決 狀態管理 問題

狀態管理:不同狀態下顯示不同的樣式,而這個需要全局共同操控,對於物件導向設計而言會盡量避免或是限制這樣的設計存在

● 以下使用 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 才會賦予這個屬性數據

image

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 數據,套用關係圖如下

graph LR; subgraph MvpInterfacePage subgraph CounterStore CounterView end end

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 開發技能!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響