不用繼承!使用組合的方式實(shí)現(xiàn)下拉刷新和上拉加載

為什么要使用組合的方式

Android下拉刷新和上拉加載的框架網(wǎng)上已經(jīng)非常多了,但是大多數(shù)都需要繼承自定義的下拉SwiperefreshLayout速梗,或者自定義recyclerview, 或者自定的adapter。主要有以下幾種:

  1. 必須要繼承自定義的xxRefreshLayout襟齿。 將下拉刷新控件及listview整個(gè)封裝到一個(gè)控件里姻锁。
  2. 必須繼承自定義的xxListView。將上拉加載功能封裝到自定義的listview或者RecyclerView里猜欺。
  3. 必須繼承作者自定義的Adapter位隶。

無論哪種我都不愿意使用。我可能有自己的下拉控件开皿,也可能直接使用原生的的SwipeRefreshLayout(會(huì)有一些定制修改)涧黄。列表部分雖然大部分情況下使用RecyclerView,但是有的地方也用到了ListView赋荆。至于Adapter,我更是不會(huì)去繼承了笋妥。相信大家都有自己的BaseAdapter,雖然大同小異窄潭,但是也有獨(dú)特和不同的地方春宣,為了LoadMore去繼承修改自己的BaseAdapter很糟糕。 比如我的BaseAdapter封裝了click事件嫉你,并基于DataBinding月帝,連ViewHolder都省了。

從代碼設(shè)計(jì)上考慮均抽,繼承并不是很好的選擇嫁赏,代碼入侵性太強(qiáng)。還是優(yōu)先使用基礎(chǔ)控件油挥,除非是Google等知名第三方庫潦蝇。這里我更傾向于使用組合包裝的方式實(shí)現(xiàn)LoadMoreHelper款熬。下拉控件可能是PtrFrameLayout也可能是SwiperefreshLayout, 列表控件可能是ListView 也可能是RecyclerView,使用自己的BaseAdapater即可攘乒。LoadMoreHelper并不是自定義控件贤牛,而是通過數(shù)據(jù)Loader,對各個(gè)組件進(jìn)行設(shè)置则酝。而對于各個(gè)組件來說不需要依賴LoadMoreHelper殉簸。

load_more.png

</br>

下拉刷新與上拉加載過程分析

雖然兩者看起來很相似,但是還是有區(qū)別沽讹。一些控件將兩種行為強(qiáng)行整合到一起是不恰當(dāng)?shù)陌惚啊K砸话阆吕⑿驴丶疾粫?huì)提供上拉加載功能,需要用戶自己去實(shí)現(xiàn)

下拉刷新和上拉加載是兩種不同的控件功能爽雄,不要混淆在一起蝠检。

目前最好用的是Google的SwipeRefreshLayout,有的工程還用到了仿ios的PtrFrameLayout挚瘟,盡管該控件不再維護(hù)更新了叹谁,但是是否使用是取決于開發(fā)者,LoadMoreHelper不做限制乘盖。下拉樣式及處理滑動(dòng)事件都是由下拉控件負(fù)責(zé)定制和處理焰檩,LoadMoreHelper不用關(guān)心這些。

先看一下數(shù)據(jù)加載的大致流程:


Pull_and_loadmore.png

雖然下拉刷新和上拉加載在UI體驗(yàn)上不同订框,但是其裝載數(shù)據(jù)的過程卻差不多

下拉刷新load第1頁數(shù)據(jù)析苫,并替換當(dāng)前全部list數(shù)據(jù)

上拉加載load大于第1頁的數(shù)據(jù),并將加載的數(shù)據(jù)補(bǔ)充到當(dāng)前l(fā)ist里

調(diào)用者必須實(shí)現(xiàn)數(shù)據(jù)加載接口布蔗,獲取數(shù)據(jù)結(jié)果藤违,并通過數(shù)據(jù)裝載器通知UI變化。

  • 數(shù)據(jù)加載接口
    可以是同步纵揍,可以是異步顿乒。
    同步數(shù)據(jù)加載接口SyncDataLoader直接返回?cái)?shù)據(jù)結(jié)果,LoadMoreHelper會(huì)在后臺線程調(diào)用該接口;
    異步數(shù)據(jù)加載接口AsyncDataLoader泽谨,在數(shù)據(jù)加載成功后的回調(diào)用LoadMoreHelper.onLoadEnd璧榄,通知數(shù)據(jù)結(jié)果

      /**
       * Load data sync. LoadHelper will call it on work thread
      */
       @WorkerThread
      public interface SyncDataLoader<VM> {
           PageData<VM> startLoadData(int page, PageData<VM> lastPageData);
      }
    
      /**
       * Load data int async thread
       */
      public interface AsyncDataLoader<VM> {
          void startLoadData(int page, PageData<VM> lastPageData);
      }
    

