Flutter | 状态管理探索篇——Redux(二)

前言

Flutter的很多灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。所以在Flutter中,它的Widget是immutable的,而它的动态部分全部放到了状态(State)中。于是状态管理自然便成了我们密切关注的对象。

在之前我们已经讨论了关于在flutter中使用scoped_model进行状态管理的应用。文章发出后,有许多同学都在问我,到底redux和scoped到底谁更好。

这个系列将会从这几个状态管理方案进行深入研究:

  • Scoped_model
  • redux
  • BLoC
  • 对比总结篇

所以今天要和大家介绍的是在flutter中使用Redux进行状态管理。
我希望各位在阅读这篇文章之前,先仔细思考以下这几个问题。

  • 什么是redux
  • redux给我们了什么好处,我们为什么要使用它
  • 它的基本思想是什么
  • redux是否真的适合我们

ok,我们开始正式的介绍redux。

Redux

为什么需要状态管理

在我们一开始构建应用的时候,也许很简单。我们有一些状态,直接把他们映射成视图就可以了。这种简单应用可能并不需要状态管理。

image

但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。

image

Wow,这是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。

这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

redux是什么

Redux是一种单向数据流架构,可以轻松开发,维护和测试应用程序。

image
  • 我们在Redux中,所有的状态都储存在Store里。这个Store会放在App顶层。
  • View拿到Store储存的状态(State)并把它映射成视图。View还会与用户进行交互,用户点击按钮滑动屏幕等等,这时会因为交互需要数据发生改变。
  • Redux让我们不能让View直接操作数据,而是通过发起一个action来告诉Reducer,状态得改变啦。
  • 这时候Reducer接收到了这个action,他就回去遍历action表,然后找到那个匹配的action,根据action生成新的状态并把新的状态放到Store中。
  • Store丢弃了老的状态对象,储存了新的状态对象后,就通知所有使用到了这个状态的View更新(类似setState)。这样我们就能够同步不同view中的状态了。

Lets do it!

这里我们以一个最简单的CountApp举例。简单介绍flutter_redux/redux的用法。该项目完整代码已上传Github

这是一个在不同页面使用Redux共享状态信息的app。这两个页面都依赖于一个数字,这个数字会随着我们按下按钮的次数而增加。

image

第一步:添加依赖

image

我们这里使用了redux/flutter_redux库,它们都是由Brian Egan大神编写的。其中flutter_redux是用来简化redux的使用的。

第二步:创建State

我们刚才介绍了Redux的流程,状态是由reducer生成并储存在Store里面的。Store更新状态的时候,并不是更改原来的状态对象,而是直接将reducer生成的新的状态对象替换掉老的状态对象。所以,我们的状态应该是immutable的。

import 'package:meta/meta.dart';
/**
 * State中所有属性都应该是只读的
 */
@immutable
class CountState{
  int _count;
  get count => _count;

  CountState(this._count);
}

第三步:创建action

可能各位最开始接触的时候对Action还会摸不着头脑。action到底是什么?View如何发出action。其实,action只是我们对状态进行操作方法的一个代号而已。在我们这个应用中,唯一的一个功能就是让count的值+1,所以我们这里只有一个action。

/**
 * 定义操作该State的全部Action
 * 这里只有增加count一个动作
 */
enum Action{
  increment
}

第四步:创建reducer

reducer是我们的状态生成器,它接收一个我们原来的状态,然后接收一个action,再匹配这个action生成一个新的状态。

/**
 * reducer会根据传进来的action生成新的CountState
 */
CountState reducer(CountState state,action){
  //匹配Action
    if(action == Action.increment){
      return CountState(state.count+1);
    }
    return state;
}

第五步:创建store

Store接收一个reducer,以及初始化State,我们想用Redux管理全局的状态的话,需要将store储存在应用的入口才行。而在应用打开时要先初始化一次应用的状态。所以在State中添加一个初始化的函数。

//这段代码写在State中
CountState.initState(){ _count = 0;}
//应用顶层
void main() {
  final store =
      Store<CountState>(reducer, initialState: CountState.initState());
  runApp(new MyApp(store));
}

第六步:将Store放入顶层

flutter_redux提供了一个很棒的widget叫做StoreProvider,它的用法也很简单,接收一个store,和child Widget。

class MyApp extends StatelessWidget {
  final Store<CountState> store;

  MyApp(this.store);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<CountState>(
      store: store,
      child: new MaterialApp(
        title: 'Flutter Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: TopScreen(),
      ),
    );
  }
}

第六步:在子页面中获取Store中的state

这里建议大家把实际代码对照下面的解释一起看。

StoreConnector<CountState,int>(
              converter: (store) => store.state.count,
              builder: (context, count) {
                return Text(
                  count.toString(),
                  style: Theme.of(context).textTheme.display1,
                );
              },
            ),

