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值,如果資料共享則它的值就會跟隨變化。
-
執行效果
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的功能更新資料。
小結
- 以上小結可以用一個流程圖代替
資料共享元件實踐切換主題
-
上一節中手寫了一個非常簡單基於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 執行效果
最後
- 看到這裡,相信你應該對InheritedWidget有了比較好的理解,瞭解了原理,使用起輪子來也會更加得心應手吧。如果要使用跨元件資料共享,還是直接使用功能完整的Provider吧。又一篇文章完成了,相信多少都會對看到文章的你有幫助,文章中如果有錯誤,請大家給我提出來,大家一起學習進步,如果覺得我的文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問我的個人部落格。