store用于存储应用数据,表达应用状态。设计store的步骤大致有两步:
- 梳理应用数据都有哪些
- 设计这些数据在store中的组织结构
梳理数据
首先说明一下,这里说的数据,有时可能包含一些状态。
数据大概有几类:
- 界面需要展示的数据
- 为了获取1中的数据,而需要的数据
- 界面相关的状态数据
- 其他
第一类不用说肯定要放到store里,指需要在界面展示的业务数据
第二类是可能会让人迷惑的,比如请求分页数据时的参数,请求到第几页了
第三类指正在加载,下拉刷新中等界面动画之类的进行状态,或者是用户的交互状态,比如用户是否是第一次打开界面,或者有没有点击过某个按键
第四类比较特殊,我遇到一个就是数据的初始化状态,即数据是否已经初始化
其他的情况暂时没有遇到
我理解的判断一个数据是否应该放入store的方法是:
判断这个数据所代表的含义是否和界面状态无关,如果和界面无关就放入store,有关就单独维护。
比如 分页的页数代表的是业务数据的页数和界面无关,就放入store,
加载中代表的是界面正在laoding,和界面有关,就不放入store
用户是否点击过某个按钮也是界面上的操作,就不放入store
初始化状态是代表数据是否有初始化,和界面无关,就放入store
设计store结构
store的结构都是树状结构
大致的原则是
- 按模块划分,比如同一个界面相关的数据放一个树节点上
- 尽量扁平,比如一级节点是模块,二级节点就是具体数据
- 显式声明使用不变量类型, 比如直接用HashPMap TreePVetor(pcollection库中),而不用Map List
按模块划分是为了方便维护,不同的模块相互不会冲突,也方便不同的模块监听数据
扁平是为了减少嵌套的层级,数据状态一目了然
使用不变量是基本要求,而显示声明类型,是让使用数据的人,可以明确的知道这个是不能修改的
细节
- 是否需要初始化状态 看情况,主要是为了解决耗时的初始化产生的前后界面不一致
- 叶子节点的数据需要注意,如果是复杂的数据结构,比如自定义类,或者HashPMap,在使用的时候需要考虑空指针
- 到底是把复杂的数据扁平化用多个叶子节点,还是用一个复杂数据结构和一个叶子节点,各有利弊,扁平化会使每个reducer的逻辑简单,但是代码较大,而且同一个action可能有好多reducer都要处理,有可能漏reducer处理,一个叶子节点,这个reducer的逻辑会很复杂,但是代码相对集中,如果你有能力有条理的写复杂代码可以用一个叶子节点,如果不行,就扁平化,每个reducer的代码逻辑都很简单,容易上手。
实例分析
背景
我们的界面是多种数据一起刷新,由于接口是复用的,就没有新定义接口,在刷新时是单独调用每个数据接口获取数据,结果就有三种:全部成功、部分成功、全部失败,需求是有数据就展示数据,有数据有失败的时候,Toast提示失败,全部失败时,界面展示异常界面。
设计
因为数据请求失败,是应用数据的状态,就把这个数据放入store了,一般失败都有错误码和错误提示信息,我就把这两个字段都放入store了。
类似下面
{
"status" : 0,
"msg" : "Success"
}
界面绑定数据的逻辑如下
if(hasData()) {
// show data
if(status != 0) {
// show Toast with msg
}
} else {
// show error view with status
}
第一个bug
在每次刷新单个数据时,如果一直是相同的错误,status和msg不会变,导致只有第一次刷新提示失败,后续刷新就没有错误提示了。
我当时觉得这个问题在于status和msg没有变,所以不会触发数据变化监听,于是我修改了store
{
"status" : 0,
"msg" : "Success",
"counter" : 0
}
每次reducer处理,如果是失败,counter就加1,让数据产生变化,从而触发监听,进行错误提示
第二个bug
每次界面(Fragment)重新加载,重新绑定数据时,如果原来的状态是有部分数据,有失败,就会在Fragment每次启动时,弹Toast。
为什么会这样呢?我当时觉得原因在status和msg其实是上次的请求失败,并不是当下的请求失败,所以我在Fragment里记录counter,绑定数据前,先读取一次,在绑定时做判断,逻辑如下
// 在绑定之前 先获取counter
int msgCounter = counter;
// 绑定逻辑
if(hasData()) {
// show data
if(status != 0 && msgCounter != counter) { // counter值不一样 说明这是新的错误,应该提示
// show Toast with msg
msgCounter = counter
}
} else {
// show error view with status
}
反思
这两个bug,我用这种方式是解决了,但是我的解决方式对吗?
先看第二个bug,status和msg是上次请求的结果,但是实际上数据也是上次请求的结果,在不重新刷新的情况下,上一次请求的数据,就是当下的数据,绑定当下的数据在逻辑上没有问题。
但是为什么会出bug呢,关键在于弹错误提示其实是一次需求,不需要多次弹,但是状态记录在store里,导致每次绑定都会弹Toast。
我开始反思status和msg到底应该不应该放入store,按照前面说的判断方法,异常状态这个数据代表的业务含义就是应用的状态,比如没有网络,退出登录,其实和界面无关,所以应该放入store,没有错。
然后我开始注意到其实我判断异常状态需要的只有status一个字段,没有msg字段,状态判断不会有任何问题,我发现msg字段只是因为我们一直一来错误码和错误信息是放一起的,所以我才把msg字段放入store的。
那我们单独对msg字段进行判断,msg字段表示的业务含义是错误状态在界面上的提示信息,它是和界面相关的,没有界面,也就不需要展示,就不需要msg字段,所以msg字段不应该放入store!
msg字段其实属于前面说的第三类数据:界面相关的状态数据。
那需求又该怎样实现呢?前面的两个bug又该如何修改呢?
// Store结构
{
"status" : 0
}
// 刷新数据
dispatcher.dispatch(new RrefreshAction(new RefreshListener(){
void refreshResult(String msg){
// 如果msg不为空,即刷新有失败的情况,需要弹Toast
// show Toast with msg
}
}));
// 数据绑定
if(hasData()) {
// show data
} else {
// show error view with status
}
这样修改之后,前面两个bug自然就不存在了













网友评论