Flutter入門三部曲(3) - 數據傳遞/狀態管理 | 掘金技術徵文

Flutter數據傳遞 分爲兩種方式。一種是沿着數的方向從上向下傳遞狀態。另外一種是 從下往上傳遞狀態值。react

沿着樹的方向,向下傳遞狀態

按照Widgets Tree的方向,從上往子樹和節點上傳遞狀態。bash

InheritedWidget & ValueNotifier

InheritedWidget

這個既熟悉又陌生類能夠幫助咱們在Flutter中沿着樹向下傳遞信息。 咱們常常經過這樣的方式,經過BuildContext,能夠拿到ThemeMediaQuerymarkdown

//獲得狀態欄的高度
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來查到MediaQueryMediaQuery是咱們存在在BuildContext中的屬性。
  • 其次,能夠看到MediaQuery存儲在的BuildContext中的位置是在WidgetsApp.(由於其實MaterialApp返回的也是它)
MediaQuery狀態保存的原理
  • 繼承InheritedWidget 架構

    image.png

  • 經過build方法中返回app

  1. MaterialApp_MaterialAppState中的build方法 框架

    image.png

  2. WidgetsApp_WidgetsAppState中的build方法 less

    image.png

  • 獲取 最後就是最上面看到的那段代碼,經過context.inheritFromWidgetOfExactType來獲取。 而後在子樹的任何地方,均可以經過這樣的方式來進行獲取。

定義一個AppState

瞭解了MediaQuery的存放方式,咱們能夠實現本身的狀態管理,這樣在子組件中,就能夠同步獲取到狀態值。ide

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,來獲得咱們的AppStatepost

/* 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-當前頁面

點擊按鈕更改狀態。

21.gif

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的狀態,回到初始的頁面,看狀態是否發生變化。

21.gif

小結和思考

經過分析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後改變值)。
  • 而後咱們退回來,等待後確實發現了數據發生了變化~~
    21.gif

這樣就感受能夠實現一個相似EventBus的功能了~~

小結

這邊文章,主要說的是,利用Flutter自身的框架來實現,狀態管理和消息傳遞的內容。

  • 經過InheritedWidget來保存狀態
  • 經過context.inheritFromWidgetOfExactType來獲取屬性
  • 使用ValueNotifer來實現屬性監聽。

咱們能夠對從上往下的數據傳遞、狀態管理作一個小結

  • Key 保存Widget的狀態,咱們能夠經過給對應Widgetkey,來保存狀態,並經過Key來拿到狀態。 好比是 ObjectKey能夠在列表中標記惟一的Key,來保存狀態,讓動畫識別。 GlobalKey,則能夠保存一個狀態,其餘地方均可以獲取。

  • InheritedWidget 能夠持有一個狀態,共它的子樹來獲取。 這樣子樹自己能夠不直接傳入這個字段(這樣能夠避免多級的Widget時,要一層一層向下傳遞狀態) 還能夠作不一樣Widget中間的狀態同步

  • ChangeNofier 繼承這裏類,咱們就能夠實現Flutter中的觀察者模式,對屬性變化作觀察。

另外,咱們還能夠經過第三方庫,好比說 ReduxScopeModel 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),
              );
            })));
  }
}

複製代碼
  • 運行結果
    image.png

小結

咱們能夠經過Notification的繼承類,將其發佈到對應的BuildContext中,來實現數據傳遞。

總結

經過這邊Flutter數據傳遞的介紹,咱們能夠大概搭建本身的Flutter App的數據流結構。 相似閒魚的界面的架構設計。

閒魚flutter的界面框架設計.png

  • 從上往下: 經過自定義不一樣ScopeInheritedWidget來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 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索