Flutter State Management状态管理全面分析

前言

2019 Google I/O 大会,google就推出 Provider ,成为官方推荐的状态管理方式之一,Flutter 状态管理一直是个很热门的话题,而且状态管理的库也是超级多,这确实是我们每一个做Flutter开发难以避免的一道坎,既然这么重要,我们如何去理解它,如何使用它,如何做到更好呢?接下来让我告诉你答案 

主要内容

一张图告诉你,我要讲的主要内容。下面将围绕这八个方面来讲。七个理论,一个实践。

  • 状态管理是什么
  • 为什么需要状态管理
  • 状态管理基本分类
  • 状态管理的底层逻辑
  • 状态管理的使用原则
  • 使用成熟状态管理库的弊端
  • 选择状态管理库的原则
  • Provider 深入分析(学以致用)

状态管理是什么

我们知道最基本的程序是什么:

  • 程序=算法+数据结构

数据是程序的中心。数据结构和算法两个概念间的逻辑关系贯穿了整个程序世界,首先二者表现为不可分割的关系。其实Flutter不就是一个程序吗,那我们面临的最底层的问题还是算法和数据结构,所以我们推导出

  • Flutter=算法+数据结构

那状态管理是什么?我也用公式来表达一下,如下:

  • Flutter状态管理=算法+数据结构+UI绑定

瞬间秒懂有没有?来看一个代码例子:

class ThemeBloc {
  final _themeStreamController = StreamController();

  get changeTheTheme => _themeStreamController.sink.add;

  get darkThemeIsEnabled => _themeStreamController.stream;

  dispose() {
    _themeStreamController.close();
  }
}

final bloc = ThemeBloc();

class AppTheme {
  ThemeData themeData;

  AppTheme(this.themeData);
}
/// 绑定到UI
StreamBuilder(
        initialData: AppTheme.LIGHT_THEME,
        stream: bloc.darkThemeIsEnabled,
        builder: (context, AsyncSnapshot snapshot) {
          return MaterialApp(
            title: 'Jetpack',
            theme: snapshot.data.themeData,
            home: PageHome(),
            routes: {
              "/pageChatGroup": (context) => PageChatGroup(),
              "/LaoMeng": (context) => LaoMeng(),
            },
          );
        })
  • AppTheme 是数据结构
  • changeTheTheme 是算法
  • StreamBuilder 是绑定UI

这样一整套代码的逻辑就是我们所说的Flutter状态管理,这样解释大家理解了吗?再细说,算法就是我们如何管理,数据结构就是数据状态,状态管理的本质还是如何通过合理的算法管理数据,如何取,如何接收等,最终展示在UI上,通过UI的变更来体现状态的管理逻辑。

为什么需要

这里就需要明白一个事情,Flutter的很多优秀的设计都来源于React,对于react来说,同级组件之间的通信尤为麻烦,或者是非常麻烦了,所以我们把所有需要多个组件使用的state拿出来,整合到顶部容器,进行分发。状态管理可以实现组件通信、跨组件数据储存。推荐阅读 对 React 状态管理的理解及方案对比 ,那么对于Flutter来说呢?你知道Android、Ios等原生于Flutter最本质的区别吗?来看一段代码: 

//android
TextView tv = TextView()
tv.setText("text")

///flutter
setState{
    text = "text"
}

从上面代码我们看出,Android的状态变更是通过具体的组件直接赋值,如果页面全部变更,你是不是需要每一个都设置一遍呢?,而Flutter的变更就简单粗暴,setState搞定,它背后的逻辑是重新build整个页面,发现有变更,再将新的数据赋值,其实Android、Ios与flutter的本质的区别就是数据与视图完全分离,当然Android也出现了UI绑定框架,似乎跟React、Flutter越来越像,所以这也在另一方面凸显出了,Flutter设计的先进性,没有什么创新,但更符合未来感,回过头来,仔细想一想,这样设计有什么弊端?

对了你猜对了: 页面如何刷新才是Flutter的关键 ,做Android的同学肯定也面临着一个问题,页面的重绘导致的丢帧问题,为了更好,我们很多时候都选择了局部刷新来优化对吧,Android、Ios已经很明确的告诉UI要刷新什么更新什么,而对于Flutter来说,这一点很不清晰,虽然Flutter也做了类似虚拟Dom优化重绘逻辑,但这些远远不够的,如何合理的更新UI才是最主要的,这个时候一大堆的状态管理就出来了,当然状态管理也不是仅仅为了解决更新问题。 