</br>

  • 數(shù)據(jù)結(jié)果PageData的定義,
    主要包含吧雹,1.pageIndex,當(dāng)前頁碼骨杂,2. list,數(shù)據(jù),3. pageMore,是否還有下一頁數(shù)據(jù)雄卷,4. result,是否加載成功

      public final class PageData<DT> {
          private int pageIndex;
          private List<DT> data;
          private boolean pageMore;
          private boolean success = true;
      }
    
  • 數(shù)據(jù)裝載接口
    數(shù)據(jù)裝載的接口定義搓蚪,IDataSwapper,在下拉刷新時(shí)調(diào)用swapData, 在上拉加載時(shí)調(diào)用appendData。建議使用Adapter繼承該接口丁鹉,當(dāng)然也可以單獨(dú)實(shí)現(xiàn)妒潭。

      public interface IDataSwapper<VM> {
          /**
          * Swap all datas
          */
          void swapData(List<? extends VM> list);
          /**
          * Append data to the end of current list
          */
          void appendData(List<? extends VM> list);
      }
    

使用裝飾模式添加加載更多view

由于RecyclerView不能像ListView那樣直接添加headerview或者footerview悴能,需要自己在Adapter里實(shí)現(xiàn)。而這里我們又并不想繼承該Adapter雳灾,使用裝飾模式將調(diào)用者的Adapter包裝起來漠酿。這樣對調(diào)用者來說,只需要關(guān)注自己的Adapter即可谎亩,不需要關(guān)注包裝者炒嘲。

我們先看一下Android原生的ListView是如何實(shí)現(xiàn)header和footer
在添加footer后,會(huì)將原adapter包裝成HeaderViewListAdapter

    public void addFooterView(View v, Object data, boolean isSelectable) {
    ...
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            wrapHeaderListAdapterInternal();
        }
        ...
    }
}

public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
private final ListAdapter mAdapter;
...
}

參考該設(shè)計(jì)匈庭,類似的我們在添加RecyclerView的footerView時(shí),也可以包裝一個(gè)FooterAdapter

//包裝origin adapter
RecyclerView.Adapter<VH> originAdapter = recyclerView.getAdapter();
footerAdapter = new FooterViewAdapter<>(originAdapter);
        recyclerView.setAdapter(footerAdapter);

FooterAdapter 會(huì)將原來的Adapter包裝起來夫凸,并在尾部添加 加載更多的view

    //FooterAdapter 繼承于BaseWrapperAdapter
    public class FooterViewAdapter<VH extends RecyclerView.ViewHolder> extends BaseWrapperAdapter

    public class BaseWrapperAdapter<VH extends RecyclerView.ViewHolder>
    extends RecyclerView.Adapter<VH> {
     private RecyclerView.Adapter<VH> mWrappedAdapter;
     ...
     }

如此,便可以不要求用戶繼承LoadMoreHelperAdapter嚎花,又能在RecyclerView的末尾添加footerView寸痢。對于調(diào)用者來說,這一層完全是透明的紊选,只需要按照之前的習(xí)慣調(diào)用原AdapternotifyDataSetChanged即可,不需要關(guān)注包裝者的存在道逗。

LoadMoreHelper調(diào)用形式

布局文件使用原生的控件即可兵罢。其中SwipeRefreshLayout可替換成PtrFrameLayout,RecyclerView可替換成ListView.

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

