DiffUtil工具类使用-让recyclerview使用更高效 问题背景 安卓开发过程中,recyclerview是很常见的滑动列表视图组件,数据刷新的时候,我们经常就是直接调用了mAdapter.notifyDataSetChanged()的方法
DiffUtil工具类使用-让recyclerview使用更高效
问题背景
安卓开发过程中,recyclerview是很常见的滑动列表视图组件,数据刷新的时候,我们经常就是直接调用了mAdapter.notifyDataSetChanged()的方法进行操作。但是很显然,这样直接操作有两个问题: (1)不会触发RecyclerView的动画效果(删除、新增、位移、change动画) (2)性能较低,毕竟是无脑的刷新了一遍整个RecyclerView。
问题分析
这时候,就有一个好用的工具类登场了。DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集到新数据集的最小变化量。使用DiffUtil后,改为如下代码:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(oldDatas, newDatas), true); diffResult.dispatchUpdatesTo(mAdapter);它会自动计算新老数据集的差异,并根据差异情况,调用以下四个方法:
adapter.notifyItemRangeInserted(position, count); adapter.notifyItemRangeRemoved(position, count); adapter.notifyItemMoved(fromPosition, toPosition); adapter.notifyItemRangeChanged(position, count, payload);显然,这个四个方法在执行时都是伴有RecyclerView的动画的,且都是定向刷新方法,效率明显是会提升不少。
实践demo
(1)新建一个JavaBean类,列表item的数据,代码如下:
class MyBean implements Cloneable { private String name; private String desc; private int pic; @Override public MyBean clone() throws CloneNotSupportedException { MyBean bean = null; try { bean = (MyBean) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return bean; } public MyBean(String name, String desc, int pic) { this.name = name; this.desc = desc; this.pic = pic; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public int getPic() { return pic; } public void setPic(int pic) { this.pic = pic; } }(2)实现recyclerview对应的adapter,代码如下:
import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffViewHolder> { private final static String TAG = "DiffAdapter"; private List<MyBean> mDatas; private Context mContext; private LayoutInflater mInflater; public DiffAdapter(Context mContext, List<MyBean> mDatas) { this.mContext = mContext; this.mDatas = mDatas; mInflater = LayoutInflater.from(this.mContext); } public void setDatas(List<MyBean> mDatas) { this.mDatas = mDatas; } @Override public DiffViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new DiffViewHolder(mInflater.inflate(R.layout.item_diff, parent, false)); } @Override public void onBindViewHolder(final DiffViewHolder holder, final int position) { MyBean bean = mDatas.get(position); holder.tv1.setText(bean.getName()); holder.tv2.setText(bean.getDesc()); holder.iv.setImageResource(bean.getPic()); } @Override public int getItemCount() { return mDatas != null ? mDatas.size() : 0; } class DiffViewHolder extends RecyclerView.ViewHolder { TextView tv1, tv2; ImageView iv; public DiffViewHolder(View itemView) { super(itemView); tv1 = itemView.findViewById(R.id.text1); tv2 = itemView.findViewById(R.id.text2); iv = itemView.findViewById(R.id.img1); } } }(3)item对应的layout布局文件,R.layout.item_diff代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:orientation="horizontal" android:layout_height="wrap_content"> <TextView android:id="@+id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android:id="@+id/img1" android:layout_width="50dp" android:layout_height="wrap_content"/> </LinearLayout>(4)新建activity代码如下:
import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.os.Bundle; import android.view.View; import java.util.ArrayList; import java.util.List; public class DiffUtilActivity extends AppCompatActivity { private List<MyBean> mDatas; private RecyclerView mRv; private DiffAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_diff_util); initData(); mRv = findViewById(R.id.recyclerView1); mRv.setLayoutManager(new LinearLayoutManager(this)); mAdapter = new DiffAdapter(this, mDatas); mRv.setAdapter(mAdapter); } private void initData() { mDatas = new ArrayList<>(); mDatas.add(new MyBean("测试1", "Android", R.drawable.apple)); mDatas.add(new MyBean("测试2", "Java", R.drawable.ball)); mDatas.add(new MyBean("测试3", "C", R.drawable.tao)); mDatas.add(new MyBean("测试4", "PHP", R.drawable.apple)); mDatas.add(new MyBean("测试5", "Python", R.drawable.ball)); } /** * 模拟刷新操作 * * @param view */ public void onRefresh(View view) { try { // 模拟数据更新 List<MyBean> newDatas = new ArrayList<>(); for (MyBean bean : mDatas) { newDatas.add(bean.clone()); } newDatas.add(new MyBean("李小龙", "帅", R.drawable.tao)); newDatas.get(0).setDesc("Android+"); MyBean myBean = newDatas.get(1); newDatas.remove(myBean); newDatas.add(myBean); //别忘了将新数据给Adapter mDatas = newDatas; mAdapter.setDatas(mDatas); // 通知数据变化刷新 mAdapter.notifyDataSetChanged(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }(5)activity对应的layout布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".activity.diffUtilTest.DiffUtilActivity" android:orientation="vertical" tools:ignore="MissingDefaultResource"> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:orientation="vertical" android:layout_height="wrap_content"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView1" android:layout_width="match_parent" android:layout_height="wrap_content"/> <Button android:id="@+id/btnRefresh" android:text="模拟刷新" android:onClick="onRefresh" android:layout_gravity="center_horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> </ScrollView> </LinearLayout>demo分析 我们在activity中模拟数据修改,通知recyclerview适配数据修改,使用的方法是:
// 新数据给Adapter mDatas = newDatas; mAdapter.setDatas(mDatas); // 通知数据变化刷新 mAdapter.notifyDataSetChanged();这个方法,基本属于全局重新刷新数据,Android studio也会提示
DiffUtil优化方案
(1)新建我们自己的DiffUtil.Callback,代码如下:
import androidx.recyclerview.widget.DiffUtil; import java.util.List; public class DiffUtilCallBack extends DiffUtil.Callback { private List<MyBean> mOldDatas; private List<MyBean> mNewDatas; public DiffUtilCallBack(List<MyBean> mOldDatas, List<MyBean> mNewDatas) { this.mOldDatas = mOldDatas; this.mNewDatas = mNewDatas; } /** * 老数据集size */ @Override public int getOldListSize() { return mOldDatas != null ? mOldDatas.size() : 0; } /** * 新数据集size */ @Override public int getNewListSize() { return mNewDatas != null ? mNewDatas.size() : 0; } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName()); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { MyBean beanOld = mOldDatas.get(oldItemPosition); MyBean beanNew = mNewDatas.get(newItemPosition); if (!beanOld.getDesc().equals(beanNew.getDesc())) { // 如果有内容不同,就返回false return false; } if (beanOld.getPic() != beanNew.getPic()) { // 如果有内容不同,就返回false return false; } // 默认两个data内容是相同的 return true; } }(2)activity中修改数据刷新方法,代码如下:
import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.os.Bundle; import android.view.View; import java.util.ArrayList; import java.util.List; public class DiffUtilActivity extends AppCompatActivity { private List<MyBean> mDatas; private RecyclerView mRv; private DiffAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_diff_util); initData(); mRv = findViewById(R.id.recyclerView1); mRv.setLayoutManager(new LinearLayoutManager(this)); mAdapter = new DiffAdapter(this, mDatas); mRv.setAdapter(mAdapter); } private void initData() { mDatas = new ArrayList<>(); mDatas.add(new MyBean("测试1", "Android", R.drawable.apple)); mDatas.add(new MyBean("测试2", "Java", R.drawable.ball)); mDatas.add(new MyBean("测试3", "C", R.drawable.tao)); mDatas.add(new MyBean("测试4", "PHP", R.drawable.apple)); mDatas.add(new MyBean("测试5", "Python", R.drawable.ball)); } /** * 模拟刷新操作 * * @param view */ public void onRefresh(View view) { try { // 模拟数据更新 List<MyBean> newDatas = new ArrayList<>(); for (MyBean bean : mDatas) { newDatas.add(bean.clone()); } newDatas.add(new MyBean("李小龙", "帅", R.drawable.tao)); newDatas.get(0).setDesc("Android+"); MyBean myBean = newDatas.get(1); newDatas.remove(myBean); newDatas.add(myBean); // 1、之前的方案******************************************* // // 新数据给Adapter // mDatas = newDatas; // mAdapter.setDatas(mDatas); // // 通知数据变化刷新 // mAdapter.notifyDataSetChanged(); ///2、DiffUtil的方式通知数据刷新****************************** DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtilCallBack(mDatas, newDatas), true); diffResult.dispatchUpdatesTo(mAdapter); mDatas = newDatas; mAdapter.setDatas(mDatas); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }demo分析: 使用新方案,当有数据需要刷新时,刷新数据的效率更高,并且数据的刷新过程可以看到有意思的动画,有兴趣可以实操感受下。