我再抛出一个问题,如果我有一个widget A,我想在另外一个widget B中改变widget A的一个状态,或者从网络、数据库取到数据,然后刷新它,怎么做?我们来模拟一下,来看代码

糟糕的状态管理代码

class WidgetTest extends StatefulWidget {
  @override
  _WidgetTestState createState() => _WidgetTestState();
}

class _WidgetTestState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          WidgetA(),
          WidgetB()
        ],
      ),
    );
  }
}

_WidgetAState _widgetAState;
class WidgetA extends StatefulWidget {
  @override
  _WidgetAState createState() {
    _widgetAState = _WidgetAState();
    return _widgetAState;
  }
}
class _WidgetAState extends State {
  var title = "";
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(title),
    );
  }
}

class WidgetB extends StatefulWidget {
  @override
  _WidgetBState createState() => _WidgetBState();
}

class _WidgetBState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        onPressed: () {
          _widgetAState.setState(() {
            _widgetAState.title = "WidgetB";
          });
        },
      ),
    );
  }
}

WidgetTest页面有两个widget,分别是WidgetA、WidgetB,WidgetB通过RaisedButton的onPressed来改变WidgetA的Text,怎么做到的呢,直接用WidgetA的_WidgetAState对象提供的setState函数来变更,没什么问题对吧,而且功能实现了,但你仔细思考一下,这有什么问题呢?

  • _WidgetAState 被全局化,而且它所有状态被暴漏出去,如果_WidgetAState有十个状态,只有一个想让别人变更,可惜已经晚了, 你加’_’也不行,组件的隐私全没了
  • 耦合变高,WidgetB有_WidgetAState的强关联,我们编码追求的解偶,在这里完全被忽视了
  • 性能变差,为什么这么说?因为每次_widgetAState.setState都会导致整个页面甚至子Widget的重新build,如果_widgetAState里面有成千上百的状态,性能肯定差到极点
  • 不可测,程序变得难以测试

如何变好呢 

这就需要选择一种合适的状态管理方式。

状态管理的目标 

其实我们做状态管理,不仅仅是因为它的特点,而为了更好架构,不是吗?

  • 代码要层次分明,易维护,易阅读
  • 可扩展,易维护,可以动态替换UI而不影响算法逻辑
  • 安全可靠,保持数据的稳定伸缩
  • 性能佳,局部优化

这些不紧紧是状态管理的目的,也是我们做一款优秀应用的基础架构哦。

基本分类

  • 局部管理 官方也称 Ephemeral state,意思是短暂的状态,这种状态根本不需要做全局处理

举个例子,如下方的_index,这就是一个局部或者短暂状态,只需要StatefulWidget处理即可完成

class MyHomepage extends StatefulWidget {
  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}
  • 全局管理 官方称 App state,即应用状态,非短暂状态,您要在应用程序的许多部分之间共享,以及希望在用户会话之间保持的状态,就是我们所说的应用程序状态(有时也称为共享状态)

例如:

  • 用户偏好
  • 登录信息
  • 购物车
  • 新闻阅读状态

状态分类官方定义

没有明确的通用规则来区分特定变量是短暂状态还是应用程序状态。有时,您必须将一个重构为另一个。例如,您将从一个明显的短暂状态开始,但是随着您的应用程序功能的增长,可能需要将其移至应用程序状态。 出于这个原因,请使用下图进行分类:

总之,任何Flutter应用程序中都有两种概念性的状态类型。临时状态可以使用State和setState()来实现,并且通常是单个窗口小部件的本地状态。剩下的就是您的应用状态。两种类型在任何Flutter应用程序中都有自己的位置,两者之间的划分取决于您自己的喜好和应用程序的复杂性

没有最好的管理方式,只有最合适的管理方式

底层逻辑

底层逻辑我想告诉你的是,Flutter中目前有哪些可以做到状态管理,有什么缺点,适合做什么不适合做什么,只有你完全明白底层逻辑,才不会畏惧复杂的逻辑,即使是复杂的逻辑,你也能选择合理的方式去管理状态。

  • State

StatefulWidget、StreamBuilder状态管理方式

  • InheritedWidget

专门负责Widget树中数据共享的功能型Widget,如Provider、scoped_model就是基于它开发

  • Notification

