19

Flutter State Management状态管理全面分析 - 简书

 4 years ago
source link: https://www.jianshu.com/p/9334b8f68004?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Flutter State Management状态管理全面分析

Flutter State Management状态管理全面分析

0.7422020.05.21 14:55:22字数 6,636阅读 1,400

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


webp

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

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

状态管理是什么


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

  • 程序=算法+数据结构
    数据是程序的中心。数据结构和算法两个概念间的逻辑关系贯穿了整个程序世界,首先二者表现为不可分割的关系。其实Flutter不就是一个程序吗,那我们面临的最底层的问题还是算法和数据结构,所以我们推导出
  • Flutter=算法+数据结构
    那状态管理是什么?我也用公式来表达一下,如下:
  • Flutter状态管理=算法+数据结构+UI绑定
    瞬间秒懂有没有?来看一个代码例子:
class ThemeBloc {
  final _themeStreamController = StreamController<AppTheme>();

  get changeTheTheme => _themeStreamController.sink.add;

  get darkThemeIsEnabled => _themeStreamController.stream;

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

final bloc = ThemeBloc();

class AppTheme {
  ThemeData themeData;

  AppTheme(this.themeData);
}
/// 绑定到UI
StreamBuilder<AppTheme>(
        initialData: AppTheme.LIGHT_THEME,
        stream: bloc.darkThemeIsEnabled,
        builder: (context, AsyncSnapshot<AppTheme> snapshot) {
          return MaterialApp(
            title: 'Jetpack',
            theme: snapshot.data.themeData,
            home: PageHome(),
            routes: <String, WidgetBuilder>{
              "/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<WidgetTest> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          WidgetA(),
          WidgetB()
        ],
      ),
    );
  }
}

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

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

class _WidgetBState extends State<WidgetB> {
  @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<MyHomepage> {
  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<TapboxA> {
  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<ParentWidget> {
  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<bool> 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<InheritedWidgetDemo>();
  }

}

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<MyInheritedWidget>{
  /// List of Items
  List<Item> _items = <Item>[];

  /// 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<MyTree> {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: <Widget>[
            new WidgetA(),
            new Container(
              child: new Row(
                children: <Widget>[
                  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<_MyInheritedWidget>(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

https://juju.one/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<WidgetListener> {
  int _test = 1;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: <Widget>[
          NotificationListener<TestNotification>(
            child: Column(
              children: <Widget>[
                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全面分析

响应式编程:从 Streams 到 BLoC

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的状态管理有了一个重新的认识呢?如果对你有帮住,请点一下下面的赞哦。谢谢🙏。

禁止转载,如需转载请通过简信或评论联系作者。
"小礼物走一走,来简书关注我"
还没有人赞赏,支持一下
i校长抽象化是一种非常的不同于模糊化的东西 … 抽象的目的并不是为了模糊,而是为了创造出一种能让我们...
总资产316 (约14.28元)共写了10.1W字获得717个赞共821个粉丝

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK