何為分頁(yè)?
以QQ好友列表為例:假如你的好友總共有100個(gè),那么考慮性能等因素,第一次只獲取并顯示前10條數(shù)據(jù)。當(dāng)用戶加載更多時(shí),再去獲取后面的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;
// 請(qǐng)求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
currentIndex++;
// 請(qǐng)求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
}
乍一看似乎沒啥問題,仔細(xì)一看,如果請(qǐng)求失敗了(這里假設(shè):沒有數(shù)據(jù)服務(wù)器也會(huì)返回失?。?,會(huì)出現(xiàn)這樣的問題:
第一次我們從服務(wù)器獲取10條數(shù)據(jù)(假設(shè)沒有網(wǎng)絡(luò)),那么必定無(wú)法獲取到數(shù)據(jù),此時(shí)
currentIndex的值變成0了。如果這時(shí)候用戶“上拉加載更多”(假設(shè)有網(wǎng)絡(luò)),那么currentIndex的值變成1了,此時(shí)從服務(wù)器獲取的數(shù)據(jù)是“第二頁(yè)”的,因?yàn)榈谝豁?yè)數(shù)據(jù)被我們跳過了~
解決辦法是什么呢?我們思考下,出現(xiàn)問題的原因是因?yàn)槲覀儭疤嵩纭备淖?code>currentIndex的值了!那么解決辦法就是在“成功”的情況下才去改變currentIndex的值。于是,我們有了第二種寫法。
- 寫法二
public class XActivity extends Activity
{
int currentIndex = 0;
int pageSize = 10;
// 下拉刷新
public void onPullDown()
{
// 請(qǐng)求服務(wù)器數(shù)據(jù)
loadFromServer(0, pageSize, new Callback(){
// 請(qǐng)求服務(wù)器數(shù)據(jù)
public void onSuccess(List list)
{
currentIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 請(qǐng)求服務(wù)器數(shù)據(jù)
loadFromServer(currentIndex + 1, pageSize, new Callback(){
public void onSuccess(List list)
{
currentIndex++;
}
public void onFailure()
{}
});
}
}
你會(huì)問:第二種寫法沒啥問題了吧?嗯~,確實(shí)沒啥問題。有一天服務(wù)器哥們跑來跟你說,分頁(yè)策略要換一種方式,納尼?分頁(yè)還能有啥策略???(以上策略為pageIndex, pageSize)
確實(shí)還有一種策略,那就是startIndex, endIndex,也就是獲取指定區(qū)間的數(shù)據(jù),萬(wàn)一哪天接口用這種策略來分頁(yè),你心里估計(jì)有一萬(wàn)個(gè)草泥馬了。
這種策略現(xiàn)實(shí)中是有它存在的場(chǎng)景的,比如說,列表頁(yè)面需要?jiǎng)h除某條數(shù)據(jù),但需要保持原位置不動(dòng),此時(shí)我們?nèi)绻ㄟ^“先刪除后刷新”的模式,那么就需要控制列表滾動(dòng)到剛剛用戶瀏覽的記錄的位置。
技術(shù)來講上是可以實(shí)現(xiàn)的,但對(duì)于用戶體驗(yàn)來講,會(huì)有一個(gè)加載的過程,顯然是不太友好的。
換一種思路,如果采用“先刪除服務(wù)器后刪除本地”,那么就可以避免“再次請(qǐng)求數(shù)據(jù)并刷新”的過程,對(duì)于用戶體驗(yàn)來講,也是非常大的提升。
如果使用pageIndex, pageSize的策略,那么就顯然無(wú)法滿足這種需求。
舉個(gè)例子,假如目前有10條數(shù)據(jù),調(diào)接口刪除了第10條數(shù)據(jù),此時(shí)請(qǐng)求下一頁(yè)數(shù)據(jù),會(huì)漏掉刪除之前原本排在第11位的數(shù)據(jù)。
而使用startIndex, endIndex策略,可以將startIndex-1之后再去獲取下一頁(yè)數(shù)據(jù),這樣數(shù)據(jù)就不會(huì)丟失。
既然如此,我們來看下這種策略如何實(shí)現(xiàn)吧(伏筆,后面會(huì)放大招如何統(tǒng)一處理這兩種策略)
- 寫法三
public class XActivity extends Activity
{
final int pageSize = 10; // 固定大小
int startIndex = -1; // 起始頁(yè)(從0開始)
// 下拉刷新
public void onPullDown()
{
// 請(qǐng)求服務(wù)器數(shù)據(jù)
loadFromServer(0, pageSize - 1, new Callback(){
// 請(qǐng)求服務(wù)器數(shù)據(jù)
public void onSuccess(List list)
{
startIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 防止第一頁(yè)直接“上拉加載更多”
int tempStartIndex = startIndex + pageSize;
if (startIndex == -1)
{
tempStartIndex = 0;
}
// 請(qǐng)求服務(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]...
分頁(yè)為何如此重要?
對(duì)于一個(gè)App來說,界面基本可以歸結(jié)為兩種:列表和單頁(yè)面。如果團(tuán)隊(duì)開發(fā),每個(gè)列表界面都讓開發(fā)去寫一套分頁(yè)的邏輯(都按照標(biāo)準(zhǔn)就謝天謝地了,見過copy都能漏的),難免會(huì)有出錯(cuò)的時(shí)候(代碼叢中走,哪有不濕鞋~)。
遇到這種情況,直覺上告訴我,有必要來一次封裝了。我們思考下,這兩種策略的共同之處有哪些?

共同之處應(yīng)該比較好理解,不同之處主要是什么呢?
那就是分頁(yè)需要的兩個(gè)參數(shù)param1和param2,計(jì)算方式如下:
- 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)前頁(yè)下標(biāo)。
具體實(shí)現(xiàn)看下面代碼,不同之處會(huì)定義為兩個(gè)抽象方法,交給不同策略去實(shí)現(xiàn)(僅貼出了關(guān)鍵代碼并作了一定裁剪)。
共同之處實(shí)現(xiàn)
public abstract class IPage {
// 默認(rèn)起始頁(yè)下標(biāo)
public static final int DEFAULT_START_PAGE_INDEX = 0;
// 默認(rèn)分頁(yè)大小
public static final int DEFAULT_PAGE_SIZE = 10;
protected int currPageIndex; // 當(dāng)前頁(yè)下標(biāo)
int lastPageIndex; // 記錄上一次的頁(yè)下標(biāo)
int pageSize; // 分頁(yè)大小
boolean isLoading; // 是否正在加載
Object lock = new Object(); // 鎖
public IPage()
{
initPageConfig();
}
/**
* 加載分頁(yè)數(shù)據(jù)
* 分頁(yè)策略1:[param1, param2] = [pageIndex, pageSize]
* 分頁(yè)策略2:[param1, param2] = [startIndex, endIndex]
* @param param1
* @param param2
*/
public abstract void load(int param1, int param2);
/**
* 根據(jù)分頁(yè)策略,處理第一個(gè)分頁(yè)參數(shù)
* @param currPageIndex
* @param pageSize
* @return
*/
public abstract int handlePageIndex(int currPageIndex, int pageSize);
/**
* 根據(jù)分頁(yè)策略,處理第二個(gè)分頁(yè)參數(shù)
* @param currPageIndex
* @param pageSize
* @return
*/
protected abstract int handlePage(int currPageIndex, int pageSize);
/**
* 初始化分頁(yè)參數(shù)
*/
private void initPageConfig()
{
currPageIndex = DEFAULT_START_PAGE_INDEX - 1;
lastPageIndex = currPageIndex;
pageSize = DEFAULT_PAGE_SIZE;
isLoading = false;
}
/**
* 分頁(yè)加載數(shù)據(jù)
* [可能會(huì)拋出異常,請(qǐng)確認(rèn)數(shù)據(jù)加載結(jié)束后,你已經(jīng)調(diào)用了finishLoad(boolean success)方法]
* @param isFirstPage true: 第一頁(yè) false: 下一頁(yè)
*/
public void loadPage()
{
synchronized (lock)
{
if (isLoading) // 如果正在加載數(shù)據(jù),則拋出異常
{
throw new RuntimeException();
}
else
{
isLoading = true;
}
}
if (isFirstPage) // 加載第一頁(yè)數(shù)據(jù)
{
currPageIndex = getStartPageIndex();
}
else
{
currPageIndex = handlePageIndex(currPageIndex, pageSize);
}
load(currPageIndex, handlePage(currPageIndex, pageSize));
}
/**
* 加載結(jié)束
* @param success true:加載成功 false:失敗(無(wú)數(shù)據(jù))
*/
public void finishLoad(boolean success)
{
synchronized (lock)
{
isLoading = false;
}
if (success)
{
lastPageIndex = currPageIndex;
}
else
{
currPageIndex = lastPageIndex;
}
}
}
handlePageIndex和handlePage兩個(gè)抽象方法分別用來計(jì)算param1和param2,需要具體分頁(yè)策略(子類)來實(shí)現(xiàn)。
關(guān)鍵方法loadPage:
首先,判斷是否是第一頁(yè),來計(jì)算第一個(gè)參數(shù)param1:
if (isFirstPage) // 加載第一頁(yè)數(shù)據(jù)
{
currPageIndex = getStartPageIndex();
}
else
{
currPageIndex = handlePageIndex(currPageIndex, pageSize);
}
緊接著,計(jì)算第二個(gè)參數(shù)param2,并調(diào)用抽象方法load(int param1, int param2)回調(diào)給調(diào)用者:
load(currPageIndex, handlePage(currPageIndex, pageSize));
不同之處的實(shí)現(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) // 加載第一頁(yè)數(shù)據(jù)(防止第一頁(yè)使用"上拉加載更多")
{
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();
}
/**
* 邊界檢測(cè)
*/
private void checkBound()
{
if (currPageIndex < getStartPageIndex() - pageSize)
{
currPageIndex = getStartPageIndex() - pageSize;
}
}
}
這兩種策略的算法應(yīng)該不用多講,其實(shí)就是我們?cè)谇懊鎺追N寫法中提到過的。
封裝好了之后,我們看下如何使用吧。
public class XActivity extends Activity
{
IPage page;
void init()
{
page = new Page1() { // pageIndex, pageSize策略
@Override
public void load(int param1, int param2) {
// 請(qǐng)求服務(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);
}
}
是不是瞬間感覺世界如此之清凈,萬(wàn)物歸于平靜。如果如要使用startIndex, endIndex策略,只需這樣做:
page = new Page1() {
}
替換為
page = new Page2() {
}
注意:不管成功還是失敗,最后一定要調(diào)用page.finishLoad(true or false),否則你再次調(diào)用page.loadPage(boolean isFirstPage)會(huì)拋出一個(gè)異常。
這里的設(shè)計(jì)思路,一方面出于加載失敗回滾分頁(yè),一方面為了控制IPage的并發(fā)訪問(實(shí)際情況,我們使用的上拉和下拉組件,不會(huì)同時(shí)觸發(fā)上拉和下拉回調(diào)函數(shù)的)。
拓展:
我們一般是用ListView或者ExpandableListView去實(shí)現(xiàn)列表,而這二者都是需要使用適配器去顯示數(shù)據(jù),那么我們是不是可以把IPage封裝到我們的“基類”適配器呢?這樣,使用者甚至都不知道IPage的存在,而只需要關(guān)心非常熟悉的適配器Adapter。
思路已經(jīng)很明顯,具體的實(shí)現(xiàn)各位可以去試試看。
寫在最后
本文所講解的分頁(yè)實(shí)現(xiàn)方式,包括拓展中如何與適配器結(jié)合的思考,其實(shí)是Android-BaseLine框架中的一個(gè)模塊而已。
另外,Android-BaseLine還提供了很多其他模塊的封裝(比如網(wǎng)絡(luò)請(qǐng)求模塊、異步任務(wù)的封裝、數(shù)據(jù)層和UI層的通信方式統(tǒng)一、key-value數(shù)據(jù)庫(kù)存儲(chǔ)、6.0動(dòng)態(tài)權(quán)限申請(qǐng)、各種適配器(普通、分頁(yè)、單選、多選)等),后續(xù)有機(jī)會(huì)跟大家作進(jìn)一步的介紹。
當(dāng)然,框架的好壞各有各的見解,我只想說,適合當(dāng)下的才是最好的。