与InheritedWidget正好相反,InheritedWidget是从上往下传递数据,Notification是从下往上,但两者都在自己的Widget树中传递,无法跨越树传递。

  • Stream

数据流 如Bloc、flutter_redux、fish_redux等也都基于它来做实现

为什么列这些东西?因为现在大部分流行的状态管理都离不开它们。理解它们比理解那些吹自己牛逼的框架要好的多。请关注底层逻辑,这样你才能游刃有余。下面我们一个个分析一下:

State

State 是我们常用而且使用最频繁的一个状态管理类,它必须结合StatefulWidget一起使用,StreamBuilder继承自StatefulWidget,同样是通过setState来管理状态

举个例子来看下:

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

引用官方的例子,这里_active状态就是通过State提供的setState函数来实现的

为什么会让State去管理状态,而不是Widget本身呢?Flutter设计时让Widget本身是不变的,类似固定的配置信息,那么就需要一个角色来控制它,State就出现了,但State的任何更改都会强制整个Widget重新构建,当然你也可以覆盖必要方法自己控制逻辑。

再看个例子:

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

从这里你看出什么?对了,父组件可以通过setState来刷新子Widget的状态变化,所以得出如下观点

注意 

setState是整个Widget重新构建(而且子Widget也会跟着销毁重建),这个点也是为什么不推荐你大量使用StatefulWidget的原因。如果页面足够复杂,就会导致严重的性能损耗。如何优化呢?建议使用StreamBuilder,它原理上也是State,但它做到了子Widget的局部刷新,不会导致整个页面的重建,是不是就好很多了呢?

State缺点

从上面的代码我们分析一下它的缺点

  • 无法做到跨组件共享数据(这个跨是无关联的,如果是直接的父子关系,我们不认为是跨组件)

setState是State的函数,一般我们会将State的子类设置为私有,所以无法做到让别的组件调用State的setState函数来刷新

  • setState会成为维护的难点,因为啥哪哪都是。

随着页面状态的增多,你可能在调用setState的地方会越来越多,不能统一管理

  • 处理数据逻辑和视图混合在一起,违反代码设计原则

比如数据库的数据取出来setState到Ui上,这样编写代码,导致状态和UI耦合在一起,不利于测试,不利于复用。

State小结

当然反过来讲,不是因为它有缺点我们就不使用了,我们追求的简单高效,简单实现,高效运行,当复杂到需要更好的管理的时候再重构。一个基本原则就是,状态是否需要跨组件使用,如果需要那就用别的办法管理状态而不是State管理。

InheritedWidget

InheritedWidget是一个无私的Widget,它可以把自己的状态数据,无私的交给所有的子Widget,所有的子Widget可以无条件的继承它的状态。就这么一个东西。有了State我们为什么还需要它呢?我们已经知道,State是可以更新直接子Widget的状态,但如果是子Widget的子Widget呢,所以说InheritedWidget的存在,一是为了更简单的获取状态,二是大家都共享这个状态,举个例子

class InheritedWidgetDemo extends InheritedWidget {

  final int accountId;

  InheritedWidgetDemo(this.accountId, {Key key, Widget child})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidgetDemo old) =>
      accountId != old.accountId;

  static InheritedWidgetDemo of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

}

class MyPage extends StatelessWidget {
  final int accountId;

  MyPage(this.accountId);

  Widget build(BuildContext context) {
    return new InheritedWidgetDemo(
      accountId,
      child: const MyWidget(),
    );
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget();

  Widget build(BuildContext context) {
    return MyOtherWidget();
  }
}

class MyOtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myInheritedWidget = InheritedWidgetDemo.of(context);
    print(myInheritedWidget.accountId);
  }

}
  • InheritedWidgetDemo共享状态accountId给了MyOtherWidget,而MyOtherWidget是MyWidget的子Widget,这就是InheritedWidget的功效,它可以做到跨组件共享状态。
  • const MyWidget() 表示该Widget是常量,不会因为页面的刷新导致重新build,这就是优化的细节,这里想一下,如果你用State实现,不是就需要它setState才能实现MyOtherWidget的重新build,这样做的坏处就是导致整个UI的刷新。
  • updateShouldNotify 它也是一个优化点,在你横屏变竖屏的同时,导致整个UI重新build,可由于updateShouldNotify的判断,系统将不会重新build MyOtherWidget,也是一种布局优化。
  • 子树中的组件通过InheritedWidgetDemo.of(context)访问共享状态。

