Flutter數據傳遞 分爲兩種方式。一種是沿着數的方向從上向下傳遞狀態。另外一種是 從下往上傳遞狀態值。react
按照Widgets Tree的方向,從上往子樹和節點上傳遞狀態。bash
這個既熟悉又陌生類能夠幫助咱們在Flutter中沿着樹向下傳遞信息。 咱們常常經過這樣的方式,經過BuildContext
,能夠拿到Theme
和MediaQuery
markdown
//獲得狀態欄的高度
var statusBarHeight = MediaQuery.of(context).padding.top;
//複製合併出新的主題
var copyTheme =Theme.of(context).copyWith(primaryColor: Colors.blue);
複製代碼
看到of
的靜態方法,第一反應是去經過這個context
去構建新的類。而後從這個類中,去調用獲取狀態的方法。(Android開發的同窗應該很熟悉的套路,相似Picasso、Glide)。但事實上是這樣嗎?網絡
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
返回的也是它)繼承InheritedWidget
架構
經過build
方法中返回app
MaterialApp
的_MaterialAppState
中的build
方法 框架
WidgetsApp
的_WidgetsAppState
中的build
方法 less
context.inheritFromWidgetOfExactType
來獲取。 而後在子樹的任何地方,均可以經過這樣的方式來進行獲取。瞭解了MediaQuery
的存放方式,咱們能夠實現本身的狀態管理,這樣在子組件中,就能夠同步獲取到狀態值。ide
//0. 定義一個變量來存儲
class AppState {
bool isLoading;
AppState({this.isLoading = true});
factory AppState.loading() => AppState(isLoading: true);
@override
String toString() {
return 'AppState{isLoading: $isLoading}';
}
}
複製代碼
//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;
}
複製代碼
建立外層的Widget
,而且提供靜態方法of
,來獲得咱們的AppState
post
/* 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,
);
}
}
複製代碼
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),
);
}));
}
}
複製代碼
點擊按鈕更改狀態。
由於上面代碼是在一個頁面內的狀況,咱們要肯定是否全局的狀態是保持一致的。因此 讓咱們再改一下代碼,點擊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.
);
}
}
複製代碼
在push的頁面修改AppState的狀態,回到初始的頁面,看狀態是否發生變化。
經過分析MediaQuery
,咱們瞭解到了InheritedWidget
的用法,而且經過自定義的AppState
等操做熟悉了總體狀態控制的流程。 咱們能夠繼續思考下面幾個問題
爲何AppState
能在整個App週期中,維持狀態呢? 由於咱們將其包裹在了最外層。 由此思考,每一個頁面可能也有本身的狀態,維護頁面的狀態,能夠將其包裹在頁面的層級的最外層,這樣它就變成了PageScope
的狀態了。
限制-like a EventBus 當咱們改變state
並關閉頁面後,由於didChangeDependencies
方法和build
方法的執行,咱們打開這個頁面時,總能拿到最新的state
。因此咱們的頁面可以同步狀態成功。 那若是是像EventBus同樣,push出一個狀態,咱們須要去進行一個耗時操做,而後才能發生的改變咱們能監聽和處理嗎?
繼承至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
//定義一個變量來存儲
class AppState {
//...忽略重複代碼。添加成員變量
ValueNotifier<bool> canListenLoading = ValueNotifier(false);
}
複製代碼
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
來作這個事情。可是其基於的原理,應該也是上方的內容。
咱們知道,咱們能夠經過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:真的強烈推薦)