Android Jetpack- paging的基本使用

??樓主最近在學(xué)習(xí)Google爸爸最新的組件--Jetpack窿撬,說(shuō)實(shí)話蔗草,看文檔誰(shuí)都看得懂咒彤,但是動(dòng)起手來(lái)就徹底懵逼。就比如說(shuō)蕉世,今天要介紹的paging的基本使用蔼紧,樓主就踩了兩天的坑。這個(gè)過(guò)程中比較麻煩的一點(diǎn)就是狠轻,官方文檔和網(wǎng)上大部分資料都是用kotlin語(yǔ)言寫的奸例,可惜我不懂kotlin,只會(huì)寫一點(diǎn)小Java向楼。
??在閱讀本文之前查吊,我默認(rèn)大家都了解jetpack是什么。因此本文不再介紹jetpack湖蜕,注重介紹paging怎么使用逻卖,本文會(huì)大量的貼出Demo代碼,供大家理解和參考昭抒,同時(shí)也會(huì)介紹這其中我踩的坑评也。
??本文參考資料:

  1. Paging library overview Part of Android Jetpack
  2. Paging library data components and considerations

??在閱讀本文之前,最好是閱讀過(guò)上面兩篇官方文檔灭返。其中第一篇介紹的是paging的基本使用盗迟,第二篇是對(duì)第一篇的補(bǔ)充,主要介紹怎么自定義DataSource熙含。

1. Demo概述

??在正式介紹paging 的使用之前罚缕,我們先來(lái)看看最終的效果是怎么樣的。


??效果是非常的簡(jiǎn)單怎静,就是有一個(gè)RecyclerView在顯示數(shù)據(jù)邮弹,然后我們向下滑動(dòng)黔衡,滑動(dòng)到一定位置,paging會(huì)自動(dòng)請(qǐng)求數(shù)據(jù)腌乡,這也就是分頁(yè)請(qǐng)求的效果.paging的目的就是實(shí)現(xiàn)分頁(yè)加載盟劫,之前我們都是自己實(shí)現(xiàn)分頁(yè),這次是Google爸爸為我們實(shí)現(xiàn)导饲。不得不說(shuō)捞高,jetpack真香!

2. 準(zhǔn)備components

??要想使用paging渣锦,我們得準(zhǔn)備幾個(gè)東西(當(dāng)然硝岗,導(dǎo)依賴這種基本操作肯定是必須的,我就不介紹了哈)袋毙。

  1. DataSource:顧名思義型檀,數(shù)據(jù)源,獲取數(shù)據(jù)是通過(guò)它實(shí)現(xiàn)的听盖。
  2. PagedList: 數(shù)據(jù)源獲取的數(shù)據(jù)最終靠PagedList來(lái)承載胀溺。對(duì)于PagedList,我們可以這樣來(lái)理解,它就是一頁(yè)數(shù)據(jù)的集合皆看。每請(qǐng)求一頁(yè)仓坞,就是新的一個(gè)PagedList對(duì)象。
  3. PagedListAdapter:這個(gè)Adapter就是一個(gè)RecyclerViewAdapter腰吟。不過(guò)我們?cè)谑褂?code>paging實(shí)現(xiàn)RecyclerView的分頁(yè)加載效果无埃,不能直接繼承RecyclerViewAdapter,而是需要繼承PagedListAdapter毛雇。

3. 自定義DataSource

??說(shuō)實(shí)話嫉称,自定義DataSource沒(méi)有什么難度,但是我為什么要單獨(dú)提出來(lái)呢灵疮?首先官方文檔上织阅,實(shí)現(xiàn)的是ItemKeyedDataSource,而我這里實(shí)現(xiàn)的是PositionalDataSource。雖然差別不是很大震捣,但是還是有點(diǎn)區(qū)別荔棉,因?yàn)樵谶@里我踩過(guò)兩個(gè)坑。我相信蒿赢,通過(guò)這兩個(gè)坑江耀,以后遇到類似的問(wèn)題,會(huì)找到解決思路诉植。
??在貼代碼之前,我們先來(lái)看看實(shí)現(xiàn)PositionalDataSource需要注意的地方昵观。
??首先晾腔,我們得了解一下需要實(shí)現(xiàn)的兩個(gè)方法:

