怎麼學習谷歌最新的元件--Jetpack?一文介紹paging的基本使用

yilian發表於2020-03-25

樓主最近在學習Google爸爸最新的元件– Jetpack,說實話,看文件誰都看得懂,但是動起手來就徹底懵逼。就比如說,今天要介紹的 paging的基本使用,樓主就踩了兩天的坑。這個過程中比較麻煩的一點就是,官方文件和網上大部分資料都是用kotlin語言寫的,可惜我不懂kotlin,只會寫一點小Java。
  在閱讀本文之前,我預設大家都瞭解 jetpack是什麼。因此本文不再介紹jetpack,注重介紹paging怎麼使用,本文會大量的貼出Demo程式碼,供大家理解和參考,同時也會介紹這其中我踩的坑。
  本文參考資料:

在閱讀本文之前,最好是閱讀過上面兩篇官方文件。其中第一篇介紹的是 paging的基本使用,第二篇是對第一篇的補充,主要介紹怎麼自定義 DataSource

1. Demo概述

在正式介紹paging 的使用之前,我們先來看看最終的效果是怎麼樣的。

效果是非常的簡單,就是有一個 RecyclerView在顯示資料,然後我們向下滑動,滑動到一定位置, paging會自動請求資料,這也就是分頁請求的效果. paging的目的就是實現分頁載入,之前我們都是自己實現分頁,這次是Google爸爸為我們實現。不得不說, jetpack真香!

2. 準備components

要想使用 paging,我們得準備幾個東西(當然,導依賴這種基本操作肯定是必須的,我就不介紹了哈)。

  1. DataSource:顧名思義,資料來源,獲取資料是透過它實現的。
  2. PagedList: 資料來源獲取的資料最終靠 PagedList來承載。對於 PagedList,我們可以這樣來理解,它就是一頁資料的集合。每請求一頁,就是新的一個 PagedList物件。
  3. PagedListAdapter:這個Adapter就是一個 RecyclerViewAdapter。不過我們在使用 paging實現 RecyclerView的分頁載入效果,不能直接繼承 RecyclerViewAdapter,而是需要繼承 PagedListAdapter

3. 自定義DataSource

說實話,自定義 DataSource沒有什麼難度,但是我為什麼要單獨提出來呢?首先官方文件上,實現的是 ItemKeyedDataSource,而我這裡實現的是 PositionalDataSource。雖然差別不是很大,但是還是有點區別,因為在這裡我踩過兩個坑。我相信,透過這兩個坑,以後遇到類似的問題,會找到解決思路。
  在貼程式碼之前,我們先來看看實現 PositionalDataSource需要注意的地方。
  首先,我們得了解一下需要實現的兩個方法:

方法名 含義
loadInitial 載入初始化資料,可以這麼來理解,載入的是第一頁的資料。形象的說,當我們第一次開啟頁面,需要回撥此方法來獲取資料。
loadRange 當有了初始化資料之後,滑動的時候如果需要載入資料的話,會呼叫此方法。

現在,我們來看看最終實現的程式碼。

  public class ConcertDataSource extends PositionalDataSource<Concert> {
      private final Retrofit mRetrofit = new Retrofit.Builder()
              .baseUrl(")
              .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);
      }
  }

然後,我結合上面的程式碼來介紹一下我踩過的兩個坑(其實這兩個坑可以歸併為一個坑,因為都是一個原因導致)。
  我先來描述我之前我遇到的問題,在第一次開啟介面時,我發現, RecyclerView沒有顯示任何的資料,同時經過debug,發現 loadInitial方法被回撥到的,但是最終 Adapter裡面的 itemCount為0。這是什麼原因導致的呢?我們來看看現在的 loadInitial方法的實現:

      @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);
      }

