本来不想写这篇文章的,但是本人在学习的时候遇到一个问题,那就是paging框架只要有一次请求失败,那么他就不会再出发自动请求加载下一页的回调,事先在网上找了一圈,简书,csdn里面查了半天,也给博主们私信了,但是没有任何结果,这方面的东西太少了,所以想分享一下。顺便简单测试下删除添加等。
简略的讲一下这三个的须知
DataBinding 是用来省去findViewById
Paging 是实现无限滚动 提前预加载
ViewModle 将数据和页面进行解耦不会受到activity和Fragment的影响 除非销毁
DataBinding
jetpack是个全家桶,有数据库room,也有很好的分页控件paging,还有DataBinding等。本人还在学习中,小菜鸡路过......
这个示例中用到了DataBinding,我们先通过一个简单示例了解一下他。
先说布局文件
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.easyar.lib_network.bean.VersionBean"/>
<import type="com.easyar.jetpackdeme.MainActivity"/>
<import type="com.easyar.jetpackdeme.tools.TestIntent"/>
<variable
name="versionBean"
type="VersionBean" />
<variable
name="testIntent"
type="TestIntent" />
<variable
name="mainActivity"
type="MainActivity" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{versionBean==null?testIntent.tipsNull:versionBean.getVersion()}"
android:textColor="#333"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#333"
android:textSize="18sp"
android:text="@{versionBean==null?testIntent.tipsNull:versionBean.getTips()}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请求"
android:onClick="@{()->mainActivity.click()}"/>
</LinearLayout>
</layout>
layout、data等标签不需要自己手动敲,只要在原始布局的跟布局点击 alt+回车 就能自动生成
然后,绑定数据,比如TextView绑定的文字来自Javabean中,那么就要引入相应的类,如上面的VersionBean类,先用import 导入然后在variable标签中添加使用时候的属性名。然后绑定数据的时候采用@{ }的形势有点像lambda表达式。这里为了能注册android:onClick属性,我也把MainActivity加进来了。然后在activity页面就能监听点击事件了,不用像以前一样setOnclick了。
public class MainActivity extends AppCompatActivity implements ViewModelStoreOwner {
private VersionViewModle mVersionViewModle;
private MutableLiveData<VersionBean> liveData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBindingImpl activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
activityMainBinding.setLifecycleOwner(this);
activityMainBinding.setMainActivity(this);
ViewModelProvider mModelProvider = new ViewModelProvider(this, new ViewModelProvider.Factory() {
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new VersionViewModle();
}
});
mVersionViewModle = mModelProvider.get(VersionViewModle.class);
liveData = mVersionViewModle.getLiveData();
liveData.observe(this, new Observer<VersionBean>() {
@Override
public void onChanged(VersionBean versionBean) {
activityMainBinding.setVersionBean(versionBean);
}
});
}
public void click(){
Log.d("yanjin","requestMain");
Call<VersionBean> versionMessage = RequestCenter.mRequestAPI.getVersionMessage();
versionMessage.enqueue(new Callback<VersionBean>() {
@Override
public void onResponse(Call<VersionBean> call, Response<VersionBean> response) {
VersionBean body = response.body();
Log.d("yanjin","requestMain---"+body.toString());
mVersionViewModle.getLiveData().setValue(body);
}
@Override
public void onFailure(Call<VersionBean> call, Throwable t) {
}
});
}
}
有些同学看到这个代码,可能会问setContentView();去哪了?没错DataBindingUtil做了一件大事,那就是他接管了对view的管理,DataBindingUtil.setContentView(this, R.layout.activity_main);就能获得DataBinding对象注意这里我们的activity名字是MainActivity所以他的DataBinding名字是ActivityMainBindingImpl,掌握好规律不要找错了。ActivityMainBindingImpl 这个类在你将布局转换成databading布局的时候就已经生成。
然后设置我们布局中要的参数一种一个就是activity,activityMainBinding.setMainActivity(this);然后ViewModelProvider的获取就是模板代码。然后在liveData.observe的onChanged里面设置会变动的数据。VersionViewModle是继承于ViewModel的类
public class VersionViewModle extends ViewModel {
private MutableLiveData<VersionBean> liveData = new MutableLiveData<>();
public MutableLiveData<VersionBean> getLiveData() {
return liveData;
}
public void setLiveData(VersionBean versionBean) {
this.liveData.setValue(versionBean);
}
}
他主要是来管理数据的。
下面的RequestCenter是基于retorfit的网络请求,在成功的时候重新设置了数据。最后是数据bean类
public class VersionBean {
/**
* version : 1
* tips : 增加新功能
* url : http://47.103.195.133/mai_1.1.apk
*/
private int version;
private String tips;
private String url;
public String getVersion() {
return version+"";
}
public void setVersion(int version) {
this.version = version;
}
public String getTips() {
return tips;
}
public void setTips(String tips) {
this.tips = tips;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "VersionBean{" +
"version=" + version +
", tips='" + tips + '\'' +
", url='" + url + '\'' +
'}';
}
}
很简单的测试了文字的设置。
paging
先来看看布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.easyar.jetpackdeme.list.view.activity.ImageListActivity"/>
<variable
name="imageListActivity"
type="ImageListActivity" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".list.view.activity.ImageListActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:id="@+id/buttom_btns"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:visibility="gone"
android:layout_alignParentBottom="true">
<Button
android:id="@+id/btn_add"
android:layout_width="0dp"
android:layout_weight="1"
android:text="增加"
android:onClick="@{()->imageListActivity.addAItem()}"
android:layout_height="match_parent"/>
<Button
android:id="@+id/btn_delete"
android:layout_width="0dp"
android:layout_weight="1"
android:text="删除"
android:onClick="@{()->imageListActivity.deleteAItem()}"
android:layout_height="match_parent"/>
</LinearLayout>
</RelativeLayout>
</layout>
很简单一个recyclerview一个底部增加和删除按钮,老规矩,如果要android:onClick那么就要加入相应的类。这里我们把click绑定在activity里面
public class ImageListActivity extends AppCompatActivity implements ImageListViewHelper.DataChangedCallBack {
private ActivityImageListBinding mBinding;
private RecyclerView mRecyclerView;
private LinearLayoutManager mLinearLayoutManager;
private ImageListPagedAdapter mAdapter;
private ImageListViewHelper mImageListViewHelper;
private View mHeaderView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_image_list);
mBinding.setImageListActivity(this);
mBinding.setLifecycleOwner(this);
initView();
}
private void initView() {
mRecyclerView = mBinding.recyclerView;
mLinearLayoutManager = new LinearLayoutManager(this);
mLinearLayoutManager.setOrientation(RecyclerView.VERTICAL);
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mAdapter = new ImageListPagedAdapter();
mRecyclerView.setAdapter(mAdapter);
mImageListViewHelper = new ImageListViewHelper(mRecyclerView,mAdapter);
mImageListViewHelper.createPagedListLiveData(this,this);
mHeaderView = LayoutInflater.from(ImageListActivity.this).inflate(R.layout.header_item_layout, mRecyclerView, false);
}
@Override
public void onChanged(List<ImageBean.DataBean> data) {
if(data == null || data.size()==0){
mBinding.buttomBtns.setVisibility(View.GONE);
return;
}else{
mAdapter.addHeaderView(mHeaderView);
mBinding.buttomBtns.setVisibility(View.VISIBLE);
}
}
/**
* 测试添加一条数据
*/
public void addAItem(){
mImageListViewHelper.addAItem();
}
public void deleteAItem(){
mImageListViewHelper.deleteAItem();
}
}
这里我们尽量不要在activity中写太多东西,所以对paging的操作统一放到了ImageListViewHelper中。activity主要留了对recyclerview的操作。
然后封装一个adapter的抽象类,支持添加头部和底部的那种
public abstract class AbsPagedListAdapter<T,VH extends RecyclerView.ViewHolder> extends PagedListAdapter<T,VH> {
private SparseArray<View> mHeader = new SparseArray<>();
private SparseArray<View> mFooter = new SparseArray<>();
private int BASE_ITEM_HEADER_RANGE=10000;
private int BASE_ITEM_FOOTER_RANGE=20000;
public AbsPagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
super(diffCallback);
}
public void addHeaderView(View view){
if(mHeader.indexOfValue(view)<0){
mHeader.put(BASE_ITEM_HEADER_RANGE++,view);
notifyDataSetChanged();
}
}
public void addEmptyView(View view) {
if(mHeader.size()>0){
mHeader.clear();
}
addHeaderView(view);
notifyDataSetChanged();
}
public void removeHeaderView(View view){
int key = mHeader.indexOfValue(view);
if(key>=0){
mHeader.removeAt(key);
notifyDataSetChanged();
}
}
public void removeEmptyView(View view) {
removeHeaderView(view);
}
public void addFooterView(View view){
if(mFooter.indexOfValue(view)<0){
mFooter.put(BASE_ITEM_FOOTER_RANGE++,view);
notifyDataSetChanged();
}
}
@Override
public int getItemCount() {
int itemCount = super.getItemCount();
return itemCount+mHeader.size()+mFooter.size();
}
//获取真实条目数量
public int getOriginalItemCount(){
return getItemCount() - mHeader.size() - mFooter.size();
}
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeader.keyAt(position);
}
if(isFooterView(position)){
position = position - getOriginalItemCount()-mHeader.size();
return mFooter.keyAt(position);
}
//正常条目,potion需要重新计算
position = position - mHeader.size();
return getItemViewType2(position);
}
private int getItemViewType2(int position) {
return 0;
}
private boolean isFooterView(int position) {
return position>=getOriginalItemCount()+mHeader.size();
}
private boolean isHeaderView(int position) {
return position<=mHeader.size()-1;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if(mHeader.indexOfKey(viewType)>=0){
View view = mHeader.get(viewType);
return (VH) new RecyclerView.ViewHolder(view){};
}
if(mFooter.indexOfKey(viewType)>=0){
View view = mFooter.get(viewType);
return (VH) new RecyclerView.ViewHolder(view){};
}
return onCreateViewHolder2(parent,viewType);
}
protected abstract VH onCreateViewHolder2(ViewGroup parent, int viewType);
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
//头部和底部不能参与绑定数据
if(isHeaderView(position) || isFooterView(position)){
return;
}
position = position - mHeader.size();
onBindViewHolder2(holder,position);
}
protected abstract void onBindViewHolder2(VH holder, int position);
@Override
public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
super.registerAdapterDataObserver(new MyObserver(observer));
}
private class MyObserver extends RecyclerView.AdapterDataObserver{
private RecyclerView.AdapterDataObserver observer;
public MyObserver(RecyclerView.AdapterDataObserver observer) {
this.observer = observer;
}
public void onChanged() {
// Do nothing
observer.onChanged();
}
public void onItemRangeChanged(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeChanged(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
observer.onItemRangeChanged(positionStart+mHeader.size(),itemCount,payload);
}
public void onItemRangeInserted(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeInserted(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeRemoved(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeRemoved(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// do nothing
observer.onItemRangeMoved(fromPosition+mHeader.size(),toPosition+mHeader.size(),itemCount);
}
}
}
以前觉得封装这些东西挺麻烦的,操作多了后发现是一步步来就行的。先安排两个容器装头部和底部
private SparseArray<View> mHeader = new SparseArray<>();
private SparseArray<View> mFooter = new SparseArray<>();
然后开拓一些方法操作view,并且操作完都要刷新下adapter
public void addHeaderView(View view){
if(mHeader.indexOfValue(view)<0){
mHeader.put(BASE_ITEM_HEADER_RANGE++,view);
notifyDataSetChanged();
}
}
public void addEmptyView(View view) {
if(mHeader.size()>0){
mHeader.clear();
}
addHeaderView(view);
notifyDataSetChanged();
}
public void removeHeaderView(View view){
int key = mHeader.indexOfValue(view);
if(key>=0){
mHeader.removeAt(key);
notifyDataSetChanged();
}
}
public void removeEmptyView(View view) {
removeHeaderView(view);
}
public void addFooterView(View view){
if(mFooter.indexOfValue(view)<0){
mFooter.put(BASE_ITEM_FOOTER_RANGE++,view);
notifyDataSetChanged();
}
}
重新计算数量
@Override
public int getItemCount() {
int itemCount = super.getItemCount();
return itemCount+mHeader.size()+mFooter.size();
}
重判断条目类型
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeader.keyAt(position);
}
if(isFooterView(position)){
position = position - getOriginalItemCount()-mHeader.size();
return mFooter.keyAt(position);
}
//正常条目,potion需要重新计算
position = position - mHeader.size();
return getItemViewType2(position);
}
对头部和底部的的ViewHolder封装
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if(mHeader.indexOfKey(viewType)>=0){
View view = mHeader.get(viewType);
return (VH) new RecyclerView.ViewHolder(view){};
}
if(mFooter.indexOfKey(viewType)>=0){
View view = mFooter.get(viewType);
return (VH) new RecyclerView.ViewHolder(view){};
}
return onCreateViewHolder2(parent,viewType);
}
然后对于绑定数据时,对头部和底部跳过就行
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
//头部和底部不能参与绑定数据
if(isHeaderView(position) || isFooterView(position)){
return;
}
position = position - mHeader.size();
onBindViewHolder2(holder,position);
}
最后最主要的是对recyclerview的AdapterDataObserver重新封装
@Override
public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
super.registerAdapterDataObserver(new MyObserver(observer));
}
private class MyObserver extends RecyclerView.AdapterDataObserver{
private RecyclerView.AdapterDataObserver observer;
public MyObserver(RecyclerView.AdapterDataObserver observer) {
this.observer = observer;
}
public void onChanged() {
// Do nothing
observer.onChanged();
}
public void onItemRangeChanged(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeChanged(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
observer.onItemRangeChanged(positionStart+mHeader.size(),itemCount,payload);
}
public void onItemRangeInserted(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeInserted(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeRemoved(int positionStart, int itemCount) {
// do nothing
observer.onItemRangeRemoved(positionStart+mHeader.size(),itemCount);
}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// do nothing
observer.onItemRangeMoved(fromPosition+mHeader.size(),toPosition+mHeader.size(),itemCount);
}
}
对于所有的position都要加上头部的数量,不然的话头部和正常条目会混在一起。
这样的话我们就提供了onCreateViewHolder2与onBindViewHolder2两个方法让子类实现
public class ImageListPagedAdapter extends AbsPagedListAdapter<ImageBean.DataBean,ImageListPagedAdapter.ViewHolder>{
public ImageListPagedAdapter() {
super(new DiffUtil.ItemCallback<ImageBean.DataBean>() {
@Override
public boolean areItemsTheSame(@NonNull ImageBean.DataBean oldItem, @NonNull ImageBean.DataBean newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull ImageBean.DataBean oldItem, @NonNull ImageBean.DataBean newItem) {
return oldItem.equals(newItem);
}
});
}
@Override
protected ViewHolder onCreateViewHolder2(ViewGroup parent, int viewType) {
ImagesListItemLayoutBinding bind = DataBindingUtil.bind(LayoutInflater.from(parent.getContext()).inflate(R.layout.images_list_item_layout, parent, false));
return new ViewHolder(bind);
}
@Override
protected void onBindViewHolder2(ViewHolder holder, int position) {
ImageBean.DataBean dataBean = getItem(position);
holder.dindData(dataBean);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ImagesListItemLayoutBinding mViewDataBinding;
public ViewHolder(@NonNull ImagesListItemLayoutBinding viewDataBinding) {
super(viewDataBinding.getRoot());
mViewDataBinding = viewDataBinding;
}
public void dindData(ImageBean.DataBean item) {
mViewDataBinding.setDataBean(item);
}
}
}
条目布局很简单就是展示个图片
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.easyar.lib_network.bean.ImageBean.DataBean"/>
<variable
name="dataBean"
type="DataBean" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.easyar.jetpackdeme.view.BindingImageView
android:id="@+id/iv_item"
android:layout_width="match_parent"
android:layout_height="200dp"
tools:src="@mipmap/ic_launcher"
android:layout_centerHorizontal="true"
app:iamge_url="@{dataBean.getUrl}"
android:layout_marginBottom="10dp"
app:isCircle="@{false}" />
</RelativeLayout>
</layout>
值的注意的是我们在这里直接用ImageView的src展示图片是不行的,所以我们创建一个自定义imageview。
public class BindingImageView extends AppCompatImageView {
public BindingImageView(Context context) {
this(context,null);
}
public BindingImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public BindingImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@BindingAdapter(value = {"iamge_url","isCircle"},requireAll = true)
public static void setImageUrl(BindingImageView view,String iamgeUrl,boolean isCircle){
RequestBuilder<Drawable> builder = Glide.with(view).load(iamgeUrl);
if(isCircle){
builder.transform(new CircleCrop());
}else{
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if(layoutParams!=null && layoutParams.width>0 && layoutParams.height>0){
builder.override(layoutParams.width,layoutParams.height);
}
builder.into(view);
}
}
其他都简单,唯一注意@BindingAdapter(value = {"iamge_url","isCircle"},requireAll = true),value 表示硕有的参数 ,requireAll = true表示设置了所有参数才能出发这个方法。
所以adapter方面就完成了。下面轮到了ImageListViewHelper
public class ImageListViewHelper extends RecyclerView.OnScrollListener implements ListEmptyView.EmptyViewClickListener {
private RecyclerView mRecyclerView;
private ImageListPagedAdapter mAdapter;
private PagedList.Config mConfig;
private ImageListDataSource mDataSource;
private DataChangedCallBack mDataChangedCallBack;
private final ListEmptyView mListEmptyView;
public ImageListViewHelper(RecyclerView recyclerView, ImageListPagedAdapter adpater){
this.mRecyclerView = recyclerView;
this.mAdapter = adpater;
mListEmptyView = new ListEmptyView(recyclerView.getContext());
mListEmptyView.setListener(this);
mRecyclerView.addOnScrollListener(this);
}
public void createPagedListLiveData(LifecycleOwner lifecycleOwner,DataChangedCallBack dataChangedCallBack){
mDataChangedCallBack = dataChangedCallBack;
mConfig = getConfig();
setLiveData(lifecycleOwner);
}
private void setLiveData(LifecycleOwner lifecycleOwner) {
LiveData<PagedList<ImageBean.DataBean>> liveData = new LivePagedListBuilder(new DataSource.Factory() {
@NonNull
@Override
public DataSource create() {
mDataSource = new ImageListDataSource(new DataChangedCallBack() {
@Override
public void onChanged(List<ImageBean.DataBean> data) {
showOrHindEmpty(data);
if(mDataChangedCallBack!=null){
mDataChangedCallBack.onChanged(data);
}
}
});
return mDataSource;
}
}, mConfig).build();
//观察者模式,将Adapter注册进去,当liveData发生改变事通知Adapter
liveData.observe(lifecycleOwner, new Observer<PagedList<ImageBean.DataBean>>() {
@Override
public void onChanged(@Nullable PagedList<ImageBean.DataBean> subjectsBeans) {
mAdapter.submitList(subjectsBeans);
}
});
}
private PagedList.Config getConfig(){
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(5) //每页显示的词条数
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(5) //首次加载的数据量
.setPrefetchDistance(1) //距离底部还有多少条数据时开始预加载
.build();
return config;
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if(layoutManager instanceof LinearLayoutManager){
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
int itemCount = recyclerView.getLayoutManager().getItemCount();
int lastPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition();
if(lastPosition == itemCount-1){
//滑动到最后一条,重新开启pagelist监听
reSetPaging(Collections.<ImageBean.DataBean>emptyList());
}
}
}
public void reSetPaging(List<ImageBean.DataBean> list) {
reSetPaging(list,false);
}
public void reSetPaging(List<ImageBean.DataBean> list,boolean delete) {
MutableItemKeyedDataSource<Integer, ImageBean.DataBean> mutableItemKeyedDataSource = new MutableItemKeyedDataSource<Integer, ImageBean.DataBean>(mDataSource) {
@NonNull
@Override
public Integer getKey(@NonNull ImageBean.DataBean item) {
return item.getId();
}
};
List<ImageBean.DataBean> data = mutableItemKeyedDataSource.data;
data.clear();
PagedList<ImageBean.DataBean> currentList = ImageListViewHelper.this.mAdapter.getCurrentList();
data.addAll(currentList);
if(list!=null){
if(delete){
//如果是要删除
for (ImageBean.DataBean dataBean : list) {
data.remove(dataBean);
}
}else{
data.addAll(list);
}
}
PagedList<ImageBean.DataBean> dataBeans = mutableItemKeyedDataSource.buildNewPagedList(mConfig);
ImageListViewHelper.this.mAdapter.submitList(dataBeans);
}
private void reStartLoadData() {
RequestCenter.mRequestAPI.getImageData().enqueue(new Callback<ImageBean>() {
@Override
public void onResponse(Call<ImageBean> call, Response<ImageBean> response) {
ImageBean body = response.body();
List<ImageBean.DataBean> data = body.getData();
reSetPaging(data);
showOrHindEmpty(data);
if(mDataChangedCallBack!=null){
mDataChangedCallBack.onChanged(data);
}
}
@Override
public void onFailure(Call<ImageBean> call, Throwable t) {
}
});
}
private void showOrHindEmpty(List<ImageBean.DataBean> data) {
if(data == null || data.size()==0){
//显示空白页面
mAdapter.addEmptyView(mListEmptyView);
}else{
mAdapter.removeEmptyView(mListEmptyView);
}
}
@Override
public void clickRestart() {
reStartLoadData();
}
public void addAItem() {
Log.d("yanjin","addAItem");
List<ImageBean.DataBean> list = new ArrayList<>();
ImageBean.DataBean dataBean = new ImageBean.DataBean();
dataBean.setId(10);
dataBean.setTips("测试插入");
dataBean.setUrl("http://img2.imgtn.bdimg.com/it/u=1050962453,2431854183&fm=26&gp=0.jpg");
list.add(dataBean);
reSetPaging(list);
}
public void deleteAItem() {
Log.d("yanjin","addAItem");
List<ImageBean.DataBean> list = new ArrayList<>();
ImageBean.DataBean dataBean = new ImageBean.DataBean();
dataBean.setId(10);
dataBean.setTips("测试插入");
dataBean.setUrl("http://img2.imgtn.bdimg.com/it/u=1050962453,2431854183&fm=26&gp=0.jpg");
list.add(dataBean);
reSetPaging(list,true);
}
public interface DataChangedCallBack{
void onChanged(List<ImageBean.DataBean> data);
}
}
paging需要一个PagedList.Config
private PagedList.Config getConfig(){
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(5) //每页显示的词条数
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(5) //首次加载的数据量
.setPrefetchDistance(1) //距离底部还有多少条数据时开始预加载
.build();
return config;
}
同样的需要liveData绑定数据
private void setLiveData(LifecycleOwner lifecycleOwner) {
LiveData<PagedList<ImageBean.DataBean>> liveData = new LivePagedListBuilder(new DataSource.Factory() {
@NonNull
@Override
public DataSource create() {
mDataSource = new ImageListDataSource(new DataChangedCallBack() {
@Override
public void onChanged(List<ImageBean.DataBean> data) {
showOrHindEmpty(data);
if(mDataChangedCallBack!=null){
mDataChangedCallBack.onChanged(data);
}
}
});
return mDataSource;
}
}, mConfig).build();
//观察者模式,将Adapter注册进去,当liveData发生改变事通知Adapter
liveData.observe(lifecycleOwner, new Observer<PagedList<ImageBean.DataBean>>() {
@Override
public void onChanged(@Nullable PagedList<ImageBean.DataBean> subjectsBeans) {
mAdapter.submitList(subjectsBeans);
}
});
}
当然我这里把空界面的view也放在这个类里面,activity只保管头部和底部布局就行。
最后就是我们关心的问题,paging框架只要有一次请求失败,那么他就不会再出发自动请求加载下一页的回调。
这个问题我们搞一个配合类,记录下paging加载过的数据
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.<Value>emptyList());
}
@NonNull
@Override
public abstract Key getKey(@NonNull Value item);
}
然后,我们在替换数据就行了
public void reSetPaging(List<ImageBean.DataBean> list,boolean delete) {
MutableItemKeyedDataSource<Integer, ImageBean.DataBean> mutableItemKeyedDataSource = new MutableItemKeyedDataSource<Integer, ImageBean.DataBean>(mDataSource) {
@NonNull
@Override
public Integer getKey(@NonNull ImageBean.DataBean item) {
return item.getId();
}
};
List<ImageBean.DataBean> data = mutableItemKeyedDataSource.data;
data.clear();
PagedList<ImageBean.DataBean> currentList = ImageListViewHelper.this.mAdapter.getCurrentList();
data.addAll(currentList);
if(list!=null){
if(delete){
//如果是要删除
for (ImageBean.DataBean dataBean : list) {
data.remove(dataBean);
}
}else{
data.addAll(list);
}
}
PagedList<ImageBean.DataBean> dataBeans = mutableItemKeyedDataSource.buildNewPagedList(mConfig);
ImageListViewHelper.this.mAdapter.submitList(dataBeans);
}
先把数据清空,然后传入新的数据和adapter中记录的原来的数据,重新submitList就好了,当然,我这里还做了一个滑动到底部才触发的判断。
Paging最直接的一个问题是:
如何管理列表额外的状态?
列表状态的响应式管理
这样的需求随处可见,比如 侧滑删除
、为评论点赞
等等:
本文将阐述:如何管理Paging
分页列表的 状态,为何这样设计,以及设计的过程。
列表的状态问题
和市面上其它热门的分页库相比,Paging
最大的亮点在于其 将列表分页加载的逻辑作为回调函数封装入 DataSource
中,开发者在配置完成后,无需通过代码手动控制分页的加载,列表会 自动加载 下一页数据并展示。
这种便利意味着开发者不需要自己持有 数据源 ,大多数时候这使得开发流程更加便利,但总有偶然,比如这样一个界面:
这种需求屡见不鲜,其本质是,列表本身展示服务端返回的列表数据之外,还需要 本地控制额外的状态。
什么叫 额外的状态 ? 我们先用简单的一张图展示没有额外状态的情形,这时,列表的所有UI元素都从服务端获取:
现在我们将上文Gif
中的点赞效果也通过一张图表示:
读者可能还未认识到两种业务场景之间的差异性:对于列表的初始化来讲,所有UI元素都被服务端返回的数据渲染,每条评论是否已经被点赞,服务端都通过Comment
进行了描述。
需要注意的是,在某一刻,用户发现某个评论非常有趣,因此他选择对该评论进行了点赞的操作。
在业务代码中,我们需要向服务端POST
一个点赞的请求,服务端返回了一个200的成功码,但问题来了,接下来我们 如何让列表中的那条评论状态发生变化(即点赞的icon由灰色变成绿色高亮,已告知用户点赞成功)?
这就引发了文章最开始的那个问题,当列表的状态发生了变更,如何管理并更新列表?
方案1:再次刷新请求接口
最简单的方案是再次请求API,每当列表状态发生了变更,重新拉取评论列表,服务端返回的最新数据中,该评论自然已经被点赞了(即列表正确进行了更新)。
读者应该清楚,该方案实际并不可行,原因有二:
- 成本太高:某些操作对于用户来说,应该是非常 轻量级 的(比如点赞),他们甚至希望这些操作能够 立即被响应 在UI上 ,而请求API并刷新列表这一个过程太重了,即使不考虑服务器的负担,对于用户来说,UI的刷新需要数秒的等待也是非常糟糕的体验。
- 不符合逻辑:我们更需要注意的是,
Paging
是一个分页列表,而刷新请求行为对于分页列表来说,是一个不符合产品预期的行为(比如,我的点赞操作是针对第5页的某个评论执行的,产品的设计不可能允许每次点赞都重置为列表的第一页数据,这意味着极度糟糕的用户体验)。
现在我们理解了 每当列表状态发生了变更就刷新接口 并非良策,因为这种通过 远程重新拉取数据源 更新UI的方式成本太高了。
方案2:额外维护一个状态的列表
大概思路是在内存中为RecyclerView
维护一个额外的List
,用于一一映射对应position
的Item
状态:
class CommentPagedAdapter(
private val likedList: ArrayList<Boolean>
)
通过在内存中维护这样一个List
,的确可以实现需求,但读者需要认识到的是,Paging
分页库本身最大的优点便是 随着列表的滚动自动加载分页数据,每次分页的行为开发者并不需要手动配置,并通过调用类似notifyItemRangeInserted()
的方法更新UI。
很显然,每当分页数据获取后,开发者依然需要手动维护这个额外状态的List
——方案2和选择使用Paging
的初衷背道而驰,因此它并非最优先考虑的方案。
库本身设计的问题?
现在问题是,既不能通过 服务端 作为数据源,也不能在 内存中 额外维护一个状态的列表, 读者难免会质疑Paging
库本身设计的问题。
我该如何控制列表额外的状态(包括修改、增加或者删除)?
事实上该问题已经在Github的这个 issue 中进行了讨论,Google
的工程师的回复是:
从技术的角度而言 ,我们可以创建一个允许部分更改数据源的API,但之后我们需要记录这些改动并在主线程上重新传递给列表。这种方法的问题在于,如果你有一个已停止的
RecyclerView
(也就是后堆栈),它将不会(也不应该)接收任何更新,因此PagedList
将保留这个可能很长的数据列表并重新应用于主线程上的每个观察者。
这使问题变得非常复杂,这就是我们使用单个列表的原因。
显然,Paging
考虑到了更多,和市面上 什么都能做 的框架相比,它 敢于收紧开发者API的调用权限,在开发者们发挥更多奇思妙想之前,将其紧紧束缚到了可控制的范围之内,这也是笔者非常推崇Paging
的原因之一。
那么我们该如何处理我们的业务?此时引入一个新的角色似乎是一个不错的选择,那就是 持久层(即缓存)。
通过架构解决业务问题
综上所述,对于分页列表的状态管理问题,需要做到的是:
- 1.将一个单独的
List
交给Paging
去进行分页加载并渲染(不应在内存中手动维护一个额外状态的列表); - 2.不应该每次都通过重的操作刷新数据源(比如网络请求刷新接口)。
因此,我们需要一个 中间件 进行业务的调度——在需要刷新整个数据源的时候(比如用户的下拉刷新操作),从服务端拉取数据;在不需要繁重的操作时(比如用户针对某个评论进行点赞),仅仅需要针对单个数据源进行简单的修改。
这已经不单单是业务业务的问题,并且涉及到了项目本身的架构,接下来, 持久层 (即本地缓存)闪亮登场。
1.用持久层作为唯一的数据源
Android
平台的数据库框架有很多种,本文以官方的架构组件Room
为例。
为什么要为项目的架构额外添加一个持久层?事实上,随着项目体系的日益庞大,数据库是终究需要添加进入项目中的,因此,在设计项目的架构之前,提前将数据库的框架配置进来是一个不错的选择——未雨绸缪总不是坏事。
以列表的渲染为例,让我们来看看项目之前的结构:
回到本文,对于Paging
来讲,我们并无法直接获取数据源,因此对于列表状态的管理,我们需要额外的角色帮助,那就是本地的持久化缓存。
让我们看看添加了持久层之后的结构:
添加了缓存之后,每当我们尝试初始化一个分页列表,框架会从服务器拉取数据,之后数据被存储到了Room
中。
请注意!Paging
原生提供了对Room
数据库框架的支持,因此它总是可以第一时间响应到数据库中数据的变化,并自动渲染在UI上。
现在,我们将 请求服务器API 和 数据的渲染 两者通过持久层进行了隔离,对于RecyclerView
来说,持久层是唯一的数据源,即:
列表只反应了数据库的变更。
现在列表的显示和服务端的请求已经 完全无关 了,读者也许会有这样的疑问——这样做的好处是什么?
2.列表状态的管理
现在我们回到文中最初的问题,如何管理列表的状态?
对于一个拥有复杂状态的分页列表,无论是 服务端 作为数据源,还是在 内存中 额外维护一个状态列表,都不是很好的选择;而现在我们加入了Room
,并作为列表唯一的数据源,局势发生了怎样微妙的变化呢?
让我们来看看加入了持久层之后,下拉刷新的逻辑发生了怎样的变化:
- 1.下拉刷新意味着我们需要重置数据,因此我们手动清除了数据库内对应表中的数据;
- 2.当表中数据被清空时,
Paging
会自动响应到数据的变化,因为没有了数据,所以Paging
会自动向服务器请求数据; - 3.数据返回后,会再次将数据存储到数据库中;
- 4.这时
Paging
会再次响应到数据库的变化,并将最新的数据渲染到UI上。
看起来逻辑复杂了很多,实际上读者需要明确的是,步骤2、3、4都是我们作为开发者在初始化Paging
时就配置好的,因此如果用户需要刷新页面,只需要进行第一步的操作即可,即类似这样的一行代码:
// 刷新操作,仅需清除表内的列表数据
fun swipeRefresh() {
// 运行一个事务
db.runInTransaction {
// 清除列表数据
db.getDao().clearDataList()
}
}
现在我们将整个流程中,Paging
自动执行的步骤用紫色标记出来:
瞧,除了我们手动执行的逻辑,所有流程都交给了Paging
去 响应式 地执行。
我们总是下意识认为复杂的业务逻辑用过程式的编码更容易实现,Paging
用事实证明了并非如此——如果说项目中的某个页面追加了下拉刷新的需求,过程式的编码也许会花费更多的时间,并且代码也许会更分散、啰嗦且易出错。
3.更灵活、且可高度扩展
接下来分析的是,对分页列表点赞这种相对 轻量级的行为 又该如何处理?
答案呼之欲出, 我们依然用熟悉的流程图表示代码的执行步骤:
即使是复杂的状态,在这种模式下也不再是难题:首先,我们将数据库对应表中对应评论的isLike
(是否被点赞)设置为true
:
// 1.对本地的评论数据点赞
fun likeCommentLocal(comment: Comment) {
// 更新评论
comment.isLike = true
// 将评论更新到数据库中
db.runInTransaction {
db.getDao().updateLikeComment(o)
}
}
与此同时,我们也向服务器请求接口,告知评论被用户点赞:
// 2.对评论点赞
fun likeCommentRemote(commentId: String) {
service.likeComment(commentId)
// ....
}
当数据库中数据发生了变更,Paging
仍然会响应到数据的更新,并第一时间更新了UI,同时我们也向服务器发起了请求,一个完整的 点赞 操作相关的业务代码实现完毕。
有了持久层作为中间件,代码组织的灵活性大大提升,同时也具备了更高的扩展性。列表状态的管理不再是问题,诸如 点赞 、 下拉刷新 、 侧滑删除 等等等等,都可以通过对持久层的数据源进行修改,paging
总是可以第一时间自动响应到变更并更新UI。
也正如Room
官方文档第一句话所说的,对于Paging
分页列表(对app也一样)复杂的状态的展示和管理,开发者应该 将缓存作为列表的唯一真实的数据源:
This cache, which serves as your app's single source of truth.