Android中如何優雅的實現分頁

Sheryl46J 8年前發布 | 29K 次閱讀 安卓開發 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

       

 本文由用戶 Sheryl46J 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!