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)系作者
  • 序言:七十年代末并扇,一起剝皮案震驚了整個濱河市去团,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌穷蛹,老刑警劉巖土陪,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肴熏,居然都是意外死亡鬼雀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門扮超,熙熙樓的掌柜王于貴愁眉苦臉地迎上來取刃,“玉大人,你說我怎么就攤上這事出刷¤盗疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵馁龟,是天一觀的道長崩侠。 經(jīng)常有香客問我,道長坷檩,這世上最難降的妖魔是什么却音? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任改抡,我火速辦了婚禮,結(jié)果婚禮上系瓢,老公的妹妹穿的比我還像新娘阿纤。我一直安慰自己贯莺,他們只是感情好喉恋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衩茸,像睡著了一般骗绕。 火紅的嫁衣襯著肌膚如雪藐窄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天酬土,我揣著相機(jī)與錄音荆忍,去河邊找鬼。 笑死撤缴,一個胖子當(dāng)著我的面吹牛刹枉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屈呕,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼嘶卧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凉袱?” 一聲冷哼從身側(cè)響起芥吟,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎专甩,沒想到半個月后钟鸵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涤躲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年棺耍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片种樱。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒙袍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嫩挤,到底是詐尸還是另有隱情害幅,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布岂昭,位于F島的核電站以现,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邑遏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一佣赖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧记盒,春花似錦憎蛤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至彬碱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奥洼,已是汗流浹背巷疼。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灵奖,地道東北人嚼沿。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像瓷患,于是被迫代替她去往敵國和親骡尽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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