Flutter入门三部曲(3) - 数据传递/状态管理

Flutter数据传递
分为两种方式。一种是沿着数的方向从上向下传递状态。另一种是 从下往上传递状态值。

沿着树的方向,从上向下传递数据、状态

按照Widgets Tree的方向,从上往子树和节点上传递状态。

InheritedWidget & ValueNotifier

InheritedWidget

这个既熟悉又陌生类可以帮助我们在Flutter中沿着树向下传递信息。这个类只是简单的保存了一个状态而已。
我们经常通过这样的方式,通过BuildContext,可以拿到ThemeMediaQuery

//得到状态栏的高度
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

    image.png

  • 通过build方法中返回

  1. MaterialApp_MaterialAppState中的build方法

    image.png

  2. WidgetsApp_WidgetsAppState中的build方法

    image.png

  • 获取
    最后就是最上面看到的那段代码,通过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-当前页面

点击按钮更改状态。


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');
    //初始化监听的回调?;氐饔米鞯木褪茄映?s后,将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:真的强烈推荐)

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容