有的人想了,InheritedWidget这么好用,那我把整个App的状态都存进来怎么样?类似这样

class AppContext {
  int teamId;
  String teamName;
  
  int studentId;
  String studentName;
  
  int classId;
  ...
}

其实这样不好,我们不光是要做技术上的组件化,更要关注的是业务,对业务的充分理解并实现模块化分工,在使用InheritedWidget时候特别是要注意这一点,更推荐你使用该方案:

class TeamContext {
  int teamId;
  String teamName;
}

class StudentContext {
  int studentId;
  String studentName;
}
 
class ClassContext {
  int classId;
  ...
}

注意 

它的数据是只读的,虽然很无私,但子widget不能修改,那么如何修改呢?

举个例子:

class Item {
   String reference;

   Item(this.reference);
}

class _MyInherited extends InheritedWidget {
  _MyInherited({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final MyInheritedWidgetState data;

  @override
  bool updateShouldNotify(_MyInherited oldWidget) {
    return true;
  }
}

class MyInheritedWidget extends StatefulWidget {
  MyInheritedWidget({
    Key key,
    this.child,
  }): super(key: key);

  final Widget child;

  @override
  MyInheritedWidgetState createState() => new MyInheritedWidgetState();

  static MyInheritedWidgetState of(BuildContext context){
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedWidgetState extends State{
  /// List of Items
  List _items = [];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  /// Helper method to add an Item
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: [
            new WidgetA(),
            new Container(
              child: new Row(
                children: [
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

该例子引用自 widget-state-context-inheritedwidget/ 欢迎阅读学习哦 

  • _MyInherited是InheritedWidget,每次我们通过单击“ Widget A”的按钮添加元素时都会重新创建
  • MyInheritedWidget是一个状态为包含元素列表的窗口小部件。可通过(BuildContext上下文)的静态MyInheritedWidgetState访问此状态
  • MyInheritedWidgetState公开一个getter(itemsCount)和一个方法(addItem),以便子控件树的一部分的控件可以使用它们
  • 每次我们向State添加元素时,都会重新构建MyInheritedWidgetState
  • MyTree类仅构建一个小部件树,将MyInheritedWidget作为该树的父级
  • WidgetA是一个简单的RaisedButton,按下该按钮时,会调用最近的MyInheritedWidget的addItem方法。
  • WidgetB是一个简单的Text,它显示在最接近的 MyInheritedWidget级别上显示的元素数量

看了一下日志输出如图:

有没有发现一个问题?当MyInheritedWidgetState.addItem,导致setState被调用,然后就触发了WidgetA、WidgetB的build的方法,而WidgetA根本不需要重新build,这不是浪费吗?那么我们如何优化呢?

static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
                    : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

通过抽象rebuild属性来控制是否需要重新build

final MyInheritedWidgetState state = MyInheritedWidget.of(context,false);

然后用的时候加以参数控制,改完代码,再看下日志:

看,已经生效了。你现在是不是对InheritedWidget有了更清晰的认识了呢?但说到它就不得不提InheritedModel,它是InheritedWidget的子类,InheritedModel可以做到部分数据改变的时候才会重建,你可以修改上面例子

class _MyInheritedWidget extends InheritedModel {

  static MyInheritedWidgetState of(BuildContext context, String aspect) {
     return InheritedModel.inheritFrom(context, aspect: aspect).data;
   }

   @override
   bool updateShouldNotifyDependent(_MyInheritedWidget old, Set aspects) {
     return aspects.contains('true');
   }
 }

调用修改为:

///不允许重新build
 final MyInheritedWidgetState state = MyInheritedWidget.of(context,"false");
///允许重新build
final MyInheritedWidgetState state = MyInheritedWidget.of(context,"true");

推荐阅读

inheritedmodel-vs-inheritedwidget

Flutter中的数据传递:InheritedWidget, InheritedModel

widget-state-context-inheritedwidget/

InheritedWidget 缺点

通过上面的分析,我们来看下它的缺点

  • 容易造成不必要的刷新
  • 不支持跨页面(route)的状态,意思是跨树,如果不在一个树中,我们无法获取
  • 数据是不可变的,必须结合StatefulWidget、ChangeNotifier或者Steam使用

InheritedWidget 小结

经过一系列的举例和验证,你也基本的掌握了InheritedWidget了吧,这个组件特别适合在同一树型Widget中,抽象出公有状态,每一个子Widget或者孙Widget都可以获取该状态,我们还可以通过手段控制rebuild的粒度来优化重绘逻辑,但它更适合从上往下传递,如果是从下往上传递,我们如何做到呢?请往下看,马上给你解答

Notification

它是Flutter中跨层数据共享的一种机制,注意,它不是widget,它提供了dispatch方法,来让我们沿着context对应的Element节点向上逐层发送通知

具个简单例子看下

class TestNotification extends Notification {
  final int test;

  TestNotification(this.test);
}

var a = 0;

// ignore: must_be_immutable
class WidgetNotification extends StatelessWidget {

  final String btnText;

  WidgetNotification({Key key, this.btnText}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        child: Text(btnText),
        onPressed: () {
          var b = ++a;
          debugPrint(b.toString());
          TestNotification(b).dispatch(context);
        },
      ),
    );
  }
}

class WidgetListener extends StatefulWidget {
  @override
  _WidgetListenerState createState() => _WidgetListenerState();
}

class _WidgetListenerState extends State {
  int _test = 1;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          NotificationListener(
            child: Column(
              children: [
                Text("监听$_test"),
                WidgetNotification(btnText: "子Widget",)
              ],
            ),
            onNotification: (TestNotification notification) {
              setState(() {
                _test = notification.test;
              });
              return true;
            },
          ),
          WidgetNotification(btnText: "非子Widget",)
        ],
      ),
    );
  }
}
  • 定义TestNotification通知的实现
  • WidgetNotification 负责通知结果,通过RaisedButton的点击事件,将数据a传递出去,通过Notification提供的dispatch方法向上传递
  • WidgetListener通过Widget NotificationListener来监听数据变化,最终通过setState变更数据
  • WidgetNotification 实例化了两次,一次在NotificationListener的树内部,一个在NotificationListener的外部,经过测试发现,在外部的WidgetNotification并不能通知到内容变化。

所以说在使用Notification的时候要注意,如果遇到无法收到通知的情况,考虑是否是Notification 未在NotificationListener的内部发出通知,这个一定要注意。

同样的思路,我想看下Notification是如何刷新Ui的

在代码里加入了跟通知无关紧要的WidgetC

这么看来,你以为是Notification导致的吗?我把这个注释掉,如图

再运行看下,连续点击了八次

原来是State的原因,那么这种情况我们如何优化呢?这就用到了Stream了,请接着往下继续看哦。

推荐阅读

flutter-notifications-bubble-up-and-values-go-down

notification

Notification缺点

  • 不支持跨页面(route)的状态,准备的说不支持NotificationListener同级或者父级Widget的状态通知
  • 本身不支持刷新UI,需要结合State使用
  • 如果结合State,会导致整个UI的重绘,效率底下不科学

Notification小结

使用起来很简单,但在刷新UI方面需要注意,如果页面复杂度很高,导致无关紧要的组件跟着刷新,得不偿失,还需要另找蹊径,躲开这些坑,下面我来介绍如何完美躲闪,重磅来袭Stream。

Stream

Stream其实就是一个生产者消费者模型,一端负责生产,一端负责消费,而且是纯Dart的实现,跟Flutter没什么关系,扯上关系的就是用StreamBuilder来构建一个Stream通道的Widget,像知名的rxdart、BloC、flutter_redux全都用到了Stream的api。所以学习它才是我们掌握状态管理的一个关键

我们先来看下如何改造上面Notification的例子达到我们想要的刷新效果

///step1
class TestBloc {

  final _testStreamController = StreamController<int>();

  get changeTest => _testStreamController.sink.add;

  get testStream => _testStreamController.stream;

  dispose() {
    _testStreamController.close();
  }
}

final testBloc = TestBloc();

/// step2
StreamBuilder<int>(
                  initialData: 0,
                  stream: testBloc.testStream,
                  builder: (context, snapshot) {
                    return Text("监听${snapshot.data}");
                  }
                )
/// step3
onNotification: (TestNotification notification) {
              testBloc.changeTest(notification.test);
              return true;
            }
  • 第一步定义TestBloc,负责管理Stream,提供changeTest函数来往Stream通道添加元素
  • 将TestBloc的流testStream赋值给StreamBuilder的stream属性加以绑定
  • 在收到通知的地方用 testBloc.changeTest来往stream中添加元素,最终由StreamBuilder的setState更新,对的StreamBuilder是通过State刷新的,想深入的可以看下面推荐阅读内容,讲的很好

看下运行效果

看见了吧,无关紧要的Widget已经不跟着刷新了,这就是Stream的重要性

推荐阅读 

我自己写的StreamBuilder源码分析

大神写的Stream全面分析

Stream 缺点

  • api生涩,不好理解
  • 需要定制化,才能满足更复杂的场景
  • 没有自动dispose逻辑 我们做开发都有个习惯,当这个流不被使用的时候,喜欢close掉,可惜Stream并没有提供这样的api,需要自己扩展实现,大部分人都是使用StatefulWidget的dispose函数来辅助流的close调用,那我们不想使用StatefulWidget怎么办,感觉它写法太麻烦

我们如何做到让Stream自动Close掉呢?

  • 一种办法是自己实现扩展StreamBuilder,在它dispose的时候调用,因为StreamBuilder是Statefulwidget的子类可以覆盖dispose函数
  • 第二是参考Provider的实现,经过源码分析Provider的原理是依赖于InheritedElement的unmount函数实现的,最终回调函数dispose,unmount函数类似于Android Activity的Destroy函数,页面彻底销毁了,不需要任何数据资源了,都释放了得了,InheritedElement是InheritedWidget的虚拟Dom对象,Flutter 页面绘制三板斧嘛(widget、element、renderObject)

第二种不太现实,实现起来逻辑复杂,但我想告诉有这么一个思路,你可以参考。

缺点恰恰是它的优点,保证了足够灵活,你更可基于它做一个好的设计,满足当下业务的设计。

小结

你有没有发现一个问题,似乎InheritedWidget、Notification、Stream的状态更新都没有离开State的支持?所以说Flutter在抽象功能的真的是做到了极致,InheritedWidget、Notification、Stream只是适合在不同的场景使用,而真正的状态变更还是需要State负责。通过对State、InheritedWidget、Notification、Stream的学习,你是不是觉得,Flutter的状态管理也就这些了呢?不一定哈,我也在不断的学习,如果碰到新的技术,继续分享给你们哦。难道这就完了吗?没有,其实我们只是学了第一步,是什么,如何用,还没有讨论怎么用好呢?需要什么标准吗,当然有,下面通过我的项目实战经验来提出一个基本原则,超过这个原则你就是在破坏平衡,请往下看。

状态管理的使用原则

局部管理优于全局

这个原则来源于,Flutter的性能优化,局部刷新肯定比全局刷新要好很多,那么我们在管理状态的同时,也要考虑该状态到底是局部还是全局,从而编写正确的逻辑。

保持数据安全性

用“_”私有化状态,因为当开发人员众多,当别人看到你的变量的时候,第一反应可能不是找你提供的方法,而是直接对变量操作,那就有可能出现想不到的后果,如果他只能调用你提供的方法,那他就要遵循你方法的逻辑,避免数据被处理错误。

考虑页面重新build带来的影响

很多时候页面的重建都会调用build函数,也就是说,在一个生命周期内,build函数是多次被调用的,所以你就要考虑数据的初始化或者刷新怎么样才能合理。

使用成熟状态管理库弊端

  • 增加代码复杂性
  • 框架bug修复需要时间等待
  • 不理解框架原理导致使用方式不对,反而带来更多问题
  • 选型错误导致不符合应用要求
  • 与团队风格冲突不适用

通过了解它们的弊端来规避一些风险,综合考虑,选框架不易,且行且珍惜。

选型原则

  • 侵入性
  • 扩展性
  • 高性能
  • 安全性
  • 驾驭性
  • 易用性
  • 范围性

所有的框架都有侵入性,你同意吗?不同意请左转,前面有个坑,你可以跳过去。目前侵入性比较高的代表ScopedModel,为啥?因为它是用extend实现的,需要继承实现的基本不是什么好实现,你同意吗?同上。

扩展性就不用说了,如果你选择的框架只能使用它提供的几个入口,那么请你放弃使用它。高性能也是很重要的,这个需要明白它的原理,看它到底如何做的管理。安全性也很重要,看他数据管理通道是否安全稳定。驾驭性,你说你都不理解你就敢用,出了问题找谁?如果驾驭不了也不要用。易用性大家应该都明白,如果用它一个框架需要N多配置,N多实现,放弃吧,不合适。简单才是硬道理。

范围性

这个特点是flutter中比较明显的,框架选型一定要考虑框架的适用范围,到底是适合做局部管理,还是适合全局管理,要做一个实际的考量。 

推荐用法

如果是初期,建议多使用Stream、State、Notification来自行处理,顺便学习源码,多理解,多实践。有架构能力的就可以着手封装了,提供更简单的使用方式

如果是后期,当然也是在前面的基础之上,再去考虑使用Provider、redux等复杂的框架,原则上要吃透源码,否则不建议使用。

注意

你以为使用框架就能万事大吉了?性能优化是一个不变的话题,包括Provider在内的,如果你使用不当,照样出现页面的性能损耗严重,所以你又回到了为啥会这样,让我们一起彻彻底底的搞明白Provider的原理,你就知道了

Provider 深入源码分析

Flutter Provider 迄今为止最深、最全、最新的源码分析


Flutter状态管理框架使用比较

在项目中使用了不同的框架完成同一功能Demo的状态管理,用来比较不同框架在状态管理中功能的异同点。
功能包含了全局账户信息,登录页表单,修改用户信息页数据反显。

项目源码:https://github.com/FantasyWind2016/state_manage_demos/

框架介绍
因为目前项目使用的Flutter版本是1.12.13,所以选择了合适的框架版本,而不是最新版本。

普通模式
全局状态使用EventBus同步状态;页面内手动setState。
未进行页面分层。

GetX2.0.7


GetX的低版本不太稳定,存在一些比较明显的问题。
本身不支持响应式编程。
本身包含了路由管理,和基本的弹框,所以可以完全抛弃StatefulWidget。
该版本的GetX是自己管理所有的state对象,没有借助InheritedWidget,所以逻辑比较清晰,出现问题也好排查。

MobX1.2.0


分为mobx和flutter_mobx两个库,mobx主要是提供响应式编程能力和基本的状态管理。flutter_mobx提供flutter中应用的工具。
通过生成代码,拦截state的每一个getter和setter,达到了精确刷新的目标。
使用上的优点在于不用在更细数据时写notify代码。
因为mobx中,state和widget的绑定关系是通过拦截getter和setter方法做到的,所以在页面使用时,Observer不需要声明state的类型。
虽然自带了响应式编程的能力,但使用起来总感觉怪怪的,没有RxDart方便。
需要脚手架来自动生成和更新模板代码。不过因为MobX的逻辑并未依赖系统框架,所以Flutter升级版本导致不兼容的问题应该不多。

BLoC4.0.0


BLoC是类似redux的框架,从架构图就能明显看出来,不过本身也做了一些精简,增加了一些易用性的调整。
所有的状态对象都是通过InheritedWidget的方式共享的,所以对context是强依赖。这一点如果是之前用了GetX或者MobX的人会很难受。
类似Redux的状态流转过程,总是让人觉得要写好多模板代码。

Provider4.0.0


功能上相较其他框架比较简单,除了刷新颗粒度因为运行机制问题无法完全解决;还是在最大范围内提供了优化的方案。
其他的缺点可以结合别的框架一起使用,或者自己手动造一些轮子。

Flutter_Redux0.7.0


Redux基本就是Redux官方版的BLoC,除了命名上的差别,其他的也就是工具类的丰富程度了。
另外一个主要的点是Redux的异步操作需要借助redux_thunk,而BLoC自己解决了。

RxDart0.25.0


实际上就是个响应式编程框架,拿来做状态管理需要配合StreamBuilder,抛弃EventBus。

功能比较

总结

在使用体验上面,BLoC/Provider/Redux这三个比较类似,BLoC发展相对成熟,而Provider现在是官方框架,Redux相对处于弱势。但Provider只是提供了状态管理,对代码没有限制;而BLoC、Redux的代码模板或许更适合多人协作式的开发模式。


MobX的思路独树一帜,效果也是最好的;但是感觉比较小众,如果是个人项目,没有什么维护压力,使用起来会比较舒服。但是这个发展现状比较尴尬,因为重要的项目会担心维护问题不敢用,小的项目又不会对页面性能有这么高的要求。


GetX的目标不仅仅是为了解决状态管理问题,它想要的是在整体上给Flutter开发提效,所以提供了很多工具方法。个人使用会很高效。团队协作的话,可能会和团队自己的轮子功能重复,这就要具体问题具体分析了。


RxDart说到底只是响应式编程工具框架,想要拿来做状态管理或者代码框架,就需要手撸一些辅助工具。

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/123239108
今日推荐