Flutter 數據共享 InheritedWidget

image

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

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值,若是數據共享則它的值就會跟隨變化。git

  • 運行效果github

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: 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,提供可選擇的主題顏色,而後點擊對應顏色則切換應用主題顏色,接下來一塊兒實現。bash

建立主題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

最後

  • 看到這裏,相信你應該對InheritedWidget有了比較好的理解,瞭解了原理,使用起輪子來也會更加駕輕就熟吧。若是要使用跨組件數據共享,仍是直接使用功能完整的Provider吧。又一篇文章完成了,相信多少都會對看到文章的你有幫助,文章中若是有錯誤,請你們給我提出來,你們一塊兒學習進步,若是以爲個人文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問個人我的博客
  • Flutter完整開源項目: github.com/maoqitian/f…

Demo 地址

參考

About me

blog:

mail:

相關文章
相關標籤/搜索