Api采用鏈?zhǔn)秸{(diào)用。推薦使用lambda表達(dá)式使代碼更簡潔滓窍。

  1. 首先設(shè)置swipeRefreshLayout,會(huì)自動(dòng)需要包含的RecyclerView卖词。 如果不需要下拉刷新只用上拉加載的話,可以直接設(shè)置RecyclerView吏夯。在LoadMoreHelper內(nèi)部此蜈,會(huì)根據(jù)傳入的view進(jìn)行設(shè)置,設(shè)置RecyclerView的滑動(dòng)監(jiān)聽噪生,包裝Adapter等裆赵。

         loadHelper = LoadMoreHelper.create(swipeRefreshLayout)
             .setDataSwapper(adapter)
             .setAsyncDataLoader((page, lastPageData) -> doLoadData(page))
             .startPullData(true);
    
  2. 設(shè)置IDataSwapper。這里Adapter實(shí)現(xiàn)了IDataSwapper接口跺嗽。當(dāng)然可以不使用Adapter繼承而單獨(dú)實(shí)現(xiàn)該接口战授,具體可參見工程里的例子。

    private class MyAdapter2 extends RecyclerView.Adapter<ViewHolder2> implements IDataSwapper<Item> {
    ...
    @Override
    public void swapData(List<? extends Item> list) {
    datas.clear();
    datas.addAll(list);
    notifyDataSetChanged();
    }

     @Override
     public void appendData(List<? extends Item> list) {
         if (list == null || list.isEmpty()) {
             return;
         }
         int start = datas.size();
         datas.addAll(list);
         notifyItemRangeInserted(start, list.size());
     }
    

    }

  3. 設(shè)置AsyncDataLoader桨嫁。 采用異步數(shù)據(jù)加載植兰,這里的loadData使用了Rxjava做例子。LoadMoreHelper會(huì)在下拉刷新或者上拉加載時(shí)調(diào)用 startLoadData璃吧。通知當(dāng)前所需數(shù)據(jù)的頁碼楣导,調(diào)用者只需填充加載方法,并將數(shù)據(jù)加載結(jié)果通過onLoadEnd傳入畜挨。

         new LoadMoreHelper.AsyncDataLoader<Item>(){
         @Override
         public void startLoadData(int page, PageData lastPageData) {
             DataLoader.loadData(pageIndex, null)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
                     .subscribe(result -> {
                         PageData<Item> pageData = PageData.createSuccess(pageIndex, result.getData(), result.isPageMore());
                         loadHelper.onLoadEnd(pageData);
                     }, e -> {
                         PageData<Item> pageData = PageData.createFailed(pageIndex);
                         loadHelper.onLoadEnd(pageData);
                     });
         }
     };
    

LoadMoreHelper也提供了一些其他接口可以設(shè)置加載更多viewsetLoadMoreViewCreator筒繁,加載失敗viewsetLoadFailedViewCreator彬坏,加載完畢viewsetLoadCompleteViewCreator。頁面數(shù)據(jù)加載失敗后可以點(diǎn)擊重試膝晾。

代碼見
https://github.com/liyuanhust/LoadMoreHelper

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栓始,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子血当,更是在濱河造成了極大的恐慌幻赚,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臊旭,死亡現(xiàn)場離奇詭異落恼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)离熏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門佳谦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人滋戳,你說我怎么就攤上這事钻蔑。” “怎么了奸鸯?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵咪笑,是天一觀的道長。 經(jīng)常有香客問我娄涩,道長窗怒,這世上最難降的妖魔是什么只祠? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任九昧,我火速辦了婚禮,結(jié)果婚禮上樱哼,老公的妹妹穿的比我還像新娘球恤。我一直安慰自己辜昵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布碎捺。 她就那樣靜靜地躺著路鹰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪收厨。 梳的紋絲不亂的頭發(fā)上晋柱,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機(jī)與錄音诵叁,去河邊找鬼雁竞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碑诉。 我是一名探鬼主播彪腔,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼进栽!你這毒婦竟也來了德挣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤快毛,失蹤者是張志新(化名)和其女友劉穎格嗅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唠帝,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屯掖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了襟衰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贴铜。...
    茶點(diǎn)故事閱讀 40,769評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瀑晒,靈堂內(nèi)的尸體忽然破棺而出绍坝,到底是詐尸還是另有隱情,我是刑警寧澤瑰妄,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布陷嘴,位于F島的核電站,受9級特大地震影響间坐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邑退,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一竹宋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧地技,春花似錦蜈七、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至作谚,卻和暖如春三娩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妹懒。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工雀监, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓会前,卻偏偏與公主長得像好乐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子瓦宜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評論 2 361

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