我們透過 LoadInitialCallbackonResult方法一共傳遞了三個引數,這三個引數分別表示什麼意思?我們先來 LoadInitialCallback這個類吧。

    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一共有兩個 onResult方法。而我這裡出現了 RecyclerView不顯示任何資料,是因為我呼叫得是兩個引數的 onResult方法,導致了 totalCount為0,所以顯示不出任何資料。
  第二個問題就是,資料顯示出來了,但是始終顯示一頁的資料,也就是說 loadRange方法不會呼叫。其實也是 totalCount導致的,當時我解決了第一個的問題時,直接給 totalCount傳遞的是 Listsize(假設為N條)。也就是說, PositionalDataSource最終一共只會獲取N條的資料,因為第一頁的大小恰好就等於N,所以不會呼叫 loadRange方法去載入更多的資料。

3. 建立ViewModel

相信比較熟悉 JetPack的同學都知道,在 Jetpack的架構裡面,推薦每個頁面持有一個 ViewModel物件,以保證資料的正確性和避免其他的問題。本文就不介紹 ViewModel,不太瞭解的同學可以看一下的官方文件,這是連結:  ViewModel Overview Part of Android Jetpack
  之前,我們已經成功的自定義一個 DataSource,現在我們來看看怎麼使用 ViewModelDataSource連線起來。
  首先,我們得建立工廠類,用來建立 DataSource物件,這在 Jetpack架構裡面是基本需要。

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

至於為什麼需要一個 MutableLiveDataDataSource包裝一下,這個我也不是很瞭解,官方文件就是這麼寫的(可能是讓 DataSource監聽 Activity或者 Fragment的生命週期,以保證只在頁面活躍狀態( onStart或者 onResume)獲取資料)。
  其次,就是建立對應的 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

首先,我們來看一下 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時,我們需要注意的是, PagedListAdapter內部預設實現 DiffUtil來進行資料的差量計算,所以我們在構造方法裡面傳遞一個 DiffUtil.ItemCallback。至於 DiffUtil是什麼,以及它內部的演算法實現,我們這裡就不介紹,有興趣的同學可以關注樓主的 RecyclerView原始碼分析系列文章,在不久的將來,我會詳細的分析它的內部實現。
  然後,我們再來看看 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));
      }
  }

整個結構就這樣的,是不是非常簡單?不過,到這裡可能有人可能有疑問,我就實現了一個 RecyclerView載入更多的資料,你就讓我定義這麼多的類,是不是有點小題大做呢?
  其實不然, Jetpack是Google官方推薦的一種App架構方案,在這種結構下,會避免很多的問題,比如說記憶體洩漏和空指標異常等等。之所以使用這種方案來架構App,不是我們吃飽撐的沒事幹,而是為了解決傳統架構方案下不能解決的問題。當然可能還有其他的第三方解決方案,但是肯定不比 Jetpack權威,畢竟Google爸爸親自設計的。

5. 原始碼

為了讓大家更好理解 paging的使用,我將我的程式碼上傳到github,供大家參考,連結: 。

6.最後

附上我的Android核心技術學習大綱,獲取相關內容來我的GitHub一起玩耍:

最近不是面試季嘛,再給大家一個福利:【2017-2019位元組跳動面試真題解析&簡歷製作PDF模板】

針對位元組跳動的面試題整理的,進行了分類,循序漸進,由基礎到深入,由易到簡。

將內容整理成了五個章節、計算機基礎面試題、資料結構和演算法面試題、Java面試題、Android面試題、其他擴充套件面試題、非技術面試題總共五個章節354頁。

獲取方式:關注我看個人介紹,或直接  免費領取

每個問題都附上1個標準參考答案,都是反覆摸索消化(真心花了很多時間),覺得寫的比較好的文章作為答案。

來節省大家自己去搜尋的時間,把時間用在正確的東西上。。

還整理了全套簡歷製作、春招困惑、HR面試等問題解析參考建議,都是福利分享給大家。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2682582/,如需轉載,請註明出處,否則將追究法律責任。

相關文章