何為分頁引有?
以QQ好友列表為例:假如你的好友總共有100個蠢挡,那么考慮性能等因素儒鹿,第一次只獲取并顯示前10條數(shù)據(jù)化撕。當(dāng)用戶加載更多時,再去獲取后面的10條數(shù)據(jù)挺身,并與之前的數(shù)據(jù)合并一起展示給用戶侯谁。
讓我們看下常見的幾種寫法(僅關(guān)鍵代碼):
- 寫法一:
public class XActivity extends Activity
{
int currentIndex = -1; // 假設(shè)從0開始
int pageSize = 10;
// 下拉刷新
public void onPullDown()
{
currentIndex = 0;
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
currentIndex++;
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
}
乍一看似乎沒啥問題,仔細(xì)一看章钾,如果請求失敗了(這里假設(shè):沒有數(shù)據(jù)服務(wù)器也會返回失斍郊),會出現(xiàn)這樣的問題:
第一次我們從服務(wù)器獲取10條數(shù)據(jù)(假設(shè)沒有網(wǎng)絡(luò))贱傀,那么必定無法獲取到數(shù)據(jù)惨撇,此時
currentIndex
的值變成0了。如果這時候用戶“上拉加載更多”(假設(shè)有網(wǎng)絡(luò))府寒,那么currentIndex
的值變成1了魁衙,此時從服務(wù)器獲取的數(shù)據(jù)是“第二頁”的,因為第一頁數(shù)據(jù)被我們跳過了~
解決辦法是什么呢株搔?我們思考下剖淀,出現(xiàn)問題的原因是因為我們“提早”改變currentIndex
的值了!那么解決辦法就是在“成功”的情況下才去改變currentIndex
的值纤房。于是纵隔,我們有了第二種寫法。
- 寫法二
public class XActivity extends Activity
{
int currentIndex = 0;
int pageSize = 10;
// 下拉刷新
public void onPullDown()
{
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(0, pageSize, new Callback(){
// 請求服務(wù)器數(shù)據(jù)
public void onSuccess(List list)
{
currentIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex + 1, pageSize, new Callback(){
public void onSuccess(List list)
{
currentIndex++;
}
public void onFailure()
{}
});
}
}
你會問:第二種寫法沒啥問題了吧炮姨?嗯~捌刮,確實沒啥問題。有一天服務(wù)器哥們跑來跟你說舒岸,分頁策略要換一種方式绅作,納尼?分頁還能有啥策略蛾派?俄认??(以上策略為pageIndex, pageSize
)
確實還有一種策略洪乍,那就是startIndex, endIndex
眯杏,也就是獲取指定區(qū)間的數(shù)據(jù),萬一哪天接口用這種策略來分頁典尾,你心里估計有一萬個草泥馬了。
這種策略現(xiàn)實中是有它存在的場景的糊探,比如說钾埂,列表頁面需要刪除某條數(shù)據(jù)河闰,但需要保持原位置不動,此時我們?nèi)绻ㄟ^“先刪除后刷新”的模式褥紫,那么就需要控制列表滾動到剛剛用戶瀏覽的記錄的位置姜性。
技術(shù)來講上是可以實現(xiàn)的,但對于用戶體驗來講髓考,會有一個加載的過程部念,顯然是不太友好的。
換一種思路氨菇,如果采用“先刪除服務(wù)器后刪除本地”儡炼,那么就可以避免“再次請求數(shù)據(jù)并刷新”的過程,對于用戶體驗來講查蓉,也是非常大的提升乌询。
如果使用pageIndex, pageSize
的策略,那么就顯然無法滿足這種需求豌研。
舉個例子妹田,假如目前有10條數(shù)據(jù),調(diào)接口刪除了第10條數(shù)據(jù)鹃共,此時請求下一頁數(shù)據(jù)鬼佣,會漏掉刪除之前原本排在第11位的數(shù)據(jù)。
而使用startIndex, endIndex
策略霜浴,可以將startIndex-1
之后再去獲取下一頁數(shù)據(jù)晶衷,這樣數(shù)據(jù)就不會丟失。
既然如此坷随,我們來看下這種策略如何實現(xiàn)吧(伏筆房铭,后面會放大招如何統(tǒng)一處理這兩種策略)
- 寫法三
public class XActivity extends Activity
{
final int pageSize = 10; // 固定大小
int startIndex = -1; // 起始頁(從0開始)
// 下拉刷新
public void onPullDown()
{
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(0, pageSize - 1, new Callback(){
// 請求服務(wù)器數(shù)據(jù)
public void onSuccess(List list)
{
startIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 防止第一頁直接“上拉加載更多”
int tempStartIndex = startIndex + pageSize;
if (startIndex == -1)
{
tempStartIndex = 0;
}
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(tempStartIndex, tempStartIndex + pageSize - 1, new Callback(){
public void onSuccess(List list)
{
startIndex = tempStartIndex;
}
public void onFailure()
{}
});
}
}
以上代碼概括來講可以這樣表示:[0, 9]、[10, 19]温眉、[20, 29]...
分頁為何如此重要缸匪?
對于一個App來說,界面基本可以歸結(jié)為兩種:列表和單頁面类溢。如果團(tuán)隊開發(fā)凌蔬,每個列表界面都讓開發(fā)去寫一套分頁的邏輯(都按照標(biāo)準(zhǔn)就謝天謝地了,見過copy都能漏的)闯冷,難免會有出錯的時候(代碼叢中走砂心,哪有不濕鞋~)。
遇到這種情況蛇耀,直覺上告訴我辩诞,有必要來一次封裝了。我們思考下纺涤,這兩種策略的共同之處有哪些译暂?
共同之處應(yīng)該比較好理解抠忘,不同之處主要是什么呢?
那就是分頁需要的兩個參數(shù)param1和param2外永,計算方式如下:
- param1
- pageIndex, pagSize:
param1 = ++currPageIndex
- startIndex, endIndex:
param1 = currPageIndex + pageSize
- pageIndex, pagSize:
- param2
- pageIndex, pagSize:
param2 = pageSize
- startIndex, endIndex:
param2 = currPageIndex + pageSize - 1
- pageIndex, pagSize:
注:currPageIndex
表示當(dāng)前頁下標(biāo)崎脉。
具體實現(xiàn)看下面代碼,不同之處會定義為兩個抽象方法伯顶,交給不同策略去實現(xiàn)(僅貼出了關(guān)鍵代碼并作了一定裁剪)囚灼。
共同之處實現(xiàn)
public abstract class IPage {
// 默認(rèn)起始頁下標(biāo)
public static final int DEFAULT_START_PAGE_INDEX = 0;
// 默認(rèn)分頁大小
public static final int DEFAULT_PAGE_SIZE = 10;
protected int currPageIndex; // 當(dāng)前頁下標(biāo)
int lastPageIndex; // 記錄上一次的頁下標(biāo)
int pageSize; // 分頁大小
boolean isLoading; // 是否正在加載
Object lock = new Object(); // 鎖
public IPage()
{
initPageConfig();
}
/**
* 加載分頁數(shù)據(jù)
* 分頁策略1:[param1, param2] = [pageIndex, pageSize]
* 分頁策略2:[param1, param2] = [startIndex, endIndex]
* @param param1
* @param param2
*/
public abstract void load(int param1, int param2);
/**
* 根據(jù)分頁策略,處理第一個分頁參數(shù)
* @param currPageIndex
* @param pageSize
* @return
*/
public abstract int handlePageIndex(int currPageIndex, int pageSize);
/**
* 根據(jù)分頁策略,處理第二個分頁參數(shù)
* @param currPageIndex
* @param pageSize
* @return
*/
protected abstract int handlePage(int currPageIndex, int pageSize);
/**
* 初始化分頁參數(shù)
*/
private void initPageConfig()
{
currPageIndex = DEFAULT_START_PAGE_INDEX - 1;
lastPageIndex = currPageIndex;
pageSize = DEFAULT_PAGE_SIZE;
isLoading = false;
}
/**
* 分頁加載數(shù)據(jù)
* [可能會拋出異常,請確認(rèn)數(shù)據(jù)加載結(jié)束后祭衩,你已經(jīng)調(diào)用了finishLoad(boolean success)方法]
* @param isFirstPage true: 第一頁 false: 下一頁
*/
public void loadPage()
{
synchronized (lock)
{
if (isLoading) // 如果正在加載數(shù)據(jù)灶体,則拋出異常
{
throw new RuntimeException();
}
else
{
isLoading = true;
}
}
if (isFirstPage) // 加載第一頁數(shù)據(jù)
{
currPageIndex = getStartPageIndex();
}
else
{
currPageIndex = handlePageIndex(currPageIndex, pageSize);
}
load(currPageIndex, handlePage(currPageIndex, pageSize));
}
/**
* 加載結(jié)束
* @param success true:加載成功 false:失敗(無數(shù)據(jù))
*/
public void finishLoad(boolean success)
{
synchronized (lock)
{
isLoading = false;
}
if (success)
{
lastPageIndex = currPageIndex;
}
else
{
currPageIndex = lastPageIndex;
}
}
}
handlePageIndex
和handlePage
兩個抽象方法分別用來計算param1
和param2
,需要具體分頁策略(子類)來實現(xiàn)汪厨。
關(guān)鍵方法loadPage
:
首先赃春,判斷是否是第一頁,來計算第一個參數(shù)param1
:
if (isFirstPage) // 加載第一頁數(shù)據(jù)
{
currPageIndex = getStartPageIndex();
}
else
{
currPageIndex = handlePageIndex(currPageIndex, pageSize);
}
緊接著劫乱,計算第二個參數(shù)param2
织中,并調(diào)用抽象方法load(int param1, int param2)
回調(diào)給調(diào)用者:
load(currPageIndex, handlePage(currPageIndex, pageSize));
不同之處的實現(xiàn)
- pageIndex, pageSize策略
public abstract class Page1 extends IPage
{
@Override
public int handlePageIndex(int currPageIndex, int pageSize) {
return ++currPageIndex;
}
@Override
protected int handlePage(int currPageIndex, int pageSize) {
return pageSize;
}
}
- startIndex, endIndex策略
public abstract class Page2 extends IPage
{
@Override
public int handlePageIndex(int currPageIndex, int pageSize) {
if (currPageIndex == getStartPageIndex() - 1) // 加載第一頁數(shù)據(jù)(防止第一頁使用"上拉加載更多")
{
return getStartPageIndex();
}
return currPageIndex + pageSize;
}
@Override
protected int handlePage(int currPageIndex, int pageSize) {
return currPageIndex + pageSize - 1;
}
/**
* 起始下標(biāo)遞減
*/
public void decreaseStartIndex()
{
currPageIndex--;
checkBound();
}
/**
* 起始下標(biāo)遞減
*/
public void decreaseStartIndex(int size)
{
currPageIndex -= size;
checkBound();
}
/**
* 邊界檢測
*/
private void checkBound()
{
if (currPageIndex < getStartPageIndex() - pageSize)
{
currPageIndex = getStartPageIndex() - pageSize;
}
}
}
這兩種策略的算法應(yīng)該不用多講,其實就是我們在前面幾種寫法中提到過的衷戈。
封裝好了之后狭吼,我們看下如何使用吧。
public class XActivity extends Activity
{
IPage page;
void init()
{
page = new Page1() { // pageIndex, pageSize策略
@Override
public void load(int param1, int param2) {
// 請求服務(wù)器數(shù)據(jù)
loadFromServer(param1, param2, new Callback(){
public void onSuccess(List list)
{
// 一定要調(diào)用殖妇,加載成功
page.finishLoad(true);
}
public void onFailure()
{
// 一定要調(diào)用刁笙,加載失敗
page.finishLoad(false);
}
});
}
};
}
// 下拉刷新
public void onPullDown()
{
page.loadPage(true);
}
// 上拉加載更多
public void onPullUp()
{
page.loadPage(false);
}
}
是不是瞬間感覺世界如此之清凈,萬物歸于平靜谦趣。如果如要使用startIndex, endIndex
策略疲吸,只需這樣做:
page = new Page1() {
}
替換為
page = new Page2() {
}
注意:不管成功還是失敗,最后一定要調(diào)用page.finishLoad(true or false)
前鹅,否則你再次調(diào)用page.loadPage(boolean isFirstPage)
會拋出一個異常摘悴。
這里的設(shè)計思路,一方面出于加載失敗回滾分頁舰绘,一方面為了控制IPage
的并發(fā)訪問(實際情況蹂喻,我們使用的上拉和下拉組件,不會同時觸發(fā)上拉和下拉回調(diào)函數(shù)的)捂寿。
拓展:
我們一般是用ListView
或者ExpandableListView
去實現(xiàn)列表口四,而這二者都是需要使用適配器去顯示數(shù)據(jù),那么我們是不是可以把IPage
封裝到我們的“基類”適配器呢秦陋?這樣蔓彩,使用者甚至都不知道IPage
的存在,而只需要關(guān)心非常熟悉的適配器Adapter。
思路已經(jīng)很明顯赤嚼,具體的實現(xiàn)各位可以去試試看大磺。
寫在最后
本文所講解的分頁實現(xiàn)方式,包括拓展中如何與適配器結(jié)合的思考探膊,其實是Android-BaseLine框架中的一個模塊而已。
另外待榔,Android-BaseLine還提供了很多其他模塊的封裝(比如網(wǎng)絡(luò)請求模塊逞壁、異步任務(wù)的封裝、數(shù)據(jù)層和UI層的通信方式統(tǒng)一锐锣、key-value數(shù)據(jù)庫存儲腌闯、6.0動態(tài)權(quán)限申請、各種適配器(普通雕憔、分頁姿骏、單選、多選)等)斤彼,后續(xù)有機(jī)會跟大家作進(jìn)一步的介紹分瘦。
當(dāng)然,框架的好壞各有各的見解琉苇,我只想說嘲玫,適合當(dāng)下的才是最好的。