要想获取store我们需要使用StoreConnector<S,ViewModel>。StoreConnector能够通过StoreProvider找到顶层的store。而且能够在state发生变化时rebuilt Widget。


image
  • 首先这里需要强制声明类型,S代表我们需要从store中获取什么类型的state,ViewModel指的是我们使用这个State时的实际类型。
  • 然后我们需要声明一个converter<S,ViewModel>,它的作用是将Store转化成实际ViewModel将要使用的信息,比如我们这里实际上要使用的是count,所以这里将count提取出来。
  • builder是我们实际根据state创建Widget的地方,它接收一个上下文context,以及刚才我们转化出来的ViewModel,所以我们就只需要把拿到的count放进Text Widget中进行渲染就好了。

第七步:发出action

我们这个应用在第二个页面中,通过点击floatingActionButton发出了action,并通知reducer生成了新的状态。

floatingActionButton: StoreConnector<CountState,VoidCallback>(
        converter: (store) {
          return () => store.dispatch(Action.increment);
        },
        builder: (context, callback) {
          return FloatingActionButton(
            onPressed: callback,
            child: Icon(Icons.add),
          );
        },
      ),
  • 同样,我们还是使用StoreConnector<S,ViewModel>。这里由于是发出了一个动作,所以是VoidCallback。
  • store.dispatch发起一个action,任何中间件都会拦截该操作,在运行中间件后,操作将被发送到给定的reducer生成新的状态,并更新状态树。

以上便是在flutter中使用redux共享状态信息的全部内容。

Q&A

ViewModel性能优化

我们的StoreConnector能够将store提取出信息并转化成ViewModel,这里其实是有一个性能优化的点的。我们这里的例子非常简单,它的ViewModel就只是一个int的值,当我们ViewModel很复杂的时候,我们可以使用StoreConnector的distinct属性进行性能优化。使用方法很简单:需要我们在ViewModel中重写[==] and [hashCode] 方法,然后把distinct属性设为true。

如何处理异步数据

Redux提供了一种简单的方法来更新应用程序的状态以响应同步操作。但是,它缺少处理异步代码的工具。我们如何应对异步相应呢。

这里就需要一个interrupt来处理异步请求,然后再发出新的action通知reducer生成新的State了。
这里有brianegan大神写的另外一个帮助在flutter中使用redux处理异步请求的库redux_thunk。我会在之后的文章中详细介绍如何在redux中处理异步操作。

你认为redux真的适合flutter吗

我们发现,redux的确能够在flutter中很好的工作。在react中数据是没有上行能力的,所以通过数据单向流动形成一个环来进行状态管理。看上去似乎并没有把flutter中的优势完全发挥出来。在这个简单的例子中我们也可以看出,使用redux还是稍微有些麻烦的,用的不好,可能会陷入redux地狱。学习成本偏高也是它的一大痛点。

当然,redux这套状态管理架构已经比较成熟,假如您已经习惯redux,也能够快速通过flutter_redux轻松构建属于您的状态管理应用。

那么你现在如何看待redux呢?

写在最后

本次所用到的代码已经上传Github:
https://github.com/Vadaski/Flutter-Notebook/tree/master/mecury_project/example/redux_demo

这篇文章参考了以下资料

了解更多

你能在这些地方了解更多关于flutter-redux

如果您对flutter_redux还有任何看法或者文章的建议或者文章中有任何不对之处,欢迎在下方评论区以及我的邮箱1652219550a@gmail.com留言,我会在24小时内与您联系!

按理说下一章我们将探索BLoC在Flutter中的实践,而BLoC非常Reactive Programming,所以我决定先让大家了解一些dart:Stream的知识再介绍它,所以下一篇文章我们会介绍Stream以及流式编程,敬请关注。

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

推荐阅读更多精彩内容

  • 前段时间在RxSwift上做了一些实践,Rx确实是一个强大的工具,但同时也是一把双刃剑,如果滥用的话反而会带来副作...
    L_Zephyr阅读 4,184评论 0 15
  • 学习必备要点: 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理 弄清楚Redux是...
    贺贺v5阅读 8,877评论 9 58
  • 作为系列文章的第二篇,本篇将为你着重展示:如何搭建一个通用的Flutter App 常用功能脚手架,快速开发一个完...
    恋猫月亮阅读 33,960评论 13 99
  • 今天早上妹妹上早教,8:00叫醒儿子。9:10才可以出发,我有点生气,跟他唠叨了几句,去到早教那里找地看...
    英才日记阅读 217评论 0 0
  • 看的见和看不见的 一个好的经济学家判断应该的是是可以判断看不见的后果。看不见的东西是暂时不会发生的,但是做决策的时...
    写个锤子阅读 192评论 0 0