方法名 含義
loadInitial 加載初始化數(shù)據(jù)舌稀,可以這么來(lái)理解,加載的是第一頁(yè)的數(shù)據(jù)灼擂。形象的說(shuō)壁查,當(dāng)我們第一次打開(kāi)頁(yè)面,需要回調(diào)此方法來(lái)獲取數(shù)據(jù)剔应。
loadRange 當(dāng)有了初始化數(shù)據(jù)之后睡腿,滑動(dòng)的時(shí)候如果需要加載數(shù)據(jù)的話,會(huì)調(diào)用此方法峻贮。

??現(xiàn)在席怪,我們來(lái)看看最終實(shí)現(xiàn)的代碼。

public class ConcertDataSource extends PositionalDataSource<Concert> {

    private final Retrofit mRetrofit = new Retrofit.Builder()
            .baseUrl("http://api.apiopen.top/")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    private final Service mService;

    public ConcertDataSource() {
        mService = mRetrofit.create(Service.class);
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams params, @NonNull final LoadInitialCallback<Concert> callback) {
//        fetchItem(0, params.pageSize, new Callback() {
//            @Override
//            public void onResult(Bean value) {
//                callback.onResult(value.getResult(), 0, 2000);
//            }
//        });
        callback.onResult(fetchItems(0, 20), 0, 2000);
    }

    @Override
    public void loadRange(@NonNull LoadRangeParams params, @NonNull final LoadRangeCallback<Concert> callback) {
//        fetchItem(params.startPosition, params.loadSize, new Callback() {
//            @Override
//            public void onResult(Bean value) {
//                callback.onResult(value.getResult());
//            }
//        });
        callback.onResult(fetchItems(params.startPosition, params.loadSize));
    }

    private List<Concert> fetchItems(int startPosition, int pageSize) {
        List<Concert> list = new ArrayList<>();
        for (int i = startPosition; i < startPosition + pageSize; i++) {
            Concert concert = new Concert();
            concert.setAuthor("author = " + i);
            concert.setContent("content = " + i);
            concert.setTitle("title = " + i);
            list.add(concert);
        }
        return list;
    }

    private void fetchItem(final int startPosition, int size, final Callback callback) {
        mService.getCall(startPosition, size).subscribe(new Observer<Bean>() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(Bean value) {
                callback.onResult(value);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onComplete() {

            }
        });
    }

    private interface Callback {

        void onResult(Bean value);
    }
}

??然后纤控,我結(jié)合上面的代碼來(lái)介紹一下我踩過(guò)的兩個(gè)坑(其實(shí)這兩個(gè)坑可以歸并為一個(gè)坑挂捻,因?yàn)槎际且粋€(gè)原因?qū)е?。
??我先來(lái)描述我之前我遇到的問(wèn)題船万,在第一次打開(kāi)界面時(shí)刻撒,我發(fā)現(xiàn),RecyclerView沒(méi)有顯示任何的數(shù)據(jù)耿导,同時(shí)經(jīng)過(guò)debug声怔,發(fā)現(xiàn)loadInitial方法被回調(diào)到的,但是最終Adapter里面的itemCount為0舱呻。這是什么原因?qū)е碌哪卮谆穑课覀儊?lái)看看現(xiàn)在的loadInitial方法的實(shí)現(xiàn):

    @Override
    public void loadInitial(@NonNull LoadInitialParams params, @NonNull final LoadInitialCallback<Concert> callback) {
//        fetchItem(0, params.pageSize, new Callback() {
//            @Override
//            public void onResult(Bean value) {
//                callback.onResult(value.getResult(), 0, 2000);
//            }
//        });
        callback.onResult(fetchItems(0, 20), 0, 2000);
    }

??我們通過(guò)LoadInitialCallbackonResult方法一共傳遞了三個(gè)參數(shù),這三個(gè)參數(shù)分別表示什么意思狮荔?我們先來(lái)LoadInitialCallback這個(gè)類吧胎撇。

    public abstract static class LoadInitialCallback<T> {
        public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
        public abstract void onResult(@NonNull List<T> data, int position);
    }

