Flutter 之數據共享 InheritedWidget

flutter-logo.png

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

InheritedWidget 實現組件數據共享

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

實現ShareDataWidget

/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019/11/15 0015
/// email: maoqitian068@163.com
/// 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: maoqitian068@163.com
/// 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: maoqitian068@163.com
/// 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值,如果數據共享則它的值就會跟隨變化。

  • 運行效果

demo.gif

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 就可以實現對事件訂閱者的狀態更新,Flutter中也有Eventbus的實現,但是這里直接使用Flutter 提供給我們的組件InheritedWidget來實現跨組件數據共享,Flutter中比較有名的Provider核心也是通過InheritedWidget來實現的,接著我們來實現一個自己的簡易Provider。

實現通用InheritedWidget

  • 要共享的數據多種多樣,使用泛型來聲明需要共享的數據
/// Created with Android Studio.
/// User: maoqitian
/// Date: 2019-11-17
/// email: maoqitian068@163.com
/// 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: maoqitian068@163.com
/// 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: maoqitian068@163.com
/// 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: maoqitian068@163.com
/// 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.gif

最后

  • 看到這里,相信你應該對InheritedWidget有了比較好的理解,了解了原理,使用起輪子來也會更加得心應手吧。如果要使用跨組件數據共享,還是直接使用功能完整的Provider吧。又一篇文章完成了,相信多少都會對看到文章的你有幫助,文章中如果有錯誤,請大家給我提出來,大家一起學習進步,如果覺得我的文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問我的個人博客
  • Flutter完整開源項目: https://github.com/maoqitian/flutter_wanandroid

Demo 地址

參考

About me

blog:

mail:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。