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自然就不存在了