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

何為分頁?

以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都能漏的),難免會有出錯的時候(代碼叢中走,哪有不濕鞋~)。

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

共同之處.png

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

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

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

handlePageIndexhandlePage兩個抽象方法分別用來計算param1param2,需要具體分頁策略(子類)來實現(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)下的才是最好的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內(nèi)容