??LoadInitialCallback一共有兩個(gè)onResult方法。而我這里出現(xiàn)了RecyclerView不顯示任何數(shù)據(jù)殖氏,是因?yàn)槲艺{(diào)用得是兩個(gè)參數(shù)的onResult方法晚树,導(dǎo)致了totalCount為0,所以顯示不出任何數(shù)據(jù)雅采。
??第二個(gè)問(wèn)題就是爵憎,數(shù)據(jù)顯示出來(lái)了,但是始終顯示一頁(yè)的數(shù)據(jù)婚瓜,也就是說(shuō)loadRange方法不會(huì)調(diào)用宝鼓。其實(shí)也是totalCount導(dǎo)致的,當(dāng)時(shí)我解決了第一個(gè)的問(wèn)題時(shí)巴刻,直接給totalCount傳遞的是Listsize(假設(shè)為N條)愚铡。也就是說(shuō),PositionalDataSource最終一共只會(huì)獲取N條的數(shù)據(jù),因?yàn)榈谝豁?yè)的大小恰好就等于N沥寥,所以不會(huì)調(diào)用loadRange方法去加載更多的數(shù)據(jù)碍舍。

3. 創(chuàng)建ViewModel

??相信比較熟悉JetPack的同學(xué)都知道,在Jetpack的架構(gòu)里面邑雅,推薦每個(gè)頁(yè)面持有一個(gè)ViewModel對(duì)象片橡,以保證數(shù)據(jù)的正確性和避免其他的問(wèn)題。本文就不介紹ViewModel淮野,不太了解的同學(xué)可以看一下的官方文檔捧书,這是鏈接: ViewModel Overview Part of Android Jetpack
??之前骤星,我們已經(jīng)成功的自定義一個(gè)DataSource,現(xiàn)在我們來(lái)看看怎么使用ViewModelDataSource連接起來(lái)经瓷。
??首先,我們得創(chuàng)建工廠類妈踊,用來(lái)創(chuàng)建DataSource對(duì)象了嚎,這在Jetpack架構(gòu)里面是基本需要。

public class ConcertFactory extends DataSource.Factory<Integer, Concert> {

    private MutableLiveData<ConcertDataSource> mSourceLiveData =
            new MutableLiveData<>();

    @Override
    public DataSource<Integer, Concert> create() {
        ConcertDataSource concertDataSource = new ConcertDataSource();
        mSourceLiveData.postValue(concertDataSource);
        return concertDataSource;
    }
}

??至于為什么需要一個(gè)MutableLiveDataDataSource包裝一下廊营,這個(gè)我也不是很了解歪泳,官方文檔就是這么寫的(可能是讓DataSource監(jiān)聽(tīng)Activity或者Fragment的生命周期,以保證只在頁(yè)面活躍狀態(tài)(onStart或者onResume)獲取數(shù)據(jù))露筒。
??其次呐伞,就是創(chuàng)建對(duì)應(yīng)的ViewModel:

public class ConcertViewModel extends ViewModel {

    private final LiveData<PagedList<Concert>> convertList;
    private DataSource<Integer, Concert> concertDataSource;

    public ConcertViewModel() {
        ConcertFactory concertFactory = new ConcertFactory();
        concertDataSource = concertFactory.create();
        convertList = new LivePagedListBuilder<>(concertFactory, 20).build();
    }

    public void invalidateDataSource() {
        concertDataSource.invalidate();
    }

    public LiveData<PagedList<Concert>> getConvertList() {
        return convertList;
    }
}

4. RecyclerView

??首先,我們來(lái)看一下Adapter的定義:

public class RecyclerAdapter extends PagedListAdapter<Concert, RecyclerAdapter.RecyclerViewHolder> {


    protected RecyclerAdapter() {
        super(DIFF_CALLBACK);
    }

    @NonNull
    @Override
    public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recyclerview, parent, false);
        return new RecyclerViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.mTitleTextView.setText(concert.getTitle());
            holder.mAuthorTextView.setText(concert.getAuthor());
            holder.mContentTextView.setText(concert.getContent());
        }
    }

    public static class RecyclerViewHolder extends RecyclerView.ViewHolder {

        TextView mTitleTextView;
        TextView mAuthorTextView;
        TextView mContentTextView;


        public RecyclerViewHolder(View itemView) {
            super(itemView);
            mTitleTextView = itemView.findViewById(R.id.title);
            mAuthorTextView = itemView.findViewById(R.id.author);
            mContentTextView = itemView.findViewById(R.id.content);
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
                @Override
                public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
                    return oldConcert.getTitle().equals(newConcert.getTitle());
                }

                @Override
                public boolean areContentsTheSame(Concert oldConcert,
                                                  Concert newConcert) {
                    return oldConcert.equals(newConcert);
                }
            };
}

