在Android應用開發中,咱們常常須要以列表的方式來展現大量的數據,這些數據可能來自網路,也能夠來自本地的數據庫。爲了不一次性加載大量的數據,對數據進行分頁就顯得頗有必要。分頁加載能夠根據須要對數據進行按需加載,在不影響用戶體驗的前提下,提高應用的性能。html
爲了方便開發者進行分頁處理,Google爲開發者提供了分頁組件(Paging),藉助Paging組件開發者能夠輕鬆的加載和呈現大型數據集,同時在 RecyclerView 中進行快速、無限滾動。而且,它能夠從本地存儲和/或網絡加載分頁數據,並讓開發者可以定義內容的加載方式,同時它還支持與Room、LiveData 和 RxJava組合使用。java
目前,Paging能夠支持3種架構類型,分別是網路、數據、網路和數據庫,架構的示意圖以下所示。android
在Android應用開發中,對網路數據進行分頁加載是一種比較常見的場景,也是咱們平時開發中遇到得最多的。不一樣公司對分頁機制所涉及的API接口一般會不同,但整體而言,能夠分爲3中。針對這些不一樣的分類,Paging提供了PageKeyedDataSource、PositionalDataSource和ItemKeyedDataSource。數據庫
除了網路外,數據源來源於數據庫的場景也很是多,若是已經掌握了對網路數據的分頁,那麼對數據庫的數據進行分頁天然十分簡單,只不過數據源的讀取方式不一樣而已。json
在這種場景中,咱們會對網路的數據進行緩存,而數據庫就是比較場景的一種數據持久化方式,好比聊天應用中。首先,咱們會利用數據庫對網路數據進行緩存,不過在這種場景下,咱們須要同時處理數據庫和網路兩個數據源,所以須要約定好網路和數據庫的數據處理邏輯。api
在正式使用Paging以前,咱們須要對Paging的工做流程有一個大體的瞭解。以下圖所示,是使用Paging須要經歷的幾個步驟。
如上圖所示,主要的步驟以下:緩存
使用Paging庫進行分頁加載時,須要用到幾個核心的類,分別是PagedListAdapter、PageList和DataSource。服務器
衆所周知,在Android列表開發中須要使用RecyclerView,而且須要配合自定義Adapter。PagedListAdapter繼承於RecyclerView.Adapter,這代表它也是一個RecyclerView.Adapter,而且擴展了RecyclerView.Adapter的支持異步差分更新功能。網絡
PageList是用於通知DataSource什麼時候獲取數據,以及如何獲取數據。好比,什麼時候獲取第一頁數據,以及什麼時候開始加載數據等待。而且,DataSource數據源都將經過PageList設置給PagedListAdapter。架構
DataSource主要用於執行數據的加載操做,而且數據的載入須要在子線程中進行,不然會形成主線程的阻塞。DataSource的來源能夠是網路,也能夠是本地的數據庫,如Room。根據分頁機制的不一樣,DataSource能夠有3種來源,分別是PageKeyedDataSource、PositionalDataSource和ItemKeyedDataSource。
首先,在app的build.gradle文件中添加Paging組件庫的依賴,以下所示。
dependencies { //網路請求庫 implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.okhttp3:okhttp:4.8.0' implementation 'com.squareup.okio:okio:2.7.0' //Paging def paging_version = "2.1.0" implementation "androidx.paging:paging-runtime:$paging_version" }
在Android開發中,數據一般來源於網路,咱們可使用retrofit完成網絡數據的請求。在獲取數據以前,咱們須要先新建一個數據實體類,主要用來存儲獲取的數據,以下所示是使用乾貨集中營的開源 Api 的數據的實體類。
public class DataBean { private int count; private boolean error; private List<ResultsBean> results; ...//省略get和set public static class ResultsBean { private String desc; private String ganhuo_id; private String publishedAt; private String readability; private String type; private String url; private String who; ... //省略get和set } }
而後,爲了完成網路請求,咱們須要按照Retrofit的使用方式新建一個Api,用於統一管理請求接口,以下所示。
public interface Api { //開源API:http://gank.io/api/search/query/listview/category/Android/count/10/page/1 @GET("api/search/query/listview/category/Android/count/10/page/{page}") Call<List<DataBean.ResultsBean>> getArticleList1(@Path("page") int page); }
而後,咱們對Retrofit進行一個簡單的封裝,而後用它完成網路請求,以下所示。
public class RetrofitClient { private static RetrofitClient instance; private Retrofit mRetrofit; private OkHttpClient getClient(){ OkHttpClient.Builder builder = new OkHttpClient().newBuilder() .connectTimeout(30, TimeUnit.SECONDS)//設置超時時間 .readTimeout(10, TimeUnit.SECONDS)//設置讀取超時時間 .writeTimeout(10, TimeUnit.SECONDS);//設置寫入超時時間 return builder.build(); } public RetrofitClient() { mRetrofit=new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .client(getClient()) .build(); } public static RetrofitClient getInstance() { if (instance==null){ instance=new RetrofitClient(); } return instance; } public <T> T createApi(Class<T> cls){ T t=mRetrofit.create(cls); return t; } }
若是使用的是網路數據,使用Paging進行分頁加載時須要自定義DataSource。前面說過,DataSource有3個抽象類,分別是PageKeyedDataSource、PositionalDataSource和ItemKeyedDataSource,所以實際使用時須要繼承他們。
因爲此處加載的是網絡數據,因此使用PageKeyedDataSource更合適,咱們新建一個繼承自PageKeyedDataSource的自定義DataSource,以下所示。
public class PagingDataSource extends PageKeyedDataSource<String, DataBean.ResultsBean> { private static final String TAG = PagingDataSource.class.getSimpleName(); private int mPage = 1; @Override public void loadInitial(@NonNull LoadInitialParams<String> params, @NonNull final LoadInitialCallback<String, DataBean.ResultsBean> callback) { Api api = RetrofitClient.getInstance().createApi(Api.class); Call<List<DataBean.ResultsBean>> call = api.getArticleList1(mPage); call.enqueue(new Callback<List<DataBean.ResultsBean>>() { @Override public void onResponse(Call<List<DataBean.ResultsBean>> call, Response<List<DataBean.ResultsBean>> response) { if (response.isSuccessful() && response.code() == 200) { List<DataBean.ResultsBean> data = response.body(); callback.onResult(data, "before", "after"); } } @Override public void onFailure(Call<List<DataBean.ResultsBean>> call, Throwable t) { Log.e(TAG, "--onFailure-->" + t.getMessage()); } }); } //加載上一頁 @Override public void loadBefore(@NonNull LoadParams<String> params, @NonNull LoadCallback<String, DataBean.ResultsBean> callback) { Log.i(TAG, "--loadBefore-->" + params.key); } //加載下一頁 @Override public void loadAfter(@NonNull final LoadParams<String> params, @NonNull final LoadCallback<String, DataBean.ResultsBean> callback) { mPage++; Api api = RetrofitClient.getInstance().createApi(Api.class); Call<List<DataBean.ResultsBean>> call = api.getArticleList1(mPage); call.enqueue(new Callback<List<DataBean.ResultsBean>>() { @Override public void onResponse(Call<List<DataBean.ResultsBean>> call, Response<List<DataBean.ResultsBean>> response) { if (response.isSuccessful() && response.code() == 200) { List<DataBean.ResultsBean> data = response.body(); callback.onResult(data, params.key); } } @Override public void onFailure(Call<List<DataBean.ResultsBean>> call, Throwable t) { Log.i(TAG, "--onFailure-->" + t.getMessage()); } }); } }
在上面的代碼中,PageKeyedDataSource須要重寫三個方法。
DataSource建立好了,再建立一個DataSource.Factory,返回對應的DataSource實例,以下所示。
public class PagingDataSourceFactory extends DataSource.Factory<String, DataBean.ResultsBean> { @NonNull @Override public DataSource<String, DataBean.ResultsBean> create() { PagingDataSource dataSource = new PagingDataSource(); return dataSource; } }
在Jetpack的架構裏面,官方推薦每一個頁面持有一個ViewModel對象,以保證數據的正確性以及避免其餘的問題產生。
在在 ViewModel 中建立 PagedList.Config 並進行分頁參數配置,建立 DataSource 工廠對象,最終生成支持分頁的 LiveData 數據。要想建立LiveData,須要先建立一個LivePagedListBuilder,LivePagedListbuilder有設分頁數量和配置參數兩種方法,以下所示。
public class PagingViewModel extends ViewModel { private int pageSize = 20; //PagedList配置 private PagedList.Config config = new PagedList.Config.Builder() .setInitialLoadSizeHint(pageSize)//設置首次加載的數量 .setPageSize(pageSize)//設置每頁加載的數量 .setPrefetchDistance(2)//設置距離每頁最後數據項來時預加載下一頁數據 .setEnablePlaceholders(false)//設置是否啓用UI佔用符 .build(); //DataSource.Factory private DataSource.Factory<String,DataBean.ResultsBean> factory = new PagingDataSourceFactory(); //LiveData private LiveData<PagedList<DataBean.ResultsBean>> mPagedList = new LivePagedListBuilder<>(factory, config) .build(); public LiveData<PagedList<DataBean.ResultsBean>> getPagedList() { return mPagedList; } }
上面代碼中,咱們提到了佔位符,佔位符的做用是在數據完成渲染以前,向用戶顯示的默認視圖效果。佔位符具備如下優勢:
不過,在添加對佔位符的支持以前,請注意如下前提條件:
PagedListAdapter是一個特殊的RecyclerView的RecyclerAdapter,使用方法也和RecyclerAdapter的使用方式相似,以下所示。
public class PagingAdapter extends PagedListAdapter<DataBean.ResultsBean, PagingAdapter.ViewHolder> { public PagingAdapter() { super(itemCallback); } @NonNull @Override public PagingAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycleview, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull PagingAdapter.ViewHolder holder, int position) { DataBean.ResultsBean bean = getItem(position); if (bean != null) { holder.desc.setText(bean.getDesc()); holder.time.setText(bean.getPublishedAt()); holder.type.setText(bean.getType()); holder.auth.setText(bean.getWho()); } } public class ViewHolder extends RecyclerView.ViewHolder{ TextView desc; TextView time; TextView type; TextView auth; public ViewHolder(View itemView) { super(itemView); desc = itemView.findViewById(R.id.desc); time = itemView.findViewById(R.id.time); type = itemView.findViewById(R.id.type); auth = itemView.findViewById(R.id.auth); } } private static DiffUtil.ItemCallback<DataBean.ResultsBean> itemCallback = new DiffUtil.ItemCallback<DataBean.ResultsBean>() { @Override public boolean areItemsTheSame(@NonNull DataBean.ResultsBean oldItem, @NonNull DataBean.ResultsBean newItem) { return oldItem.getGanhuo_id().equals(newItem.getGanhuo_id()); } @SuppressLint("DiffUtilEquals") @Override public boolean areContentsTheSame(@NonNull DataBean.ResultsBean oldItem, @NonNull DataBean.ResultsBean newItem) { return oldItem.equals(newItem); } }; }
在使用PagedListAdapter時,PagedListAdapter內部默認實現DiffUtil來進行數據的差量計算,因此咱們在構造方法裏面傳遞一個DiffUtil.ItemCallback。
通過上面的處理後,接下來只須要在Activity中進行數據的請求和綁定便可,以下所示。
public class MainActivity extends AppCompatActivity { private final String TAG = "MainActivity"; private ActivityMainBinding activityMainBinding; private PagingViewModel mViewModel; private PagingAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); initRecyclerView(); // getData(); } private void initRecyclerView() { adapter = new PagingAdapter(); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); activityMainBinding.recycle.setLayoutManager(layoutManager); activityMainBinding.recycle.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL)); activityMainBinding.recycle.setAdapter(adapter); ViewModelProvider mViewModelProvider = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())); mViewModel = mViewModelProvider.get(PagingViewModel.class); } public void getData() { mViewModel.getPagedList().observe(this, new Observer<PagedList<DataBean.ResultsBean>>() { @Override public void onChanged(PagedList<DataBean.ResultsBean> dataBeans) { adapter.submitList(dataBeans); } }); } }
咱們使用 LiveData 監聽加載的數據,而後使用 sumbitList 將數據提交給 PagedListAdapter,PagedListAdapter會在後臺線程中對比新舊數據的差別,最後更新 RecyclerView。
Paging是JetPack框架提供的一個分頁庫,它能夠幫助開發者從本地存儲或經過網絡加載顯示數據,不過因爲歷史緣由,早期的Paging存在各類使用上的問題,所以Android在後面提供了Paging3用來替換早期的Paging2。相比Paging2,Paging3有以下一些優勢。
在 Paging3 以前,Paging提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 這三個類來進行數據獲取的操做。
在 Paging3 以後,ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 合併爲一個 PagingSource,全部舊 API 加載方法被合併到 PagingSource 中的單個 load() 方法中,以下所示。
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
除此以外,變化的內容還包括:
在正式學習Paging3以前,咱們須要弄清楚幾個重要的概念:
PagingData
> 的類,實現數據加載完成的回調。總的來講,使用Paging3加載網絡數據的流程是:PagingSource 和 RemoteMediator 充當數據源的角色,ViewModel 使用 Pager 中提供的 Flow<PagingData
> 監聽數據刷新,每當 RecyclerView 即將滾動到底部的時候,就會加載新的數據,最後再使用PagingAdapter 展現數據。
下面是Android官方推薦的接入 Paging3的應用架構圖。
能夠發現,使用Paging3實現數據分頁時主要包含3個對象:
#### 數據倉庫層Repository
Repository層主要使用PagingSource這個分頁組件來實現,每一個PagingSource對象都對應一個數據源,以及該如何從該數據源中查找數據,PagingSource能夠從任何單個數據源好比網絡或者數據庫中查找數據。
Repository層還有另外一個分頁組件可使用RemoteMediator,它是一個分層數據源,好比有本地數據庫緩存的網絡數據源。
Repository最終返回一個異步流包裹的PagingDataFlow<PagingData<Value
>>,PagingData存儲了數據結果,最終可使用它將數據跟UI界面關聯起來。ViewModel通常都使用LiveData來跟UI層交互,Flow的擴展函數能夠直接轉換成一個LiveData可觀察對象。
UI層其實就是Activity/Fragment等視圖層,主要的做用是給RecycleView設置Adapter,給Adater設置數據。
首先,在app的build.gradle文件中添加Paging3組件庫的依賴,以下所示。
dependencies { ... //Paging3 def paging_version = "3.0.0-alpha11" implementation "androidx.paging:paging-runtime:$paging_version" }
Paging 2提供了三種類型的 PageSource,開發者須要根據使用場景去進行選擇。而Paging 3對數據源進行了統一處理,開發時只須要繼承 PagingSource 便可。
Paging 3的數據源能夠是PagingSource,也能夠是RemoteMediator,它們的區別以下。
那實際使用時,如何進行選擇呢?PagingSource主要用於加載有限的數據集,而RemoteMediator則主要用來加載網絡分頁數據,實際使用時須要結合 PagingSource 實現保存更多數據操做並映射到 UI 上。
下面以WanAndroid的接口爲例,接口地址爲:https://www.wanandroid.com/article/list/1/json,數據源的代碼以下。
public class Paging3DataSource extends PagingSource<Integer, ArticleBean.DataBean.DatasBean> { @Nullable @Override public Object load(@NotNull LoadParams<Integer> params, @NotNull Continuation<? super LoadResult<Integer, ArticleBean.DataBean.DatasBean>> continuation) { int page = 0; if(params.getKey()!=null){ page=params.getKey(); } //獲取網絡數據 ArticleBean result = (ArticleBean) RetrofitClient.getInstance().getApi().getArticleList(page); //須要加載的數據 List<ArticleBean.DataBean.DatasBean> datas= result.getData().getDatas(); //若是能夠往上加載更多就設置該參數,不然不設置 String prevKey=null; //加載下一頁的key 若是傳null就說明到底了 String nextKey=null; if(result.getData().getCurPage() == result.getData().getPageCount()){ nextKey=null; }else { nextKey=String.valueOf(page+1); } return new LoadResult.Page<>(datas,prevKey,nextKey); } }
在上面的代碼中,自定義的PagingSource須要繼承自PagingSource,須要傳入兩個泛型,第一個表示下一頁數據的加載方式,另外一個是返回數據的實體類。同時,自定義的PagingSource還須要重寫load方法來觸發異步加載,能夠看到它是一個用suspend修飾的掛起函數,能夠很方便的使用協程異步加載。
而load方法的參數LoadParams中有一個key值,能夠在加載下一頁數據時使用。返回值是一個LoadResult,出現異常調用LoadResult.Error(e),正常強開狀況下調用LoadResult.Page方法來設置從網絡或者數據庫獲取到的數據。
在實際應用開發中,咱們須要從網絡中獲取數據,而後再進行其餘業務操做。網絡請求通常會藉助Retrofit來實現,下面是使用Retrofit完成WanAndroid接口請求的簡單的封裝,代碼以下。
public class RetrofitClient { private static String BASE_URL="https://www.wanandroid.com/"; private static RetrofitClient instance; private Retrofit mRetrofit; private OkHttpClient getClient(){ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { @Override public void log(String message) { Log.i("RetrofitClient: ", message); } }); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(interceptor) .connectTimeout(60, TimeUnit.SECONDS) .build(); return client; } public RetrofitClient() { mRetrofit=new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client(getClient()) .build(); } public static RetrofitClient getInstance() { if (instance==null){ instance=new RetrofitClient(); } return instance; } public <T> T createApi(Class<T> cls){ T t=mRetrofit.create(cls); return t; } }
而後,在建立一個WanAndroidApi來管理全部的Api接口,以下所示。
public interface WanAndroidApi { @GET("article/list/{pageNum}/json") Call<ArticleBean> getArticleList(@Path("page") int page); }
分頁數據的容器被稱爲 PagingData,每次刷新數據時,都會建立一個 PagingData 的實例。若是要建立 PagingData 數據流,那麼須要建立一個 Pager 實例,並提供一個 PagingConfig 配置對象和一個能夠告訴 Pager 如何獲取您實現的 PagerSource 的實例的函數,以供 Pager 使用。
實際開發中,Repository返回的是一個異步流包裹的PagingDataFlow<PagingData<Value
>>,PagingData存儲了數據結果。而在MVVM中,咱們須要構建ViewModel來實現是LiveData和UI層交互,而ViewModel的Flow的擴展函數能夠將直接將PagingSource轉換成一個LiveData可觀察對象,代碼以下。
public class Paging3ViewModel extends ViewModel { PagingConfig pagingConfig = new PagingConfig(20, 3); public LiveData<PagingData<ArticleBean.DataBean.DatasBean>> getArticleData() { CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(this); Pager<Integer, ArticleBean.DataBean.DatasBean> pager = new Pager<>(pagingConfig, () -> new Paging3DataSource()); LiveData<PagingData<ArticleBean.DataBean.DatasBean>> cachedResult=PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope); return cachedResult; } }
和Paging2的Adapter使用步驟相似,在Paging3中建立Adapter須要繼承 PagingDataAdapter,而後提供 DiffUtil.ItemCallback<T
>,以下所示。
public class Paging3Adapter extends PagingDataAdapter<ArticleBean.DataBean.DatasBean, Paging3Adapter.ViewHolder> { public Paging3Adapter(@NotNull DiffUtil.ItemCallback<ArticleBean.DataBean.DatasBean> diffCallback) { super(itemCallback); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycleview, parent, false); return new Paging3Adapter.ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ArticleBean.DataBean.DatasBean bean = getItem(position); if (bean != null) { holder.desc.setText(bean.getDesc()); holder.time.setText(String.valueOf(bean.getPublishTime())); holder.type.setText(bean.getType()); holder.auth.setText(bean.getAuthor()); } } public static class ViewHolder extends RecyclerView.ViewHolder{ TextView desc; TextView time; TextView type; TextView auth; public ViewHolder(View itemView) { super(itemView); desc = itemView.findViewById(R.id.desc); time = itemView.findViewById(R.id.time); type = itemView.findViewById(R.id.type); auth = itemView.findViewById(R.id.auth); } } public static DiffUtil.ItemCallback<ArticleBean.DataBean.DatasBean> itemCallback = new DiffUtil.ItemCallback<ArticleBean.DataBean.DatasBean>() { @Override public boolean areItemsTheSame(@NonNull ArticleBean.DataBean.DatasBean oldItem, @NonNull ArticleBean.DataBean.DatasBean newItem) { return oldItem.getId()==newItem.getId(); } @SuppressLint("DiffUtilEquals") @Override public boolean areContentsTheSame(@NonNull ArticleBean.DataBean.DatasBean oldItem, @NonNull ArticleBean.DataBean.DatasBean newItem) { return oldItem.equals(newItem); } }; }
能夠發現,Paging3Adapter的代碼和普通的Adapter的代碼是差很少的,也是須要重寫onCreateViewHolder和onBindViewHolder兩個方法。
最後,咱們在Activity中使用RecyclerView展現獲取的數據便可,以下所示。
public class MainActivity extends AppCompatActivity { private final String TAG = "MainActivity"; private Paging3Adapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView( R.layout.activity_main); initRecyclerView(); } private void initRecyclerView() { adapter = new Paging3Adapter(); RecyclerView recyclerView = findViewById(R.id.recycle); recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL)); recyclerView.setAdapter(adapter); Paging3ViewModel viewModel=new ViewModelProvider(this).get(Paging3ViewModel.class); viewModel.getArticleData().observe(this, pagingData -> adapter.submitData(getLifecycle(),pagingData)); } }
除此以外,Paging3還支持添加 Header 和 Footer來實現上拉刷新和下拉加載更多的功能。