Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager
Android Jetpack组件(九)DataStore
Android Jetpack组件(十)App Startup
Android Jetpack组件(十一)Compose
首语
我们经常以列表的形式加载大量的数据,这些数据一次性加载处理,必须消耗大量的时间和数据流畅,因此便有了分页加载。应用开发过程中分页加载时很普遍的需求,它能节省数据流量,提升应用的性能。
Google为了方便开发者完成分页加载而推出了分页组件—Paging。为几种常见的分页机制提供了统一的解决方案。
优势
- 分页数据的内存中缓存。该功能可确保您的应用在处理分页数据时高效利用系统资源。
- 内置的请求重复信息删除功能,可确保您的应用高效利用网络带宽和系统资源。
- 可配置的
RecyclerView
适配器,会在用户滚动到已加载数据的末尾时自动请求数据。 - 对Kotlin协程和Flow以及LiveData和RxJava的一流支持。
- 内置对错误处理功能的支持,包括刷新和重试功能。
数据架构
Paging支持三种数据架构类型。
- 网络
对网络数据进行分页加载是最常见的需求。API接口通常不太一样,Paging提供了三种不同的方案,应对不同的分页机制。Paging不提供任务错误处理功能,发生错误后可重试网络请求。 - 数据库
数据库进行分页加载和网络类似,推荐使用Room数据库修改和插入数据。 - 网络+数据库
出于用户体验的考虑,我们会利用数据库对网络数据进行缓存,这时需要处理网络和数据库两个数据源,但是这样会让业务逻辑复杂,通常只采用单一数据源作为解决方案,从网络获取数据,直接缓存进数据库,列表直接从数据库中获取数据。
依赖
dependencies {
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version"
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}
核心类
Paging的工作原理主要涉及三个类。
- PagedListAdapter
RecyclerView.Adapter
基类,用于在RecyclerView
显示来自PagedList
的分页数据。 - PagedList
PagedList
负责通知DataSource
何时获取数据,如加载第一页、最后一页及加载数量等。从DataSource获取的数据将存储在PagedList
中。 - DataSource
DataSource
中执行具体的数据载入工作,数据载入需要在工作线程中进行。
DataSource
根据分页机制的不同,Paing为我们提供了三种DataSource。
- PositionalDataSource
适用于可通过任意位置加载数据,且目标数据源数量固定的情况。 - PageKeyedDataSource
适合数据源以“页”的方式进行请求的情况。如获取数据携带page
和pageSize
时。 - ItemKeyedDataSource
适用于当目标数据的下一页需要依赖上一页数据中的最后一个对象中的某个字段作为key的情况,如评论数据的接口携带参数since
和pageSize
。
使用
我们对三种DataSource进行分别使用来展示分页加载的效果。
PositionalDataSource
我们从网络获取数据,选取玩Android 开放API中的查看某个公众号历史数据接口来获取数据。
https://wanandroid.com/wxarticle/list/408/1/json
方法:GET
参数:
公众号 ID:拼接在 url 中,eg:405
公众号页码:拼接在url 中,eg:1
首先创建DataSource,继承自PositionalDataSource,获取网络数据。
public class ArticlesDataSource extends PositionalDataSource<Article> {
public static final int PAGE_SIZE=20;
/**
* 页面首次加载数据调用,在方法里调用接口,并通过callback.onResult()返回给PagedList
*/
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Article> callback) {
int start = 0;
//网络请求采用Retrofit
RetrofitClient.getInstance().getApi().articlesData(start).enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
callback.onResult(response.body().data.datas, start, response.body().data.total);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
Log.e("yhj", "loadInitial: "+t.getMessage());
}
});
}
/**
*负责每一页数据的加载,当第一页数据加载完成后,加载下一页数据的工作,通过callback.onResult()回传PagedList
*/
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Article> callback) {
RetrofitClient.getInstance().getApi().articlesData(params.startPosition/PAGE_SIZE).enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if(response.body()!=null){
callback.onResult(response.body().data.datas);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, Throwable t) {
Log.e("yhj", "loadRange: "+t.getMessage());
}
});
}
}
接下来创建一个Factory类,负责创建DataSource,并使用LiveData包装DataSource,暴露给ViewModel。
public class ArticlesDataSourceFactory extends DataSource.Factory<Integer, Article> {
private MutableLiveData<ArticlesDataSource> liveDataSource = new MutableLiveData<>();
@NonNull
@Override
public DataSource<Integer, Article> create() {
ArticlesDataSource articlesDataSource = new ArticlesDataSource();
liveDataSource.postValue(articlesDataSource);
return articlesDataSource;
}
}
在ViewModel中通过LivePagedListBuilder
创建和配置PagedList
,并使用LiveData包装PagedList,暴露给Activity
。
public class ArticlesViewModel extends ViewModel {
public LiveData<PagedList<Article>> articlePagedList;
PagedList.Config config = new PagedList.Config.Builder()
//设置控件占位,预留位置。默认为true,如果设置为true,需要在DataSource中callback.onResult()的totalCount设置总数,否则会崩溃
//使用此方法数据不宜太大,否则会消耗性能。
.setEnablePlaceholders(true)
//设置每页的大小
.setPageSize(ArticlesDataSource.PAGE_SIZE)
//设置距离底部多少条数据加载下一页数据
.setPrefetchDistance(3)
//设置首次加载的数据量,要求是PageSize的整数倍,默认为3倍。
.setInitialLoadSizeHint(ArticlesDataSource.PAGE_SIZE * 4)
//设置PagedList承受的最大数量,超过会有异常
.setMaxSize(65536 * ArticlesDataSource.PAGE_SIZE)
.build();
public ArticlesViewModel() {
articlePagedList = new LivePagedListBuilder<>(new ArticlesDataSourceFactory(), config).build();
}
}
列表数据展示通过Adapter来进行展示。
public class ArticlesAdapter extends PagedListAdapter<Article, ArticlesAdapter.ArticlesViewHolder> {
private Context context;
private ArticleItemBinding binding;
public ArticlesAdapter(Context context) {
super(DIFF_CALLBACK);
this.context = context;
}
/**
* 用于计算列表中两个非空项之间的差异的回调。
* 之前数据更新了,需要通过notifyDataSetChanged()通知整个RecyclerView,效率不高
* 使用DiffUtil只会更新需要更新的Item,不需要刷新整个RecyclerView,并且可以在Item删除的时候加上动画效果
* 原理使用的Myers差分算法,平常使用的版本控制工具git就是通过这种算法来比较文件差异
*/
private static DiffUtil.ItemCallback<Article> DIFF_CALLBACK = new DiffUtil.ItemCallback<Article>() {
/**
* 当DiffUtil想要检测两个对象是否代表同一个Item时,调用该方法进行判断
* */
@Override
public boolean areItemsTheSame(@NonNull Article oldItem, @NonNull Article newItem) {
return oldItem.id == newItem.id;
}
/**
* 当DiffUtil想要检测两个Item是否有一样的数据时,调用该方法进行判断
* 内容如果更新了,展示给用户看的东西可能也需要更新,所以需要这个判断
* */
@SuppressLint("DiffUtilEquals")
@Override
public boolean areContentsTheSame(@NonNull Article oldItem, @NonNull Article newItem) {
return oldItem.equals(newItem);
}
};
@NonNull
@Override
public ArticlesViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ArticleItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.article_item, parent, false);
return new ArticlesViewHolder(binding.getRoot());
}
@Override
public void onBindViewHolder(@NonNull ArticlesViewHolder holder, int position) {
binding = DataBindingUtil.getBinding(holder.itemView);
Article article = getItem(position);
if (article != null) {
//数据绑定
binding.setArticle(article);
}
}
static class ArticlesViewHolder extends RecyclerView.ViewHolder {
public ArticlesViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}
在Activity中将RecycleView
和Adapter绑定,当数据发生变化时,通过LiveData传递,最后通过Adapter.submitList()
刷新数据。
ArticlesAdapter articlesAdapter = new ArticlesAdapter(this);
binding.recyclerView.setHasFixedSize(true);
ArticlesViewModel articlesViewModel = new ViewModelProvider(this).get(ArticlesViewModel.class);
friendsViewModel.articlePagedList.observe(this, new Observer<PagedList<Articles.DataBean.Article>>() {
@Override
public void onChanged(PagedList<Articles.DataBean.Article> friends) {
articlesAdapter.submitList(friends);
}
});
binding.recyclerView.setAdapter(articlesAdapter);
PageKeyedDataSource
仍然采用上面的接口,创建DataSource,继承自PageKeyedDataSource
,获取数据。
public class ArticlePageDataSource extends PageKeyedDataSource<Integer, Articles.DataBean.Article> {
public static final int FIRST_PAGE = 1;
public static final int PAGE_SIZE = 20;
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull final LoadInitialCallback<Integer, Articles.DataBean.Article> callback) {
RetrofitClient.getInstance()
.getApi()
.articlesData(FIRST_PAGE)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
//previousPageKey:前一页key,当前为第一页,设置为null
//nextPageKey:下一页key,当前页+1
callback.onResult(response.body().data.datas, null, FIRST_PAGE + 1);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
/**
*加载下一页的工作,设置nextKey通过LoadParams传递过来
* 设置下一页的key,判断是否有更多数据,没有置null
*/
@Override
public void loadAfter(@NonNull final LoadParams<Integer> params, @NonNull final LoadCallback<Integer, Articles.DataBean.Article> callback) {
RetrofitClient.getInstance()
.getApi()
.articlesData(params.key)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
Integer nextKey = null;
if (params.key * PAGE_SIZE < response.body().data.total) {
nextKey = params.key + 1;
}
callback.onResult(response.body().data.datas, nextKey);
Log.e("loadAfter()", " response:" + nextKey);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
@Override
public void loadBefore(@NonNull final LoadParams<Integer> params, @NonNull final LoadCallback<Integer, Articles.DataBean.Article> callback) {
Log.e("loadBefore()", String.valueOf(params.key));
}
}
剩下的步骤和使用PositionalDataSource
的一致。
ItemKeyedDataSource
仍然使用上面的接口,创建DataSource,继承自ItemKeyedDataSource
,获取数据。
public class ArticleItemDataSource extends ItemKeyedDataSource<Integer, Articles.DataBean.Article> {
public static final int PAGE_SIZE = 20;
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull final LoadInitialCallback<Articles.DataBean.Article> callback) {
int since = 1;
RetrofitClient.getInstance()
.getApi()
.articlesData(since)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
Log.e("loadInitial()", " response:" + response.body());
callback.onResult(response.body().data.datas);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
@Override
public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull final LoadCallback<Articles.DataBean.Article> callback) {
RetrofitClient.getInstance()
.getApi()
.articlesData(params.key)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
callback.onResult(response.body().data.datas);
Log.e("yhj", "onResponse: " + params.key);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
//向上加载,如初始加载第三页,向上可加载第二页
@Override
public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Articles.DataBean.Article> callback) {
}
/**
* 设置作为下一页请求的key,返回即可
*/
@NonNull
@Override
public Integer getKey(@NonNull Articles.DataBean.Article article) {
return article.nextPage;
}
}
剩下的步骤和使用PositionalDataSource
的一致。
使用Paging分页请求网络数据,各个类的关系如图所示。
BoundaryCallback
在实际项目开发中,为了更好的用户体验,需要对数据进行缓存。加入缓存,数据源变成了网络数据和本地数据组成的双数据源。这会增加应用程序的复杂度,需要处理好数据的时效性及新旧数据的切换等问题。为此,Google在Paging中加入了BoundaryCallback,通过BoundaryCallback实现数据的单一来源架构,简化应用的复杂度。
使用Room和BoundaryCallback来获取公众号历史数据。
创建数据库
@Database(entities = {Articles.DataBean.Article.class}, version = 1, exportSchema = false)
public abstract class ArticleDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "article_db";
private static ArticleDatabase databaseInstance;
public static synchronized ArticleDatabase getInstance(Context context) {
if (databaseInstance == null) {
databaseInstance = Room
.databaseBuilder(context.getApplicationContext(), ArticleDatabase.class, DATABASE_NAME)
.build();
}
return databaseInstance;
}
public abstract ArticleDao articleDao();
}
Model
@Entity
public class Article {
@ColumnInfo(typeAffinity = ColumnInfo.TEXT)
public String author;
@ColumnInfo(typeAffinity = ColumnInfo.TEXT)
public String desc;
@PrimaryKey()
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(typeAffinity = ColumnInfo.TEXT)
public String title;
}
Dao
@Dao
public interface ArticleDao {
/**
* 插入数据
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertArticles(List<Article> article);
/**
* 清空数据
*/
@Query("DELETE FROM article")
void clear();
/**
* Room对Paging提供了原生支持,这里直接返回DataSource.Factory,
* 以便LivePagedListBuilder在创建的时候使用。
*/
@Query("SELECT * FROM article")
DataSource.Factory<Integer, Article> getArticlesList();
}
BoundaryCallback
public class ArticleBoundaryCallback extends PagedList.BoundaryCallback<Article> {
private String TAG = this.getClass().getName();
private Application application;
public ArticleBoundaryCallback(Application application) {
this.application = application;
}
/**
* 当数据库为空时,回调该方法,请求第一页数据
*/
@Override
public void onZeroItemsLoaded() {
super.onZeroItemsLoaded();
Log.e(TAG, "onZeroItemsLoaded()");
getTopData();
}
/**
* 加载数据库数据
* @param itemAtFront 数据库中的第一条数据
*/
@Override
public void onItemAtFrontLoaded(@NonNull Article itemAtFront) {
super.onItemAtFrontLoaded(itemAtFront);
Log.e(TAG, "onItemAtFrontLoaded()");
}
/**
* 请求下一页数据,并且数据库中数据全部加载完毕
* @param itemAtEnd 数据库中的最后一条数据
*/
@Override
public void onItemAtEndLoaded(@NonNull Article itemAtEnd) {
super.onItemAtEndLoaded(itemAtEnd);
Log.e(TAG, "onItemAtEndLoaded()");
getTopAfterData(itemAtEnd);
}
/**
* 没有数据的时候,加载第一页数据
*/
private void getTopData() {
int since = 0;
RetrofitClient.getInstance()
.getApi()
.articlesData(ArticlesViewModel.PER_PAGE / 20)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
Log.e("getTopData()", " response:" + response.body());
insertArticles(response.body().data.datas);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
/**
* 获取下一页数据
*/
private void getTopAfterData(Article article) {
RetrofitClient.getInstance()
.getApi()
.articlesData(ArticlesViewModel.PER_PAGE / 20)
.enqueue(new Callback<Articles>() {
@Override
public void onResponse(@NonNull Call<Articles> call, @NonNull Response<Articles> response) {
if (response.body() != null) {
Log.e("getTopAfterData()", " response:" + response.body());
insertArticles(response.body().data.datas);
}
}
@Override
public void onFailure(@NonNull Call<Articles> call, @NonNull Throwable t) {
}
});
}
/**
* 插入数据
*/
private void insertArticles(final List<Article> articles) {
ArchTaskExecutor.getIOThreadExecutor().execute(new Runnable() {
@Override
public void run() {
ArticleDatabase.getInstance(application).articleDao().insertArticles(articles);
}
});
}
}
ViewModel
public ArticlesViewModel(Application application) {
super(application);
ArticleDatabase articleDatabase = ArticleDatabase.getInstance(application);
articlePagedList = (new LivePagedListBuilder<>(articleDatabase.articleDao().getArticlesList(), ArticlesViewModel.PER_PAGE))
.setBoundaryCallback(new ArticleBoundaryCallback(application))
.build();
}
public void refresh(Context context) {
ArchTaskExecutor.getIOThreadExecutor().execute(() -> ArticleDatabase.getInstance(context).articleDao().clear());
}
Room对Paging组件提供原生支持,因此LivePagedListBuilder
创建PagedList时,可以直接将Room作为数据源。
PagedList实现增删改查
通过阅读PagedList
的源码发现,它只支持get
(查询)操作,不支持增、删和改,但在项目开发中有这样的需求,我们对PagedList
进行扩展。
创建DataSource数据源。
/**
* 一个可变更的ItemKeyedDataSource 数据源
* 工作原理是:我们知道DataSource是会被PagedList 持有的。
* 一旦,我们调用了new PagedList.Builder<Key, Value>().build(); 那么就会立刻触发当前DataSource的loadInitial()方法,而且是同步
* 详情见ContiguousPagedList的构造函数,而我们在当前DataSource的loadInitial()方法中返回了 最新的数据集合 data。
* 一旦,我们再次调用PagedListAdapter#submitList()方法 就会触发差分异计算 把新数据变更到列表之上了。
*/
public abstract class MutableItemKeyedDataSource<Key, Value> extends ItemKeyedDataSource<Key, Value> {
private ItemKeyedDataSource mDataSource;
public List<Value> data = new ArrayList<>();
public PagedList<Value> buildNewPagedList(PagedList.Config config) {
PagedList<Value> pagedList = new PagedList.Builder<Key, Value>(this, config)
.setFetchExecutor(ArchTaskExecutor.getIOThreadExecutor())
.setNotifyExecutor(ArchTaskExecutor.getMainThreadExecutor())
.build();
return pagedList;
}
public MutableItemKeyedDataSource(ItemKeyedDataSource dataSource) {
mDataSource = dataSource;
}
@Override
public void loadInitial(@NonNull LoadInitialParams<Key> params, @NonNull LoadInitialCallback<Value> callback) {
callback.onResult(data);
}
@Override
public void loadAfter(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Value> callback) {
if (mDataSource != null) {
//一旦 和当前DataSource关联的PagedList被提交到PagedListAdapter。那么ViewModel中创建的DataSource 就不会再被调用了
//我们需要在分页的时候 代理一下 原来的DataSource,迫使其继续工作
mDataSource.loadAfter(params, callback);
}
}
@Override
public void loadBefore(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Value> callback) {
callback.onResult(Collections.emptyList());
}
@NonNull
@Override
public abstract Key getKey(@NonNull Value item);
}
添加
public void addAndRefreshList(Comment item) {
PagedList<Comment> currentList = getCurrentList();
MutableItemKeyedDataSource<Integer, Comment> mutableItemKeyedDataSource = new MutableItemKeyedDataSource<Integer, Comment>((ItemKeyedDataSource) currentList.getDataSource()) {
@NonNull
@Override
public Integer getKey(@NonNull Comment item) {
return item.id;
}
};
mutableItemKeyedDataSource.data.add(item);
mutableItemKeyedDataSource.data.addAll(currentList);
PagedList<Comment> pagedList = mutableItemKeyedDataSource.buildNewPagedList(currentList.getConfig());
submitList(pagedList);
}
删除
/**
* 删除
* @param item 删除的item
*/
public void deleteItem(Comment item) {
MutableItemKeyedDataSource<Integer, Comment> dataSource = new MutableItemKeyedDataSource<Integer, Comment>((ItemKeyedDataSource) getCurrentList().getDataSource()) {
@NonNull
@Override
public Integer getKey(@NonNull Comment item) {
return item.id;
}
};
PagedList<Comment> currentList = getCurrentList();
for (Comment comment : currentList) {
//晒选出不是要删除的item,添加到dataSource.data
if (comment != item) {
dataSource.data.add(comment);
}
}
PagedList<Comment> pagedList = dataSource.buildNewPagedList(getCurrentList().getConfig());
submitList(pagedList);
}
修改
public void updateItem(Comment item) {
PagedList<Comment> currentList = getCurrentList();
MutableItemKeyedDataSource<Integer, Comment> dataSource = new MutableItemKeyedDataSource<Integer, Comment>((ItemKeyedDataSource) currentList.getDataSource()) {
@NonNull
@Override
public Integer getKey(@NonNull Comment item) {
return item.id;
}
};
for (Comment comment : currentList) {
if (comment == item) {
comment.setName("yhj");
}
break;
}
dataSource.data.addAll(currentList);
PagedList<Comment> pagedList = dataSource.buildNewPagedList(currentList.getConfig());
submitList(pagedList, new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
Paging3
Paging3与旧版Paging存在很大区别,目前是beta版。
优势
- 为 Kotlin 协程和流程提供一流的支持。
- 支持使用RxJava Single或Guava ListenableFuture基元进行异步加载。
- 针对自适应界面设计的内置加载状态和错误信号,包括重试和刷新功能。
- 改进了代码库层,包括取消支持和简化的数据源界面。
- 改进了演示层、列表分隔符、自定义页面转换和加载状态页眉和页脚。
依赖
implementation "androidx.paging:paging-runtime:3.0.0-beta02"
区别
Paging3向后兼容,仍然可以使用,只是标注已过时。
DataSource
Paing2中的DataSource有三种,Paging3中将它们合并到了PagingSource
中,实现load()
和getRefreshKey()
,在Paging3中,所有加载方法参数被一个LoadParams
密封类替代,该类中包含了每个加载类型所对应的子类。如果需要区分load()
中的加载类型,请检查传入了LoadParams
的哪个子类。
public class Articles3DataSource extends PagingSource<Integer,Article> {
@Nullable
@Override
public Integer getRefreshKey(@NotNull PagingState<Integer, Article> pagingState) {
return pagingState.getAnchorPosition();
}
@Nullable
@Override
public Object load(@NotNull LoadParams<Integer> loadParams, @NotNull Continuation<? super LoadResult<Integer, Article>> continuation) {
.....
}
}
PagedListAdapter
Adapter不在继承PagedListAdapter
,而是由PagingDataAdapter
替代,其它不变。
public class Articles3Adapter extends PagingDataAdapter<Article, Articles3Adapter.ArticlesViewHolder> {
.......
}