??在使用PagedListAdapter時(shí)慎式,我們需要注意的是伶氢,PagedListAdapter內(nèi)部默認(rèn)實(shí)現(xiàn)DiffUtil來(lái)進(jìn)行數(shù)據(jù)的差量計(jì)算,所以我們?cè)跇?gòu)造方法里面?zhèn)鬟f一個(gè)DiffUtil.ItemCallback。至于DiffUtil是什么瘪吏,以及它內(nèi)部的算法實(shí)現(xiàn)癣防,我們這里就不介紹,有興趣的同學(xué)可以關(guān)注樓主的RecyclerView源碼分析系列文章掌眠,在不久的將來(lái)蕾盯,我會(huì)詳細(xì)的分析它的內(nèi)部實(shí)現(xiàn)。
??然后蓝丙,我們?cè)賮?lái)看看Activity里面是怎么使用的:

public class MainActivity extends AppCompatActivity {


    private RecyclerView mRecyclerView;
    private RecyclerAdapter adapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = findViewById(R.id.recyclerView);
        adapter = new RecyclerAdapter();
        ConcertViewModel viewModel =
                ViewModelProviders.of(this).get(ConcertViewModel.class);
        viewModel.getConvertList().observe(this, concerts -> adapter.submitList(concerts));
        mRecyclerView.setAdapter(adapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        mRecyclerView.addItemDecoration(new SimpleDividerDecoration(this));
    }
}

??整個(gè)結(jié)構(gòu)就這樣的级遭,是不是非常簡(jiǎn)單?不過(guò)渺尘,到這里可能有人可能有疑問(wèn)挫鸽,我就實(shí)現(xiàn)了一個(gè)RecyclerView加載更多的數(shù)據(jù),你就讓我定義這么多的類鸥跟,是不是有點(diǎn)小題大做呢丢郊?
??其實(shí)不然,Jetpack是Google官方推薦的一種App架構(gòu)方案,在這種結(jié)構(gòu)下蚂夕,會(huì)避免很多的問(wèn)題迅诬,比如說(shuō)內(nèi)存泄漏和空指針異常等等。之所以使用這種方案來(lái)架構(gòu)App婿牍,不是我們吃飽撐的沒(méi)事干,而是為了解決傳統(tǒng)架構(gòu)方案下不能解決的問(wèn)題惩歉。當(dāng)然可能還有其他的第三方解決方案等脂,但是肯定不比Jetpack權(quán)威,畢竟Google爸爸親自設(shè)計(jì)的撑蚌。

5. 源碼

??為了讓大家更好理解paging的使用上遥,我將我的代碼上傳到github,供大家參考争涌,鏈接:PagingDemo粉楚。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亮垫,隨后出現(xiàn)的幾起案子模软,更是在濱河造成了極大的恐慌,老刑警劉巖饮潦,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燃异,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡继蜡,警方通過(guò)查閱死者的電腦和手機(jī)回俐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)稀并,“玉大人仅颇,你說(shuō)我怎么就攤上這事〉饩伲” “怎么了忘瓦?”我有些...
    開(kāi)封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)殴俱。 經(jīng)常有香客問(wèn)我政冻,道長(zhǎng),這世上最難降的妖魔是什么线欲? 我笑而不...
    開(kāi)封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任明场,我火速辦了婚禮,結(jié)果婚禮上李丰,老公的妹妹穿的比我還像新娘苦锨。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布舟舒。 她就那樣靜靜地躺著拉庶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秃励。 梳的紋絲不亂的頭發(fā)上氏仗,一...
    開(kāi)封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音夺鲜,去河邊找鬼皆尔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛币励,可吹牛的內(nèi)容都是我干的慷蠕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼食呻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼流炕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起仅胞,我...
    開(kāi)封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤每辟,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后饼问,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體影兽,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年莱革,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了峻堰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盅视,死狀恐怖捐名,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闹击,我是刑警寧澤镶蹋,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站赏半,受9級(jí)特大地震影響贺归,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜断箫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一拂酣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仲义,春花似錦婶熬、人聲如沸剑勾。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)虽另。三九已至,卻和暖如春饺谬,著一層夾襖步出監(jiān)牢的瞬間捂刺,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工募寨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叠萍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓绪商,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辅鲸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子格郁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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