store用于存儲(chǔ)應(yīng)用數(shù)據(jù)茧痒,表達(dá)應(yīng)用狀態(tài)疮跑。設(shè)計(jì)store的步驟大致有兩步:
- 梳理應(yīng)用數(shù)據(jù)都有哪些
- 設(shè)計(jì)這些數(shù)據(jù)在store中的組織結(jié)構(gòu)
梳理數(shù)據(jù)
首先說明一下食寡,這里說的數(shù)據(jù)辟汰,有時(shí)可能包含一些狀態(tài)典格。
數(shù)據(jù)大概有幾類:
- 界面需要展示的數(shù)據(jù)
- 為了獲取1中的數(shù)據(jù)岛宦,而需要的數(shù)據(jù)
- 界面相關(guān)的狀態(tài)數(shù)據(jù)
- 其他
第一類不用說肯定要放到store里,指需要在界面展示的業(yè)務(wù)數(shù)據(jù)
第二類是可能會(huì)讓人迷惑的耍缴,比如請(qǐng)求分頁數(shù)據(jù)時(shí)的參數(shù)砾肺,請(qǐng)求到第幾頁了
第三類指正在加載,下拉刷新中等界面動(dòng)畫之類的進(jìn)行狀態(tài)防嗡,或者是用戶的交互狀態(tài)变汪,比如用戶是否是第一次打開界面,或者有沒有點(diǎn)擊過某個(gè)按鍵
第四類比較特殊蚁趁,我遇到一個(gè)就是數(shù)據(jù)的初始化狀態(tài)裙盾,即數(shù)據(jù)是否已經(jīng)初始化
其他的情況暫時(shí)沒有遇到
我理解的判斷一個(gè)數(shù)據(jù)是否應(yīng)該放入store的方法是:
判斷這個(gè)數(shù)據(jù)所代表的含義是否和界面狀態(tài)無關(guān),如果和界面無關(guān)就放入store他嫡,有關(guān)就單獨(dú)維護(hù)番官。
比如 分頁的頁數(shù)代表的是業(yè)務(wù)數(shù)據(jù)的頁數(shù)和界面無關(guān),就放入store钢属,
加載中代表的是界面正在laoding徘熔,和界面有關(guān),就不放入store
用戶是否點(diǎn)擊過某個(gè)按鈕也是界面上的操作淆党,就不放入store
初始化狀態(tài)是代表數(shù)據(jù)是否有初始化酷师,和界面無關(guān),就放入store
設(shè)計(jì)store結(jié)構(gòu)
store的結(jié)構(gòu)都是樹狀結(jié)構(gòu)
大致的原則是
- 按模塊劃分染乌,比如同一個(gè)界面相關(guān)的數(shù)據(jù)放一個(gè)樹節(jié)點(diǎn)上
- 盡量扁平山孔,比如一級(jí)節(jié)點(diǎn)是模塊,二級(jí)節(jié)點(diǎn)就是具體數(shù)據(jù)
- 顯式聲明使用不變量類型荷憋, 比如直接用HashPMap TreePVetor(pcollection庫中)饱须,而不用Map List
按模塊劃分是為了方便維護(hù),不同的模塊相互不會(huì)沖突台谊,也方便不同的模塊監(jiān)聽數(shù)據(jù)
扁平是為了減少嵌套的層級(jí)蓉媳,數(shù)據(jù)狀態(tài)一目了然
使用不變量是基本要求,而顯示聲明類型锅铅,是讓使用數(shù)據(jù)的人酪呻,可以明確的知道這個(gè)是不能修改的
細(xì)節(jié)
- 是否需要初始化狀態(tài) 看情況,主要是為了解決耗時(shí)的初始化產(chǎn)生的前后界面不一致
- 葉子節(jié)點(diǎn)的數(shù)據(jù)需要注意盐须,如果是復(fù)雜的數(shù)據(jù)結(jié)構(gòu)玩荠,比如自定義類,或者HashPMap贼邓,在使用的時(shí)候需要考慮空指針
- 到底是把復(fù)雜的數(shù)據(jù)扁平化用多個(gè)葉子節(jié)點(diǎn)阶冈,還是用一個(gè)復(fù)雜數(shù)據(jù)結(jié)構(gòu)和一個(gè)葉子節(jié)點(diǎn),各有利弊塑径,扁平化會(huì)使每個(gè)reducer的邏輯簡單女坑,但是代碼較大,而且同一個(gè)action可能有好多reducer都要處理统舀,有可能漏reducer處理匆骗,一個(gè)葉子節(jié)點(diǎn),這個(gè)reducer的邏輯會(huì)很復(fù)雜誉简,但是代碼相對(duì)集中碉就,如果你有能力有條理的寫復(fù)雜代碼可以用一個(gè)葉子節(jié)點(diǎn),如果不行闷串,就扁平化瓮钥,每個(gè)reducer的代碼邏輯都很簡單,容易上手烹吵。
實(shí)例分析
背景
我們的界面是多種數(shù)據(jù)一起刷新碉熄,由于接口是復(fù)用的,就沒有新定義接口年叮,在刷新時(shí)是單獨(dú)調(diào)用每個(gè)數(shù)據(jù)接口獲取數(shù)據(jù)具被,結(jié)果就有三種:全部成功、部分成功只损、全部失敗一姿,需求是有數(shù)據(jù)就展示數(shù)據(jù),有數(shù)據(jù)有失敗的時(shí)候跃惫,Toast提示失敗叮叹,全部失敗時(shí),界面展示異常界面爆存。
設(shè)計(jì)
因?yàn)閿?shù)據(jù)請(qǐng)求失敗蛉顽,是應(yīng)用數(shù)據(jù)的狀態(tài),就把這個(gè)數(shù)據(jù)放入store了先较,一般失敗都有錯(cuò)誤碼和錯(cuò)誤提示信息携冤,我就把這兩個(gè)字段都放入store了悼粮。
類似下面
{
"status" : 0,
"msg" : "Success"
}
界面綁定數(shù)據(jù)的邏輯如下
if(hasData()) {
// show data
if(status != 0) {
// show Toast with msg
}
} else {
// show error view with status
}
第一個(gè)bug
在每次刷新單個(gè)數(shù)據(jù)時(shí),如果一直是相同的錯(cuò)誤曾棕,status和msg不會(huì)變扣猫,導(dǎo)致只有第一次刷新提示失敗,后續(xù)刷新就沒有錯(cuò)誤提示了翘地。
我當(dāng)時(shí)覺得這個(gè)問題在于status和msg沒有變申尤,所以不會(huì)觸發(fā)數(shù)據(jù)變化監(jiān)聽,于是我修改了store
{
"status" : 0,
"msg" : "Success",
"counter" : 0
}
每次reducer處理衙耕,如果是失敗昧穿,counter就加1,讓數(shù)據(jù)產(chǎn)生變化橙喘,從而觸發(fā)監(jiān)聽时鸵,進(jìn)行錯(cuò)誤提示
第二個(gè)bug
每次界面(Fragment)重新加載,重新綁定數(shù)據(jù)時(shí)渴杆,如果原來的狀態(tài)是有部分?jǐn)?shù)據(jù)寥枝,有失敗,就會(huì)在Fragment每次啟動(dòng)時(shí)磁奖,彈Toast囊拜。
為什么會(huì)這樣呢?我當(dāng)時(shí)覺得原因在status和msg其實(shí)是上次的請(qǐng)求失敗比搭,并不是當(dāng)下的請(qǐng)求失敗冠跷,所以我在Fragment里記錄counter,綁定數(shù)據(jù)前身诺,先讀取一次蜜托,在綁定時(shí)做判斷,邏輯如下
// 在綁定之前 先獲取counter
int msgCounter = counter;
// 綁定邏輯
if(hasData()) {
// show data
if(status != 0 && msgCounter != counter) { // counter值不一樣 說明這是新的錯(cuò)誤霉赡,應(yīng)該提示
// show Toast with msg
msgCounter = counter
}
} else {
// show error view with status
}
反思
這兩個(gè)bug橄务,我用這種方式是解決了,但是我的解決方式對(duì)嗎穴亏?
先看第二個(gè)bug蜂挪,status和msg是上次請(qǐng)求的結(jié)果,但是實(shí)際上數(shù)據(jù)也是上次請(qǐng)求的結(jié)果嗓化,在不重新刷新的情況下棠涮,上一次請(qǐng)求的數(shù)據(jù),就是當(dāng)下的數(shù)據(jù)刺覆,綁定當(dāng)下的數(shù)據(jù)在邏輯上沒有問題严肪。
但是為什么會(huì)出bug呢,關(guān)鍵在于彈錯(cuò)誤提示其實(shí)是一次需求,不需要多次彈驳糯,但是狀態(tài)記錄在store里篇梭,導(dǎo)致每次綁定都會(huì)彈Toast。
我開始反思status和msg到底應(yīng)該不應(yīng)該放入store酝枢,按照前面說的判斷方法很洋,異常狀態(tài)這個(gè)數(shù)據(jù)代表的業(yè)務(wù)含義就是應(yīng)用的狀態(tài),比如沒有網(wǎng)絡(luò)隧枫,退出登錄,其實(shí)和界面無關(guān)谓苟,所以應(yīng)該放入store官脓,沒有錯(cuò)。
然后我開始注意到其實(shí)我判斷異常狀態(tài)需要的只有status一個(gè)字段涝焙,沒有msg字段卑笨,狀態(tài)判斷不會(huì)有任何問題,我發(fā)現(xiàn)msg字段只是因?yàn)槲覀円恢币粊礤e(cuò)誤碼和錯(cuò)誤信息是放一起的仑撞,所以我才把msg字段放入store的赤兴。
那我們單獨(dú)對(duì)msg字段進(jìn)行判斷,msg字段表示的業(yè)務(wù)含義是錯(cuò)誤狀態(tài)在界面上的提示信息隧哮,它是和界面相關(guān)的桶良,沒有界面,也就不需要展示沮翔,就不需要msg字段陨帆,所以msg字段不應(yīng)該放入store!
msg字段其實(shí)屬于前面說的第三類數(shù)據(jù):界面相關(guān)的狀態(tài)數(shù)據(jù)采蚀。
那需求又該怎樣實(shí)現(xiàn)呢疲牵?前面的兩個(gè)bug又該如何修改呢?
// Store結(jié)構(gòu)
{
"status" : 0
}
// 刷新數(shù)據(jù)
dispatcher.dispatch(new RrefreshAction(new RefreshListener(){
void refreshResult(String msg){
// 如果msg不為空榆鼠,即刷新有失敗的情況纲爸,需要彈Toast
// show Toast with msg
}
}));
// 數(shù)據(jù)綁定
if(hasData()) {
// show data
} else {
// show error view with status
}
這樣修改之后妆够,前面兩個(gè)bug自然就不存在了