Android中如何優雅的實現分頁
何為分頁?
以QQ好友列表為例:假如你的好友總共有100個,那么考慮性能等因素,第一次只獲取并顯示前10條數據。當用戶加載更多時,再去獲取后面的10條數據,并與之前的數據合并一起展示給用戶。
讓我們看下常見的幾種寫法(僅關鍵代碼):
-
寫法一:
public class XActivity extends Activity
{
int currentIndex = -1; // 假設從0開始
int pageSize = 10;
// 下拉刷新
public void onPullDown()
{
currentIndex = 0;
// 請求服務器數據
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
currentIndex++;
// 請求服務器數據
loadFromServer(currentIndex, pageSize, new Callback(){
public void onSuccess(List list)
{}
public void onFailure()
{}
});
}
}</code></pre>
乍一看似乎沒啥問題,仔細一看,如果請求失敗了(這里假設:沒有數據服務器也會返回失敗),會出現這樣的問題:
第一次我們從服務器獲取10條數據(假設沒有網絡),那么必定無法獲取到數據,此時 currentIndex 的值變成0了。如果這時候用戶“上拉加載更多”(假設有網絡),那么 currentIndex 的值變成1了,此時從服務器獲取的數據是“第二頁”的,因為第一頁數據被我們跳過了~
解決辦法是什么呢?我們思考下,出現問題的原因是因為我們“提早”改變 currentIndex 的值了!那么解決辦法就是在“成功”的情況下才去改變 currentIndex 的值。于是,我們有了 第二種 寫法。
-
寫法二
public class XActivity extends Activity
{
int currentIndex = 0;
int pageSize = 10;
// 下拉刷新
public void onPullDown()
{
// 請求服務器數據
loadFromServer(0, pageSize, new Callback(){
// 請求服務器數據
public void onSuccess(List list)
{
currentIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 請求服務器數據
loadFromServer(currentIndex + 1, pageSize, new Callback(){
public void onSuccess(List list)
{
currentIndex++;
}
public void onFailure()
{}
});
}
}</code></pre>
你會問:第二種寫法沒啥問題了吧?嗯~,確實沒啥問題。有一天服務器哥們跑來跟你說,分頁策略要換一種方式,納尼?分頁還能有啥策略???(以上策略為 pageIndex, pageSize )
確實還有一種策略,那就是 startIndex, endIndex ,也就是獲取指定 區間 的數據,萬一哪天 接口 用這種策略來分頁,你心里估計有一萬個草泥馬了。
這種策略現實中是有它存在的場景的,比如說,列表頁面需要刪除某條數據,但需要保持原位置不動,此時我們如果通過 “先刪除后刷新” 的模式,那么就需要控制列表滾動到剛剛用戶瀏覽的記錄的位置。
技術來講上是可以實現的,但對于用戶體驗來講,會有一個加載的過程,顯然是不太友好的。
換一種思路,如果采用 “先刪除服務器后刪除本地” ,那么就可以避免 “再次請求數據并刷新” 的過程,對于用戶體驗來講,也是非常大的提升。
如果使用 pageIndex, pageSize 的策略,那么就顯然無法滿足這種需求。
舉個例子,假如目前有10條數據,調接口刪除了第10條數據,此時請求下一頁數據,會漏掉刪除之前原本排在第11位的數據。
而使用 startIndex, endIndex 策略,可以將 startIndex-1 之后再去獲取下一頁數據,這樣數據就不會丟失。
既然如此,我們來看下這種策略如何實現吧(伏筆,后面會放大招如何統一處理這兩種策略)
-
寫法三
public class XActivity extends Activity
{
final int pageSize = 10; // 固定大小
int startIndex = -1; // 起始頁(從0開始)
// 下拉刷新
public void onPullDown()
{
// 請求服務器數據
loadFromServer(0, pageSize - 1, new Callback(){
// 請求服務器數據
public void onSuccess(List list)
{
startIndex = 0;
}
public void onFailure()
{}
});
}
// 上拉加載更多
public void onPullUp()
{
// 防止第一頁直接“上拉加載更多”
int tempStartIndex = startIndex + pageSize;
if (startIndex == -1)
{
tempStartIndex = 0;
}
// 請求服務器數據
loadFromServer(tempStartIndex, tempStartIndex + pageSize - 1, new Callback(){
public void onSuccess(List list)
{
startIndex = tempStartIndex;
}
public void onFailure()
{}
});
}
}</code></pre>
以上代碼概括來講可以這樣表示:[0, 9]、[10, 19]、[20, 29]...
分頁為何如此重要?
對于一個App來說,界面基本可以歸結為兩種: 列表 和 單頁面 。如果團隊開發,每個列表界面都讓開發去寫一套分頁的邏輯(都按照標準就謝天謝地了,見過copy都能漏的),難免會有出錯的時候(代碼叢中走,哪有不濕鞋~)。
遇到這種情況,直覺上告訴我,有必要來一次封裝了。我們思考下,這兩種策略的共同之處有哪些?

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