Flutter入门三部曲(3) - 数据传递/状态管理 | 掘金技术征文
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);@overrideString toString() {return 'AppState{isLoading: $isLoading}';} }復制代碼1. 繼承InheritedWidget
//1. 模仿MediaQuery。簡單的讓這個持有我們想要保存的data class _InheritedStateContainer extends InheritedWidget {final AppState data;//我們知道InheritedWidget總是包裹的一層,所以它必有child_InheritedStateContainer({Key key, this.data, Widget child}): super(key: key, child: child);//參考MediaQuery,這個方法通常都是這樣實現的。如果新的值和舊的值不相等,就需要notifybool 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, this.child});//4.模仿MediaQuery,提供一個of方法,來得到我們的State.static AppState of(BuildContext context) {//這個方法內,調用 context.inheritFromWidgetOfExactTypereturn (context.inheritFromWidgetOfExactType(_InheritedStateContainer)as _InheritedStateContainer).data;}_AppStateContainerState createState() => _AppStateContainerState(); }class _AppStateContainerState extends State<AppStateContainer> {//2. 在build方法內返回我們的InheritedWidget//這樣App的層級就是 AppStateContainer->_InheritedStateContainer-> real appWidget build(BuildContext context) {return _InheritedStateContainer(data: widget.state,child: widget.child,);} } 復制代碼3. 使用
- 包括在最外層
- 在任何你想要的位置中,使用。 文檔里面推薦,在didChangeDependencies中查詢它。所以我們也
運行效果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
運行效果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.T get value => _value;T _value;set value(T newValue) {if (_value == newValue)return;_value = newValue;notifyListeners();}String toString() => '${describeIdentity(this)}($value)'; } 復制代碼源碼看到,只要改變值value值,相當于調用set方法,都會notifyListeners
修改代碼
AppState添加成員
//定義一個變量來存儲 class AppState {//...忽略重復代碼。添加成員變量ValueNotifier<bool> canListenLoading = ValueNotifier(false); } 復制代碼_MyHomeInheritedPageState 中添加監聽
class _MyHomeInheritedPageState extends State<MyInheritedHomePage> {//...忽略重復代碼。添加成員變量void didChangeDependencies() {super.didChangeDependencies();print('didChangeDependencies');if (appState == null) {print('state == null');appState = AppStateContainer.of(context);//在這里添加監聽事件appState.canListenLoading.addListener(listener);}}void dispose() {print('dispose');if (appState != null) {//在這里移除監聽事件appState.canListenLoading.removeListener(listener);}super.dispose();}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;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中發布數據,完成數據傳遞的。 下面我們簡單的來實現一個我們自己的。
- 代碼
- 運行結果
小結
我們可以通過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:真的強烈推薦)
從 0 到 1:我的 Flutter 技術實踐 | 掘金技術征文,征文活動正在進行中
總結
以上是生活随笔為你收集整理的Flutter入门三部曲(3) - 数据传递/状态管理 | 掘金技术征文的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mybatis$和#的区别
- 下一篇: ansible笔记(5):常用模块之文件