Flutter數(shù)據(jù)傳遞
分為兩種方式。一種是沿著數(shù)的方向從上向下傳遞狀態(tài)探橱。另一種是 從下往上傳遞狀態(tài)值资柔。
沿著樹的方向,從上向下傳遞數(shù)據(jù)淑际、狀態(tài)
按照Widgets Tree的方向畏纲,從上往子樹和節(jié)點上傳遞狀態(tài)扇住。
InheritedWidget & ValueNotifier
InheritedWidget
這個既熟悉又陌生類可以幫助我們在Flutter中沿著樹向下傳遞信息。這個類只是簡單的保存了一個狀態(tài)而已盗胀。
我們經(jīng)常通過這樣的方式艘蹋,通過BuildContext
,可以拿到Theme
和MediaQuery
//得到狀態(tài)欄的高度
var statusBarHeight = MediaQuery.of(context).padding.top;
//復(fù)制合并出新的主題
var copyTheme =Theme.of(context).copyWith(primaryColor: Colors.blue);
看到of
的靜態(tài)方法,第一反應(yīng)是去通過這個context
去構(gòu)建新的類票灰。然后從這個類中女阀,去調(diào)用獲取狀態(tài)的方法。(Android開發(fā)的同學(xué)應(yīng)該很熟悉的套路屑迂,類似Picasso浸策、Glide)。但事實上是這樣嗎惹盼?
MediaQuery
通過context.inheritFromWidgetOfExactType
static MediaQueryData of(BuildContext context, { bool nullOk: false }) {
assert(context != null);
assert(nullOk != null);
final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery);
if (query != null)
return query.data;
if (nullOk)
return null;
throw new FlutterError(
'MediaQuery.of() called with a context that does not contain a MediaQuery.\n'
'No MediaQuery ancestor could be found starting from the context that was passed '
'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
'if the context you use comes from a widget above those widgets.\n'
'The context used was:\n'
' $context'
);
}
- 首先庸汗,可以看到通過這個方法
context.inheritFromWidgetOfExactType
來查到MediaQuery
。
MediaQuery
是我們存在在BuildContext
中的屬性手报。 - 其次蚯舱,可以看到
MediaQuery
存儲在的BuildContext
中的位置是在WidgetsApp
.(因為其實MaterialApp
返回的也是它)
MediaQuery狀態(tài)保存的原理
-
繼承
InheritedWidget
image.png 通過
build
方法中返回
-
MaterialApp
的_MaterialAppState
中的build
方法
image.png -
WidgetsApp
的_WidgetsAppState
中的build
方法
image.png
-
獲取
最后就是最上面看到的那段代碼,通過context.inheritFromWidgetOfExactType
來獲取掩蛤。
然后在子樹的任何地方枉昏,都可以通過這樣的方式來進行獲取。
定義一個AppState
了解了MediaQuery
的存放方式揍鸟,我們可以實現(xiàn)自己的狀態(tài)管理兄裂,這樣在子組件中,就可以同步獲取到狀態(tài)值阳藻。
0.先定義一個AppState
//0. 定義一個變量來存儲
class AppState {
bool isLoading;
AppState({this.isLoading = true});
factory AppState.loading() => AppState(isLoading: true);
@override
String toString() {
return 'AppState{isLoading: $isLoading}';
}
}
1. 繼承InheritedWidget
//1. 模仿MediaQuery晰奖。簡單的讓這個持有我們想要保存的data
class _InheritedStateContainer extends InheritedWidget {
final AppState data;
//我們知道InheritedWidget總是包裹的一層,所以它必有child
_InheritedStateContainer(
{Key key, @required this.data, @required Widget child})
: super(key: key, child: child);
//參考MediaQuery,這個方法通常都是這樣實現(xiàn)的稚配。如果新的值和舊的值不相等畅涂,就需要notify
@override
bool updateShouldNotify(_InheritedStateContainer oldWidget) =>
data != oldWidget.data;
}
2. 創(chuàng)建外層的Widget
創(chuàng)建外層的Widget
,并且提供靜態(tài)方法of
,來得到我們的AppState
/*
1. 從MediaQuery模仿的套路道川,我們知道午衰,我們需要一個StatefulWidget作為外層的組件,
將我們的繼承于InheritateWidget的組件build出去
*/
class AppStateContainer extends StatefulWidget {
//這個state是我們需要的狀態(tài)
final AppState state;
//這個child的是必須的冒萄,來顯示我們正常的控件
final Widget child;
AppStateContainer({this.state, @required this.child});
//4.模仿MediaQuery,提供一個of方法臊岸,來得到我們的State.
static AppState of(BuildContext context) {
//這個方法內(nèi),調(diào)用 context.inheritFromWidgetOfExactType
return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
as _InheritedStateContainer)
.data;
}
@override
_AppStateContainerState createState() => _AppStateContainerState();
}
class _AppStateContainerState extends State<AppStateContainer> {
//2. 在build方法內(nèi)返回我們的InheritedWidget
//這樣App的層級就是 AppStateContainer->_InheritedStateContainer-> real app
@override
Widget build(BuildContext context) {
return _InheritedStateContainer(
data: widget.state,
child: widget.child,
);
}
}
3. 使用
- 包括在最外層
class MyInheritedApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
//因為是AppState尊流,所以他的范圍是全生命周期的帅戒,所以可以直接包裹在最外層
return AppStateContainer(
//初始化一個loading
state: AppState.loading(),
child: new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
- 在任何你想要的位置中,使用。
文檔里面推薦逻住,在didChangeDependencies
中查詢它钟哥。所以我們也
class _MyHomePageState extends State<MyHomePage> {
_MyHomePageState() {}
AppState appState;
//在didChangeDependencies方法中,就可以查到對應(yīng)的state了
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
if(appState==null){
appState= AppStateContainer.of(context);
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
//根據(jù)isLoading來判斷瞎访,顯示一個loading腻贰,或者是正常的圖
child: appState.isLoading
? CircularProgressIndicator()
: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${appState.isLoading}',
),
],
),
),
floatingActionButton: new Builder(builder: (context) {
return new FloatingActionButton(
onPressed: () {
//點擊按鈕進行切換
//因為是全局的狀態(tài),在其他頁面改變扒秸,也會導(dǎo)致這里發(fā)生變化
appState.isLoading = !appState.isLoading;
//setState觸發(fā)頁面刷新
setState(() {});
},
tooltip: 'Increment',
child: new Icon(Icons.swap_horiz),
);
}));
}
}
運行效果1-當(dāng)前頁面
點擊按鈕更改狀態(tài)播演。
4. 在另外一個頁面修改AppState
因為上面代碼是在一個頁面內(nèi)的情況,我們要確定是否全局的狀態(tài)是保持一致的伴奥。所以
讓我們再改一下代碼写烤,點擊push出新的頁面,在新頁面內(nèi)改變appState的狀態(tài)拾徙,看看就頁面會不會發(fā)生變化洲炊。
代碼修改如下:
//修改floatingButton的點擊事件
floatingActionButton: new Builder(builder: (context) {
return new FloatingActionButton(
onPressed: () {
//push出一個先的頁面
Navigator.of(context).push(
new MaterialPageRoute<Null>(builder: (BuildContext context) {
return MyHomePage(
title: 'Second State Change Page');
}));
//注釋掉原來的代碼
// appState.isLoading = !appState.isLoading;
// setState(() {});
},
tooltip: 'Increment',
child: new Icon(Icons.swap_horiz),
);
})
- 新增的
MyHomePage
基本上和上面的代碼一致。同樣讓他修改appState
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _changeState() {
setState(() {
state.isLoading = !state.isLoading;
});
}
AppState state;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if(state ==null){
state = AppStateContainer.of(context);
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${state.isLoading}',
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _changeState,
tooltip: 'ChangeState',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
運行效果2-另外一個頁面內(nèi)修改狀態(tài)
在push的頁面修改AppState的狀態(tài)尼啡,回到初始的頁面选浑,看狀態(tài)是否發(fā)生變化。
小結(jié)和思考
通過分析MediaQuery
玄叠,我們了解到了InheritedWidget
的用法,并且通過自定義的AppState
等操作熟悉了整體狀態(tài)控制的流程拓提。
我們可以繼續(xù)思考下面幾個問題
為什么
AppState
能在整個App周期中读恃,維持狀態(tài)呢?
因為我們將其包裹在了最外層代态。
由此思考寺惫,每個頁面可能也有自己的狀態(tài),維護頁面的狀態(tài)蹦疑,可以將其包裹在頁面的層級的最外層西雀,這樣它就變成了PageScope
的狀態(tài)了。限制-like a EventBus
當(dāng)我們改變state
并關(guān)閉頁面后歉摧,因為didChangeDependencies
方法和build
方法的執(zhí)行艇肴,我們打開這個頁面時,總能拿到最新的state
叁温。所以我們的頁面能夠同步狀態(tài)成功再悼。
那如果是像EventBus一樣,push出一個狀態(tài)膝但,我們需要去進行一個耗時操作冲九,然后才能發(fā)生的改變我們能監(jiān)聽和處理嗎?
ValueNotifier
繼承至ChangeNotifier
跟束≥杭椋可以注冊監(jiān)聽事件丑孩。當(dāng)值發(fā)生改變時,會給監(jiān)聽則發(fā)送監(jiān)聽灭贷。
/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);
/// The current value stored in this notifier.
///
/// When the value is replaced, this class notifies its listeners.
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
源碼看到温学,只要改變值value
值,相當(dāng)于調(diào)用set
方法氧腰,都會notifyListeners
修改代碼
AppState添加成員
//定義一個變量來存儲
class AppState {
//...忽略重復(fù)代碼枫浙。添加成員變量
ValueNotifier<bool> canListenLoading = ValueNotifier(false);
}
_MyHomeInheritedPageState 中添加監(jiān)聽
class _MyHomeInheritedPageState extends State<MyInheritedHomePage> {
//...忽略重復(fù)代碼。添加成員變量
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
if (appState == null) {
print('state == null');
appState = AppStateContainer.of(context);
//在這里添加監(jiān)聽事件
appState.canListenLoading.addListener(listener);
}
}
@override
void dispose() {
print('dispose');
if (appState != null) {
//在這里移除監(jiān)聽事件
appState.canListenLoading.removeListener(listener);
}
super.dispose();
}
@override
void initState() {
print('initState');
//初始化監(jiān)聽的回調(diào)古拴÷嶂悖回調(diào)用作的就是延遲5s后,將result修改成 "From delay"
listener = () {
Future.delayed(Duration(seconds: 5)).then((value) {
result = "From delay";
setState(() {});
});
};
super.initState();
}
//添加成員變量黄痪。 result參數(shù)和 listener回調(diào)
String result = "";
VoidCallback listener;
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: appState.isLoading
? CircularProgressIndicator()
: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${appState.isLoading}',
),
//新增紧帕,result的顯示在屏幕上
new Text(
'${result}',
),
],
),
),
//...忽略重復(fù)代碼
}
}
運行結(jié)果
運行結(jié)果和我們預(yù)想的一樣。
- 顯示打開一個新的頁面桅打。
- 在新的頁面內(nèi)改變
canListenLoading
的value是嗜。這樣會觸發(fā)上一個頁面已經(jīng)注冊的監(jiān)聽事件(4s后改變值)。 -
然后我們退回來挺尾,等待后確實發(fā)現(xiàn)了數(shù)據(jù)發(fā)生了變化~~
21.gif
這樣就感覺可以實現(xiàn)一個類似EventBus的功能了~~
總結(jié)
這邊文章鹅搪,主要說的是,利用Flutter自身的框架來實現(xiàn)遭铺,狀態(tài)管理和消息傳遞的內(nèi)容丽柿。
- 通過
InheritedWidget
來保存狀態(tài) - 通過
context.inheritFromWidgetOfExactType
來獲取屬性 - 使用
ValueNotifer
來實現(xiàn)屬性監(jiān)聽。
我們可以對狀態(tài)管理做一個小結(jié)
Key
保存Widget
的狀態(tài)魂挂,我們可以通過給對應(yīng)Widget
的key
,來保存狀態(tài)甫题,并通過Key來拿到狀態(tài)。
比如是ObjectKey
可以在列表中標記唯一的Key
涂召,來保存狀態(tài)坠非,讓動畫識別。
GlobalKey
果正,則可以保存一個狀態(tài)炎码,其他地方都可以獲取。InheritedWidget
可以持有一個狀態(tài)舱卡,共它的子樹來獲取辅肾。
這樣子樹本身可以不直接傳入這個字段(這樣可以避免多級的Widget時,要一層一層向下傳遞狀態(tài))
還可以做不同Widget
中間的狀態(tài)同步ChangeNofier
繼承這里類轮锥,我們就可以實現(xiàn)Flutter
中的觀察者模式矫钓,對屬性變化做觀察。
另外,我們還可以通過第三方庫
新娜,比如說 Redux
和ScopeModel
Rx
來做這個事情赵辕。但是其基于的原理,應(yīng)該也是上方的內(nèi)容概龄。
從下往上傳遞分發(fā)數(shù)據(jù)还惠、狀態(tài)
Notification
我們知道,我們可以通過NotificationListener
的方式來監(jiān)聽ScrollNotification
頁面的滾動情況私杜。Flutter中就是通過這樣的方式蚕键,通過來從子組件往父組件的BuildContext
中發(fā)布數(shù)據(jù),完成數(shù)據(jù)傳遞的衰粹。
下面我們簡單的來實現(xiàn)一個我們自己的锣光。
- 代碼
//0.自定義一個Notification。
class MyNotification extends Notification {}
class _MyHomePageState extends State<MyHomePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//2.在Scaffold的層級進行事件的監(jiān)聽铝耻。創(chuàng)建`NotificationListener`,并在`onNotification`就可以得到我們的事件了誊爹。
return NotificationListener(
onNotification: (event) {
if (event is MyNotification) {
print("event= Scaffold MyNotification");
}
},
child: new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
//3.注意,這里是監(jiān)聽不到事件的瓢捉。這里需要監(jiān)聽到事件频丘,需要在body自己的`BuildContext`發(fā)送事件才行!E萏B!
body: new NotificationListener<MyNotification>(
onNotification: (event) {
//接受不到事件某弦,因為`context`不同
print("body event=" + event.toString());
},
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ',
),
new Text(
'appState.canListenLoading.value',
),
],
),
)),
floatingActionButton: Builder(builder: (context) {
return FloatingActionButton(
onPressed: () {
//1.創(chuàng)建事件状答,并通過發(fā)送到對應(yīng)的`BuildContext`中。注意刀崖,這里的`context`是`Scaffold`的`BuildContext`
new MyNotification().dispatch(context);
},
tooltip: 'ChangeState',
child: new Icon(Icons.add),
);
})));
}
}
-
運行結(jié)果
image.png
小結(jié)
我們可以通過Notification的繼承類,將其發(fā)布到對應(yīng)的buildContext
中拍摇,來實現(xiàn)數(shù)據(jù)傳遞亮钦。
總結(jié)
通過這邊Flutter數(shù)據(jù)傳遞的介紹,我們可以大概搭建自己的Flutter App的數(shù)據(jù)流結(jié)構(gòu)充活。
類似閑魚的界面的架構(gòu)設(shè)計蜂莉。
從上往下:
通過自定義不同Scope
的InheritedWidget
來hold住不同Scope
的數(shù)據(jù),這樣當(dāng)前Scope下的子組件都能得到對應(yīng)的數(shù)據(jù)混卵,和得到對應(yīng)的更新映穗。從下往上:
通過自定義的Notification
類。在子組件中通過Notification(data).dispatch(context)
這樣的方式發(fā)布幕随,在對應(yīng)的Context
上蚁滋,在通過NotificationListener
進行捕獲和監(jiān)聽。
最后
通過三遍文章,對Flutter
文檔中一些細節(jié)做了必要的入門補充辕录。
還沒有介紹相關(guān)的 手勢
睦霎,網(wǎng)絡(luò)請求
,Channel和Native通信
走诞,還有動畫
等內(nèi)容副女。請結(jié)合文檔學(xué)習(xí)。
在豐富了理論知識之后蚣旱,下一編開始碑幅,我們將進行Flutter
的實戰(zhàn)分析。
參考文章
Build reactive mobile apps in Flutter?—?companion article
set-up-inherited-widget-app-state
深入了解Flutter界面開發(fā)(強烈推薦) (ps:真的強烈推薦)