Flutter 状态管理框架对比

Flutter 本身就有非常强大的状态管理方式 setState,但是如果用于多组件的开发,通过 setState 实现子组件通信是非常麻烦的。

框架信息

状态管理框架 Pub like GitHub star 状态更新 共享状态 颗粒度 说明
setState - - Yes No - 适用于较小规模 widget 的暂时性状态的基础管理方法
StreamController - - Yes No 变量 基于流/观察者模式的基础管理方法
RxDart 712 2600 Yes No 变量 基于流/观察者模式的框架
MobX 224 1700 Yes No 变量 基于观察及响应的状态管理常用库
GetX 1723 1300 Yes Yes 变量 轻量级响应式状态管理解决方案
Provider 2488 3000 Yes? Yes 官方推荐的状态共享框架
flutter_bloc 1236 5700 Yes Yes 基于流/观察者模式的框架
flutter_redux 164 1300 Yes Yes 前端开发者较为熟悉的状态容器实现。
Fish-Redux 34 6800 Yes Yes 基于 Redux 状态管理的组合式 Flutter 应用框架

计数器示例

通过简单的计数器示例对比几个框架的实现,部分框架是不带有状态共享的,所以会配合Provider一起实现。计数器本身是一个非常简单的示例,为了凸显状态管理的意义,我会把“计数器值视图”和“点击按钮”拆分为2个组件,让结构变得复杂一些,就是一个页面加两个组件的方式。

setState

SDK中最简单的更新方式,不同组件之间的通信只能通过构造函数和回调实现。

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

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

class _CounterPageState extends State<CounterPage> {
  var counter = 0;

  @override
  Widget build(BuildContext ct) {
    return Scaffold(
      body: Center(
        child: CounterText(counter),
      ),
      floatingActionButton: Button(
        onPressed: () {
          setState(() {
            counter++;
          });
        },
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  final int count;

  CounterText(this.count);

  @override
  Widget build(BuildContext context) {
    return Text('$count');
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  final VoidCallback onPressed;

  Button({@required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: onPressed,
      child: new Icon(Icons.add),
    );
  }
}

StreamController+InheritedWidget

这个示例是通过StreamController和InheritedWidget结合从而实现状态更新和状态共享。

/// 用于实现Widget树共享状态状态
class MyProvider extends InheritedWidget {
  final dynamic value;

  MyProvider({this.value, Widget child}) : super(child: child);

  @override
  bool updateShouldNotify(covariant MyProvider oldWidget) {
    return oldWidget.value != this.value;
  }

  static T of<T>(BuildContext context) {
    MyProvider provider =
        context.dependOnInheritedWidgetOfExactType(aspect: MyProvider);
    return provider.value;
  }
}

class CounterBloc {
  StreamController<int> _counterController = StreamController();
  int counter = 0;

  Stream get counterStream => _counterController.stream;

  increment() {
    _counterController.add(++counter);
  }

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


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

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

class _CounterPageState extends State<CounterPage> {
  var _counterBloc = CounterBloc();

  @override
  void dispose() {
    _counterBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext ct) {
    return MyProvider(
      value: _counterBloc,
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var bloc = MyProvider.of<CounterBloc>(context);
    return StreamBuilder<int>(
      stream: bloc.counterStream,
      initialData: bloc.counter,
      builder: (context, snapshot) {
        return Text('${snapshot.data}');
      },
    );
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var bloc = MyProvider.of<CounterBloc>(context);
    return FloatingActionButton(
      onPressed: bloc.increment,
      child: new Icon(Icons.add),
    );
  }
}

RxDart+Provider

ReactiveX 团队开发的,是一个 dart 语言的 Rx 框架,使用极其轻量级,如果需要共享状态能力建议配合 Provider 一起使用。从下面的示例可以看到,RxDart的用法是接近于 StreamController,其实 BehaviorSubject 就是实现 StreamController,进行了高级的封装,加入了很多简便的用法。由于RxDart不具备状态共享能力,所以结合了Provider实现这个示例。

class CounterBloc {
  var counterSubject = BehaviorSubject.seeded(0);

  increment() {
    counterSubject.value++;
  }

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

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext ct) {
    return Provider<CounterBloc>(
      create: (context) => CounterBloc(),
      
      /// 需要手动调动dispose
      dispose: (context, bloc) => bloc.dispose(),
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var bloc = Provider.of<CounterBloc>(context);
    return StreamBuilder<int>(
      stream: bloc.counterSubject,
      initialData: bloc.counterSubject.value,
      builder: (context, snapshot) {
        return Text('${snapshot.data}');
      },
    );
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: (){
        Provider.of<CounterBloc>(context, listen: false).increment();
      },
      child: new Icon(Icons.add),
    );
  }
}

dependencies:

rxdart: ^0.24.1

MobX+Provider

flutter_mobx 和其它框架的实现都不太相同,它是基于注解实现,需要配合 mobx_codegen 使用的。需要添加 flutter_mobx、build_runner 和 mobx_codegen 依赖。

dependencies:
  flutter_mobx: ^1.1.0+2

dev_dependencies:
  build_runner:
  mobx_codegen: ^1.1.1+1

创建 counter.dart 文件。

/// counter.dart

import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class CounterStore = _CounterStore with _$CounterStore;

abstract class _CounterStore with Store {
  @observable
  int counter = 0;

  @action
  void increment() {
    counter++;
  }
}

执行命令,就会生成一个 counter.g.dart 文件

flutter packages pub run build_runner build

MobX 的用法很简洁,但是每次修改文件都需要执行 build_runner 命令,如果项目很大,执行时间很长,这个也是一个问题。由于MobX不具备状态共享能力,所以结合了Provider实现这个示例。

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => CounterStore(),
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var store = Provider.of<CounterStore>(context);
    return Observer(
      builder: (context) {
        return Text('${store.counter}');
      },
    );
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: (){
        Provider.of<CounterStore>(context, listen: false).increment();
      },
      child: new Icon(Icons.add),
    );
  }
}

#### GetX+Provider
这个框架非常简洁,而且设计的思路和MobX很相似,不同的是,GetX不需要使用 build_runner 技术即可实现相同的效果。不过GetX的功能远不止如此,它提供了入侵性很强的用法,有国际化、主题、路由等功能,功能涉及非常广泛。由于GetX不具备基于Widget树的状态共享能力,所以结合了Provider实现这个示例。
```dart
class CounterController extends GetxController {
  var counter = 0.obs;

  increment() => counter++;
}

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => CounterController(),
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var controller = Provider.of<CounterController>(context);
    return Obx(() => Text('${controller.counter}'));
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: (){
        Provider.of<CounterController>(context, listen: false).increment();
      },
      child: new Icon(Icons.add),
    );
  }
}
```
> dependencies:
>   get:

#### Provider
这个框架是 [Google I/O 2019](https://www.youtube.com/watch?v=d_m5csmrf7I) 上推荐的这个框架,并写入了 [Flutter 的官方开发文档](https://flutter.cn/docs/development/data-and-backend/state-mgmt/simple)。这个框架主要是用于不同组件之间实现状态共享,不过它也提供了一个ChangeNotifier 轻量级类,可以用于实现状态更新。
```dart
class CounterModel extends ChangeNotifier {
  int counter = 0;

  increment() {
    counter++;
    // 通知视图刷新
    notifyListeners();
  }
}

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<CounterModel>(builder: (context, counterModel, child) {
      return Text('${counterModel.counter}');
    });
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        Provider.of<CounterModel>(context, listen: false).increment();
      },
      child: new Icon(Icons.add),
    );
  }
}
```


> dependencies:
>   provider: ^4.3.2+2

bloc

bloc 的用法和 Provider 很相似,也提供不同组件之间实现状态共享,也提供了状态更新功能。

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterCubit(),
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CounterCubit, int>(builder: (context, count) {
      return Text('$count');
    });
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        context.bloc<CounterCubit>().increment();
      },
      child: new Icon(Icons.add),
    );
  }
}

