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