Flutter 資料共享 InheritedWidget

maoqitian 發表於 2019-11-26

image

Flutter 中Widget 多種多樣,有UI的,當然也有功能型的元件InheritedWidget 元件就是Flutter 中的一個功能元件,它可以實現Flutter 元件之間的資料共享,他的資料傳遞方向在Widget樹傳遞是從上到下的。

InheritedWidget 實現元件資料共享

  • 既然要使用InheritedWidget,首先寫一個Widget繼承InheritedWidget

實現ShareDataWidget

/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/15 0015
/// email: [email protected]
/// des:  InheritedWidget是Flutter中非常重要的一個功能型元件,它提供了一種資料在widget樹中從上到下傳遞、共享的方式
import 'package:flutter/material.dart';


class ShareDataWidget extends InheritedWidget  {


  final int data; //需要在子樹中共享的資料,儲存點選次數

  ShareDataWidget( {@required this.data,Widget child})
      :super(child:child);


  // 子樹中的widget通過該方法獲取ShareDataWidget,從而獲取共享資料
  static ShareDataWidget of(BuildContext context){
    return context.inheritFromWidgetOfExactType(ShareDataWidget);
  }


  //繼承 InheritedWidget 實現的方法 返回值 決定當data發生變化時,是否通知子樹中依賴data的Widget 更新資料
  @override
  bool updateShouldNotify(ShareDataWidget oldWidget) {
    //如果返回true,則子樹中依賴(build函式中有呼叫)本widget的子widget的`state.didChangeDependencies`會被呼叫
    return oldWidget.data != data;
  }
}
複製程式碼
  • 由以上實現我們可以看到updateShouldNotify 返回值 決定當data發生變化時,是否通知子樹中依賴data的Widget 更新資料,並且實現了of 方法方便子widget獲取共享資料。

測試ShareDataWidget資料共享

  • 前面我們已經實現了InheritedWidget,現在我們來看看如何使用隨便寫一個widget,讓其顯示ShareDataWidget的data 資料
 /// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/15 0015
/// email: [email protected]
/// des:  測試 ShareDataWidget
import 'package:flutter/material.dart';
import 'package:flutter_hellow_world/InheritedWidget/ShareDataWidget.dart';

class TestShareDataWidget extends StatefulWidget {
  @override
  _TestShareDataWidgetState createState() => _TestShareDataWidgetState();
}

class _TestShareDataWidgetState extends State<TestShareDataWidget> {


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //上層 widget中的InheritedWidget改變(updateShouldNotify返回true)時會被呼叫。
    //如果build中沒有依賴InheritedWidget,則此回撥不會被呼叫。
    print("didChangeDependencies");
  }

  @override
  Widget build(BuildContext context) {
    //顯示 ShareDataWidget 資料變化,如果build中沒有依賴InheritedWidget,則此回撥不會被呼叫。
    return Text(ShareDataWidget.of(context).data.toString());

  }
}
複製程式碼
  • 接著新建widget 來使用ShareDataWidget,建立一個按鈕,每點選一次,就將ShareDataWidget的值自增
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/15 0015
/// email: [email protected]
/// des:  建立一個按鈕,每點選一次,就將ShareDataWidget的值自增
import 'package:flutter/material.dart';
import 'package:flutter_hellow_world/InheritedWidget/ShareDataWidget.dart';
import 'package:flutter_hellow_world/InheritedWidget/TestShareDataWidget.dart';

class InheritedWidgetTest extends StatefulWidget {
  @override
  _InheritedWidgetTestState createState() => _InheritedWidgetTestState();
}

class _InheritedWidgetTestState extends State<InheritedWidgetTest> {

  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ShareDataWidget(
        data: count, //共享資料 data
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: TestShareDataWidget()//子widget中依賴ShareDataWidget
            ),
            RaisedButton(
              child: Text("計數增加"),
              onPressed: (){ 
                setState(() {
                  ++ count;
                });
              },
            )
          ],
        ),
      ),
    );
  }
}
複製程式碼
  • 程式碼很簡單,建立一個按鈕,每點選一次,就將ShareDataWidget的data值加一,而前面建立的TestShareDataWidget中依賴了ShareDataWidget的data值,如果資料共享則它的值就會跟隨變化。

  • 執行效果

