Android中如何優(yōu)雅的實(shí)現(xiàn)分頁(yè)

何為分頁(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í)候(代碼叢中走,哪有不濕鞋~)。

遇到這種情況,直覺上告訴我,有必要來一次封裝了。我們思考下,這兩種策略的共同之處有哪些?

共同之處.png

共同之處應(yīng)該比較好理解,不同之處主要是什么呢?
那就是分頁(yè)需要的兩個(gè)參數(shù)param1和param2,計(jì)算方式如下:

  • param1
    • pageIndex, pagSize:param1 = ++currPageIndex
    • startIndex, endIndex:param1 = currPageIndex + pageSize
  • param2
    • pageIndex, pagSize:param2 = pageSize
    • startIndex, endIndex:param2 = currPageIndex + pageSize - 1

注: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;
        }
    }
}

handlePageIndexhandlePage兩個(gè)抽象方法分別用來計(jì)算param1param2,需要具體分頁(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)下的才是最好的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容