Redux

这个相比以上的框架,是复杂一些的,其思想源于前端开发。

enum Actions { Increment }

int counterReducer(int state, dynamic action) {
  if (action == Actions.Increment) {
    return state + 1;
  }

  return state;
}

class CounterPage extends StatelessWidget {
  CounterPage({Key key}) : super(key: key);

  final store = new Store<int>(counterReducer, initialState: 0);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<int>(
      store: store,
      child: Scaffold(
        body: Center(
          child: CounterText(),
        ),
        floatingActionButton: Button(),
      ),
    );
  }
}

/// 计数器值视图
class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<int, String>(
      converter: (store) => store.state.toString(),
      builder: (context, count) {
        return Text(count);
      },
    );
  }
}

/// 点击按钮
class Button extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new StoreConnector<int, VoidCallback>(
      converter: (store) {
        return () => store.dispatch(Actions.Increment);
      },
      builder: (context, callback) {
        return new FloatingActionButton(
          onPressed: callback,
          child: new Icon(Icons.add),
        );
      },
    );
  }
}

fish_redux

闲鱼开源基于Redux思想的框架,但是比官方Redux要复杂很多。这个项目虽然 star 很多,但是半年没有更新了,还处于 0.3.4 版本,被提了130多个 Issue,也没有人去处理,估计闲鱼团队自己都已经放弃这个框架了吧,建议不要使用这个框架。

总结

状态管理的框架很多,Redux的思想和其它的都不太一样,由于我对Redux的思想没有足够深入的了解,这里就不做更多的评价。Flutter还是推荐BLoC思想的,基于颗粒度主要是分为2类,“变量”和“类”。如果需要涉及很多状态的改变,如果颗粒度为“变量”,每一个状态只需要创建一个变量即可,但是也有一个缺点,需要搭配Provider一起使用,自动管理能力较差。对于颗粒度为“类”的框架,要么就是牺牲更新的颗粒度,定义一个包含很多状态的State,缺点就是无法做到小颗粒更新,一个小小的改变,所有依赖这个State的地方都需要刷新。要么就只能为每一个状态创建一个class,有多少状态就有多少个class,缺点就是整体代码量大大增加。我个人的偏向是使用颗粒度为变量的RxDart框架,搭配Provider,使用更加灵活。 虽然 MobX 更加优雅,但是 MobX 是基于注解,业务层本身就经常需要修改,用起来成本较高。GetX 确实是最优雅的,值得去研究。

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

推荐阅读更多精彩内容