Flutter 資料共享 InheritedWidget

didChangeDependencies呼叫

  • 執行上面的例子我們看到日誌中會列印出如下日誌,這就說明改變ShareDataWidget的data值時TestShareDataWidget的didChangeDependencies方法被呼叫了,該方法我們在寫StatefulWidget時很少用到,我們可以在該方法中做一些耗時操作,比如資料持久化、網路請求等。
I/flutter ( 7082): didChangeDependencies
複製程式碼
  • 如果不想呼叫讓didChangeDependencies被呼叫,也是有辦法的,如下改變ShareDataWidget的of方法
 // 子樹中的widget獲取共享資料 方法
  static ShareDataWidget of(BuildContext context){
    //return context.inheritFromWidgetOfExactType(ShareDataWidget);
    //使用 ancestorInheritedElementForWidgetOfExactType 方法當資料變化則不會呼叫 子widget 的didChangeDependencies 方法 
    return context.ancestorInheritedElementForWidgetOfExactType(ShareDataWidget).widget;
  }
複製程式碼
  • 這裡可以看到改變使用context.ancestorInheritedElementForWidgetOfExactType方法,而為什麼使用這個方法didChangeDependencies就不會被呼叫呢?看原始碼就是最好的解釋,我們直接翻到framework.dart中這兩個方法的原始碼
/**
 * framework.dart  inheritFromWidgetOfExactType和ancestorInheritedElementForWidgetOfExactType方法原始碼
 */
 @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }


  @override
  InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    return ancestor;
  }
複製程式碼
  • 顯然,一對比我們就可以看到inheritFromWidgetOfExactType多呼叫了inheritFromElement方法,繼續看該方法原始碼
/**
 * framework.dart  inheritFromElement方法原始碼
 */
 
@override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
複製程式碼
  • 到這裡,一切都變得很清晰, inheritFromWidgetOfExactType方法中呼叫了inheritFromElement方法,而在該方法中InheritedWidget將其子widget新增了依賴關係,所以InheritedWidget發生改變,依賴它的子widget就會更新,也就會呼叫剛剛所說的didChangeDependencies方法,而ancestorInheritedElementForWidgetOfExactType方法沒有和子widget註冊依賴關係,當然也不會呼叫didChangeDependencies方法。

小結

  • 以上通過一個使用InheritedWidget的簡單例子,實現了InheritedWidget的使用,瞭解了didChangeDependencies呼叫,可以說對InheritedWidget這個元件有了一定了解,接下來通過對InheritedWidget封裝,實現一個簡易的Provider實現跨元件資料共享。

實現跨元件資料共享元件

  • 作為一個原生Android 開發者,跨元件資料共享對於我們來說並不陌生,比如Android 開發中的Eventbus 就可以實現對事件訂閱者的狀態更新,並且使用SharedPreferences來資料持久化。Flutter中也有Eventbus的實現,但是這裡直接使用Flutter 提供給我們的元件InheritedWidget來實現跨元件資料共享,Flutter中比較有名的Provider核心也是通過InheritedWidget來實現的,接著我們來實現一個自己的簡易Provider。

實現通用InheritedWidget

  • 要共享的資料多種多樣,使用泛型來宣告需要共享的資料
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019-11-17
/// email: [email protected]
/// des:  實現InheritedWidget  儲存需要共享的資料InheritedWidget

import 'package:flutter/material.dart';


class InheritedProvider<T> extends InheritedWidget{

  //共享資料  外部傳入
  final T data;

  InheritedProvider({@required this.data, Widget child}):super(child:child);

  @override
  bool updateShouldNotify(InheritedProvider<T> oldWidget) {
    ///返回true,則每次更新都會呼叫依賴其的子孫節點的`didChangeDependencies`方法。
    return true;
  }

}
複製程式碼

InheritedWidget 封裝

  • 通過上面的實現,可以看到InheritedProvider中並沒有方讓呼叫者可以獲取InheritedWidget元件,彆著急,這裡需要先明確兩點;首先,資料更新通知使用ChangeNotifier(FlultterSDK提供的一個Flutter風格的釋出者-訂閱者模式類)來進行通知,其次,接收到通知之後則由訂閱者本身更新來重新構建InheritedProvider。
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019-11-17
/// email: [email protected]
/// des:  訂閱者

