Flutter資料傳遞 分為兩種方式。一種是沿著數的方向從上向下傳遞狀態。另一種是 從下往上傳遞狀態值。
沿著樹的方向,向下傳遞狀態
按照Widgets Tree的方向,從上往子樹和節點上傳遞狀態。
InheritedWidget & ValueNotifier
InheritedWidget
這個既熟悉又陌生類可以幫助我們在Flutter中沿著樹向下傳遞資訊。
我們經常通過這樣的方式,通過BuildContext
,可以拿到Theme
和MediaQuery
//得到狀態列的高度
var statusBarHeight = MediaQuery.of(context).padding.top;
//複製合併出新的主題
var copyTheme =Theme.of(context).copyWith(primaryColor: Colors.blue);
複製程式碼
看到of
的靜態方法,第一反應是去通過這個context
去構建新的類。然後從這個類中,去呼叫獲取狀態的方法。(Android開發的同學應該很熟悉的套路,類似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狀態儲存的原理
-
繼承
InheritedWidget
-
通過
build
方法中返回
-
MaterialApp
的_MaterialAppState
中的build
方法 -
WidgetsApp
的_WidgetsAppState
中的build
方法
- 獲取
最後就是最上面看到的那段程式碼,通過
context.inheritFromWidgetOfExactType
來獲取。 然後在子樹的任何地方,都可以通過這樣的方式來進行獲取。
定義一個AppState
瞭解了MediaQuery
的存放方式,我們可以實現自己的狀態管理,這樣在子元件中,就可以同步獲取到狀態值。
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,這個方法通常都是這樣實現的。如果新的值和舊的值不相等,就需要notify
@override
bool updateShouldNotify(_InheritedStateContainer oldWidget) =>
data != oldWidget.data;
}
複製程式碼
2. 建立外層的Widget
建立外層的Widget
,並且提供靜態方法of
,來得到我們的AppState
/*
1. 從MediaQuery模仿的套路,我們知道,我們需要一個StatefulWidget作為外層的元件,
將我們的繼承於InheritateWidget的元件build出去
*/
class AppStateContainer extends StatefulWidget {
//這個state是我們需要的狀態
final AppState state;
//這個child的是必須的,來顯示我們正常的控制元件
final Widget child;
AppStateContainer({this.state, @required this.child});
//4.模仿MediaQuery,提供一個of方法,來得到我們的State.
static AppState of(BuildContext context) {
//這個方法內,呼叫 context.inheritFromWidgetOfExactType
return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
as _InheritedStateContainer)
.data;
}
@override
_AppStateContainerState createState() => _AppStateContainerState();
}
class _AppStateContainerState extends State<AppStateContainer> {
//2. 在build方法內返回我們的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方法中,就可以查到對應的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(
//根據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: () {
//點選按鈕進行切換
//因為是全域性的狀態,在其他頁面改變,也會導致這裡發生變化
appState.isLoading = !appState.isLoading;
//setState觸發頁面重新整理
setState(() {});
},
tooltip: 'Increment',
child: new Icon(Icons.swap_horiz),
);
}));
}
}
複製程式碼
執行效果1-當前頁面
點選按鈕更改狀態。
4. 在另外一個頁面修改AppState
因為上面程式碼是在一個頁面內的情況,我們要確定是否全域性的狀態是保持一致的。所以 讓我們再改一下程式碼,點選push出新的頁面,在新頁面內改變appState的狀態,看看就頁面會不會發生變化。 程式碼修改如下:
//修改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-另外一個頁面內修改狀態
在push的頁面修改AppState的狀態,回到初始的頁面,看狀態是否發生變化。
小結和思考
通過分析MediaQuery
,我們瞭解到了InheritedWidget
的用法,並且通過自定義的AppState
等操作熟悉了整體狀態控制的流程。
我們可以繼續思考下面幾個問題
-
為什麼
AppState
能在整個App週期中,維持狀態呢? 因為我們將其包裹在了最外層。 由此思考,每個頁面可能也有自己的狀態,維護頁面的狀態,可以將其包裹在頁面的層級的最外層,這樣它就變成了PageScope
的狀態了。 -
限制-like a EventBus 當我們改變
state
並關閉頁面後,因為didChangeDependencies
方法和build
方法的執行,我們開啟這個頁面時,總能拿到最新的state
。所以我們的頁面能夠同步狀態成功。 那如果是像EventBus一樣,push出一個狀態,我們需要去進行一個耗時操作,然後才能發生的改變我們能監聽和處理嗎?
ValueNotifier
繼承至ChangeNotifier
。可以註冊監聽事件。當值發生改變時,會給監聽則傳送監聽。
/// 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
值,相當於呼叫set
方法,都會notifyListeners
修改程式碼
AppState新增成員
//定義一個變數來儲存
class AppState {
//...忽略重複程式碼。新增成員變數
ValueNotifier<bool> canListenLoading = ValueNotifier(false);
}
複製程式碼
_MyHomeInheritedPageState 中新增監聽
class _MyHomeInheritedPageState extends State<MyInheritedHomePage> {
//...忽略重複程式碼。新增成員變數
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
if (appState == null) {
print('state == null');
appState = AppStateContainer.of(context);
//在這裡新增監聽事件
appState.canListenLoading.addListener(listener);
}
}
@override
void dispose() {
print('dispose');
if (appState != null) {
//在這裡移除監聽事件
appState.canListenLoading.removeListener(listener);
}
super.dispose();
}
@override
void initState() {
print('initState');
//初始化監聽的回撥。回撥用作的就是延遲5s後,將result修改成 "From delay"
listener = () {
Future.delayed(Duration(seconds: 5)).then((value) {
result = "From delay";
setState(() {});
});
};
super.initState();
}
//新增成員變數。 result引數和 listener回撥
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}',
),
],
),
),
//...忽略重複程式碼
}
}
複製程式碼
執行結果
執行結果和我們預想的一樣。
- 顯示開啟一個新的頁面。
- 在新的頁面內改變
canListenLoading
的value。這樣會觸發上一個頁面已經註冊的監聽事件(4s後改變值)。 - 然後我們退回來,等待後確實發現了資料發生了變化~~
這樣就感覺可以實現一個類似EventBus的功能了~~
小結
這邊文章,主要說的是,利用Flutter自身的框架來實現,狀態管理和訊息傳遞的內容。
- 通過
InheritedWidget
來儲存狀態 - 通過
context.inheritFromWidgetOfExactType
來獲取屬性 - 使用
ValueNotifer
來實現屬性監聽。
我們可以對從上往下的資料傳遞、狀態管理做一個小結
-
Key 儲存
Widget
的狀態,我們可以通過給對應Widget
的key
,來儲存狀態,並通過Key來拿到狀態。 比如是ObjectKey
可以在列表中標記唯一的Key
,來儲存狀態,讓動畫識別。GlobalKey
,則可以儲存一個狀態,其他地方都可以獲取。 -
InheritedWidget
可以持有一個狀態,共它的子樹來獲取。 這樣子樹本身可以不直接傳入這個欄位(這樣可以避免多級的Widget時,要一層一層向下傳遞狀態) 還可以做不同Widget
中間的狀態同步 -
ChangeNofier
繼承這裡類,我們就可以實現Flutter
中的觀察者模式,對屬性變化做觀察。
另外,我們還可以通過第三方庫
,比如說 Redux
和ScopeModel
Rx
來做這個事情。但是其基於的原理,應該也是上方的內容。
從下往上傳遞分發狀態值
Notification
我們知道,我們可以通過NotificationListener
的方式來監聽ScrollNotification
頁面的滾動情況。Flutter中就是通過這樣的方式,通過來從子元件往父元件的BuildContext
中釋出資料,完成資料傳遞的。
下面我們簡單的來實現一個我們自己的。
- 程式碼
//0.自定義一個Notification。
class MyNotification extends Notification {}
class _MyHomePageState extends State<MyHomePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//2.在Scaffold的層級進行事件的監聽。建立`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.注意,這裡是監聽不到事件的。這裡需要監聽到事件,需要在body自己的`BuildContext`傳送事件才行!!!!
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.建立事件,並通過傳送到對應的`BuildContext`中。注意,這裡的`context`是`Scaffold`的`BuildContext`
new MyNotification().dispatch(context);
},
tooltip: 'ChangeState',
child: new Icon(Icons.add),
);
})));
}
}
複製程式碼
- 執行結果
小結
我們可以通過Notification的繼承類,將其釋出到對應的BuildContext
中,來實現資料傳遞。
總結
通過這邊Flutter資料傳遞的介紹,我們可以大概搭建自己的Flutter App的資料流結構。 類似閒魚的介面的架構設計。
-
從上往下: 通過自定義不同
Scope
的InheritedWidget
來hold住不同Scope
的資料,這樣對應的Scope
下的子元件都能得到對應的資料,和得到對應的更新。 -
從下往上: 通過自定義的
Notification
類。在子元件中通過Notification(data).dispatch(context)
這樣的方式釋出,在對應的Context
上,在通過NotificationListener
進行捕獲和監聽。
最後
通過三遍文章,對Flutter
文件中一些細節做了必要的入門補充。
還沒有介紹相關的 手勢
,網路請求
,Channel和Native通訊
,還有動畫
等內容。請結合文件學習。
在豐富了理論知識之後,下一編開始,我們將進行Flutter
的實戰分析。
參考文章
Build reactive mobile apps in Flutter — companion article
set-up-inherited-widget-app-state
深入瞭解Flutter介面開發(強烈推薦) (ps:真的強烈推薦)