为什么要用Redux
当前的问题
举个例子,假如我们现在有一款拥有购物车的应用,那这个购物车肯定是需要能被不同的页面刷新数据,比如加入购物车,移除购物车,修改数量等等操作,
而且购物车作为应用核心逻辑肯定是需要测试充分,比如单元测试。
对于这种需求,我们可以通过创建一个ShopCart的单例,或者在 Root Widget中创建一个ShopWidget,然后一层层的向下传递来解决购物车的更新问题。
关于单元测试,使代码可测试的一个基本属性是它与周围环境松耦合,这样更容易在单元测试时通过模拟依赖类来达到测试目的,但是单例通常和使用它的类耦合很严重,而且很难控制单例的创建和Mock数据,这是因为单例只是将创建的细节隐藏了,如果在使用单例时,需要同时弄懂单例所有依赖的类,才能更好的完成测试目标,这就导致对依赖了单例的类进行单元测试时很困难,所以单例对单元测试是不太友好的。替换单例的方式是使用依赖注入,但是Flutter没有依赖注入的框架,因为要这种框架都依赖于反射,Flutter本身的特性是不允许这种框架使用的。
而另一种实现方式将ShopCart 层层向下传递的话,只能把代码搞得一团槽,任何购物车的修改都会引起很多改动,而且我们不仅要更新购物车,同时还要在使用购物车的widget中监听购物车的数据变化,来更新自己的状态,一个简单的方式就是在Root Widget中调用setState方法,但是这会导致应用的所有Widget全部rebuild,这对APP来说太粗暴了,而且基于性能考虑,也不允许我们这样做。
在更复杂的应用中,各种异步同步的状态改变,需要我们需要管理更多的状态,比如Http请求响应,数据缓存,文件读写,以及UI状态,而基于redux的flutter_redux能较好解决我们问题。
Redux为什么可以解决以上的问题
flutter_redux提供了一系列的工具帮我们使用redux,允许widget以一种简单的方式分发操作,然后在reducer或者middleware中编写业务逻辑,该逻辑将执行这些操作,并以一种安全、可测试的方式更新ShoppingCart。
而且,一旦Store中更新了ShoppingCart,商店将发出一个onChange事件。这让你可以监听Store更新,并在UI更改时在正确的位置重新构建UI 。而且通过flutter_redux可以以一种可测试的、可观察的方式将业务逻辑与UI逻辑分离,而不必自己连接一堆东西。
了解Redux
严格的单向数据流是 Redux 架构的设计核心。
Store : 对象,只存在一个,维护整个APP的状态,负责分发和接收Action
Reducer:纯函数,接收State和Action,返回新的State
State:对象,应用的状态树,不能直接修改,只能通过Action更新
Action,对象,描述变化的载体
View:视图层
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
Action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store,本质就是一对象。
Reducer
Reducer是纯函数,可以用这样描述Reducer的作用
(state,action)=>newState
即传入当前的State和Action,返回新的State。
保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
State
存储在Store中的对象树,只有一个,持有了整个APP的状态,并且是只读的,如果想要修改,只能通过发送Action
Store
Store 就是把所有东西联系到一起的对象。Redux 应用只有一个单一的 store,Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器,在Flutter中会有其他的封装实现; - 通过
subscribe(listener)
返回的函数注销监听器,在Flutter中会有其他的封装实现。
Middleware
Middleware即中间件,不用和Recucer一样保持纯净,可以有副作用,执行网络请求等,所有的Action分发都会经过Middleware,数据流就成了这样,Middleware在同步操作的时候不是必须的。
在Flutter中如何使用
在Flutter中使用redux,建议使用flutter-redux,他提供了一系列的工具类,帮助我们更好的使用redux。
-
StoreProvider
- InheritedWidget,可以将初始化时传进的store,向依赖了它的widget传递。 -
StoreBuilder
-Widget ,把从StoreProvider获取到的store直接通过build方法返回。属于StoreConnector的一个封装 -
StoreConnector
-Widget, 提供了将state数据转换为ViewModel的接口,当Store 分发更新后都会重新创建用来更新UI。
重要概念:
- App
State
是一个不可变的对象,在单一的数据源Store中单一存在 -
Store
通过InheritedWidget
的实现StoreProvider来传递 -
State
对象是不可变. 如果要创建新的State,必须通过分发Action -
Action
将会通过Reducer 基于当前的State生成新的State - Reducers 必须保持纯净
- 当有状态改变时,所有依赖了StoreConnector的Widget都会重建,如果想避免不必要的重建,可以使用distinct属性
- 创建container的Widgets专门用来负责将App State转换为一个ViewModel
- 创建 presentation的Widgets 用来负责展示数据即UI
- 建议创建一系列的查询(
selector
functions)方法从State中读取数据,将State当作数据库。 - 在Middleware中操作网络请求,数据库查询等等
具体的使用
在rootWidget中创建store并初始化,然后传进StoreProvider中,这样整个应用都能通过StoreProvider.of(context)获取store使用。
flutter中建议使用container链接Store,使用presentation显示数据,从Store的State中获取数据并转换为ViewModel。
需要注意的是StoreConnector的distinct属性,设置为true可以避免不必要的重建,如果使用了ViewModel转换,需要重写== 和hashCode方法。
Q&A
收集了一些我自己开发中遇到问题,以及针对这些问题搜集到的答案在这来集中叙述下
必须将所有 state 都维护在 Redux 中吗? 可以用 setState()
方法吗?
没有 “标准”。可以选择将所有数据都在 Redux 中维护,那么在任何时刻,应用都是完全有序及可控的。也可以将类似于“下拉菜单是否打开”的非关键或者 UI 状态,在组件内部维护。适合自己的才是最好的。
使用局部组件状态是更好的。
这里有一些将怎样的数据放入 Redux 的经验法则:
- 应用的其他部分是否关心这个数据?
- 是否需要根据需要在原始数据的基础上创建衍生数据?
- 相同的数据是否被用作驱动多个组件?
- 能否将状态恢复到特定时间点(撤销操作)?
- 是否要缓存数据(比如:数据存在的情况下直接去使用它而不是重复去请求它)?
State和Entity怎么来区分
State是整个应用的状态,为了方便查询和更新和减少数据冗余,对State的结构是经过重新组织的,并不是APP的一个完整映射,而Entity往往是接口返回的数据映射,但是如果不考虑State的重新组织,两者在结构上市没有太大的区别的,如果State是重新组织过的,在开发中就是需要在Entity和State有一个读写的转换操作。