import 'package:flutter/material.dart';
import 'package:flutter_theme_change/provider/InheritedProvider.dart';

// 該方法用於在Dart中獲取模板型別
Type _typeOf<T>(){
  return T;
}
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget{


  final Widget child;
  final T data;

  ChangeNotifierProvider({Key key,this.child,this.data});


  //方便子樹中的widget獲取共享資料
  static T of<T> (BuildContext context,{bool listen = true}){ //listen 是否註冊依賴關係 預設註冊
    final type = _typeOf<InheritedProvider<T>>();
    final provider = listen ? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T> :
    context.ancestorInheritedElementForWidgetOfExactType(type)?.widget as InheritedProvider<T>;
    return provider.data;

  }


  @override
  State<StatefulWidget> createState() {
    return _ChangeNotifierProviderState<T>();
  }
}

class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{

  @override
  Widget build(BuildContext context) {
  //構建 InheritedProvider
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}
複製程式碼
  • 由上程式碼,建立了一個StatefulWidget,最終build構建的還是InheritedProvider,這時建立了返回對應data 資料的of方法,並且可以通過設定讓子控制元件是否與InheritedWidget繫結(上一小節已經分析過),這樣改變資料的控制元件就可以靈活的不與InheritedWidget繫結,也不用每次都更新改變資料的控制元件widget。
  • 接著我們完善 _ChangeNotifierProviderState,當外部控制元件更新資料,並通過ChangeNotifier通知更新,ChangeNotifierProvider能夠更新自身,讓新資料生效,如何更新,那就是是使用setState方法,這也是建立StatefulWidget的目的。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{

  @override
  void initState() {
    // 給model新增監聽器
    widget.data.addListener(update);
    super.initState();
  }


  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    //當Provider更新時,如果新舊資料不"==",則解綁舊資料監聽,同時新增新資料監聽
    if(widget.data != oldWidget.data){
       oldWidget.data.removeListener(update);
       widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  // build方法 省略
  ........

  @override
  void dispose() {
    // 移除model監聽器
    widget.data.removeListener(update);
    super.dispose();
  }

  void update() {
    //如果資料發生變化(model類呼叫了notifyListeners),重新構建InheritedProvider
    setState(() => {

    });
  }
複製程式碼

資料消費者封裝(Consumer)

  • 資料有更新,有訊息發出,還得有人消費,這樣訂閱者-消費者模式才完整,消費數說白了就是呼叫ChangeNotifierProvider的of方法來獲取新資料,上一步我們已經觸發訂閱者的更新,間接就會重新構建它的子widget,子widget重新構建也就是對應消費消費資料,因為消費者依賴了訂閱者本身,來看程式碼
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/18 0018
/// email: [email protected]
/// des:  事件 消費者 獲得當前context和指定資料型別的Provider
import 'package:flutter/material.dart';
import 'package:flutter_theme_change/provider/ChangeNotifierProvider.dart';

class Consumer<T> extends StatelessWidget{

  final Widget child;
  //獲得當前context
  final Widget Function(BuildContext context, T value) builder;

  Consumer({Key key,@required this.builder,this.child}):assert(builder !=null),super(key:key);


  @override
  Widget build(BuildContext context) {  //預設繫結 註冊依賴關係
    return builder(context,ChangeNotifierProvider.of<T>(context)); //自動獲取Model 獲取更新的資料
  }

}
複製程式碼
  • 由上程式碼,Consumer的build呼叫ChangeNotifierProvider.of方法預設就註冊了依賴關係,所以由Consumer實現的widget就會由InheritedWidget的功能更新資料。

小結

  • 以上小結可以用一個流程圖代替

Provider 資料共享原理流程圖

資料共享元件實踐切換主題

  • 上一節中手寫了一個非常簡單基於InheritedWidget的Provider資料共享元件,接下來通過一個切換主題的例子來使用剛剛寫好的ChangeNotifierProvider。

  • 主題切換這裡簡單的改變主題顏色,所以共享資料就是顏色值,Demo 思路為使用Dialog,提供可選擇的主題顏色,然後點選對應顏色則切換應用主題顏色,接下來一起實現。

建立主題model

  • model 也可以看做是共享資料,繼承ChangeNotifier,這樣就能夠呼叫notifyListeners方法觸發ChangeNotifierProvider收到資料改變通知
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/18 0018
/// email: [email protected]
/// des:  主題 model
import 'package:flutter/material.dart';

class ThemeModel extends ChangeNotifier {
  int settingThemeColor ;
  ThemeModel(this.settingThemeColor);

  void changeTheme (int themeColor){
    this.settingThemeColor = themeColor;
    // 通知監聽器(訂閱者),重新構建InheritedProvider, 更新狀態。
    notifyListeners();
  }
}
複製程式碼

MaterialApp作為ChangeNotifierProvider子widget

  • 改變主題顏色,也就是MaterialApp的theme 屬性,所以講 MaterialApp作為ChangeNotifierProvider子widget,這樣MaterialApp就能收到共享的主題顏色資料值
class _MyHomePageState extends State<MyHomePage> {

  int themeColor =0;
  
  @override
  void initState() {
    super.initState();
    themeColor = sp.getInt(SharedPreferencesKeys.themeColor);
    if(themeColor == null ){
      themeColor = 0xFF3391EA;//預設藍色
    }
  }
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ChangeNotifierProvider<ThemeModel>(
        data: ThemeModel(themeColor),
        child: Consumer<ThemeModel>(
          builder: (BuildContext context,themeModel){
            return MaterialApp(
              theme: ThemeData(
                primaryColor: Color(themeModel.settingThemeColor),
              ),
              home: Scaffold(
                  appBar: AppBar(
                    title: Text("Flutter Theme Change"),
                    actions: <Widget>[
                      Builder(builder: (context){
                        return IconButton(icon: new Icon(Icons.color_lens), onPressed: (){
                          _changeColor(context);
                        });
                      },)
                      // onPressed 點選事件
                    ],
                  ),
                  body: Center(
                    child: Text("主題變化測試"),
                  )
              ),
            );
          },
        ),
      ),
    );
  }

  void _changeColor(BuildContext context) {
      buildSimpleDialog(context);
  }
複製程式碼
  • 在AppBar 加入IconButton 讓其點選能顯示顏色選擇Dialog,Dialog 顯示的是一個顏色值陣列widget,每個widget實現如下
class SingleThemeColor extends StatelessWidget {

  final int themeColor;
  final String colorName;

  const SingleThemeColor({Key key,this.themeColor, this.colorName}):
        super(key:key);

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () async{
         print("點選了改變主題");
         //改變主題
         ChangeNotifierProvider.of<ThemeModel>(context,listen: false).changeTheme(this.themeColor);
         await SpUtil.getInstance()..putInt(SharedPreferencesKeys.themeColor, this.themeColor);
         Navigator.pop(context);
      },
      child: new Column( // 豎直佈局
        children: <Widget>[
           Container(
             width: 50,
             height: 50,
             margin: const EdgeInsets.all(5.0),
             decoration: BoxDecoration( //圓形背景裝飾
               borderRadius:BorderRadius.all(
                  Radius.circular(50)
               ),
               color: Color(this.themeColor)
             ),
           ),
           Text(
             colorName,
             style: TextStyle(
               color: Color(this.themeColor),
               fontSize: 14.0),
           ),
        ],
      ),
    );
  }
}
複製程式碼
  • 可以看到每個widget點選響應onTap 則呼叫ChangeNotifierProvider.of獲取ThemeModel物件呼叫changeTheme方法來觸發notifyListeners方法。還有一些細節,比如通過SharedPreferences儲存顏色值等程式碼,具體可以檢視文末demo 專案原始碼地址。
  • Demo 執行效果

theme-change

最後

  • 看到這裡,相信你應該對InheritedWidget有了比較好的理解,瞭解了原理,使用起輪子來也會更加得心應手吧。如果要使用跨元件資料共享,還是直接使用功能完整的Provider吧。又一篇文章完成了,相信多少都會對看到文章的你有幫助,文章中如果有錯誤,請大家給我提出來,大家一起學習進步,如果覺得我的文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問我的個人部落格

Demo 地址

參考

About me

blog:

mail: