如何更新RecyclerView适配器数据

392

我正在尝试找出更新 RecyclerView 适配器的问题。

当我获取到新产品列表时,我尝试:

  1. Update the ArrayList from the fragment where recyclerView is created, set new data to adapter, and then call adapter.notifyDataSetChanged(); it did not work.

  2. Create a new adapter, as others did, and it worked for them, but there wasn't any change for me: recyclerView.setAdapter(new RecyclerViewAdapter(newArrayList))

  3. Create a method in Adapter which updates the data as follows:

     public void updateData(ArrayList<ViewModel> viewModels) {
        items.clear();
        items.addAll(viewModels);
        notifyDataSetChanged();
     }
    

    Then I call this method whenever I want to update the data list; it did not work.

  4. To check if I can modify the recyclerView in any way, and I tried to remove at least an item:

      public void removeItem(int position) {
         items.remove(position);
         notifyItemRemoved(position);
     }
    

一切都保持不变。

这是我的适配器:

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> implements View.OnClickListener {

    private ArrayList<ViewModel> items;
    private OnItemClickListener onItemClickListener;

    public RecyclerViewAdapter(ArrayList<ViewModel> items) {
        this.items = items;
    }


    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycler, parent, false);
        v.setOnClickListener(this);
        return new ViewHolder(v);
    }

    public void updateData(ArrayList<ViewModel> viewModels) {
        items.clear();
        items.addAll(viewModels);
        notifyDataSetChanged();
    }
    public void addItem(int position, ViewModel viewModel) {
        items.add(position, viewModel);
        notifyItemInserted(position);
    }

    public void removeItem(int position) {
        items.remove(position);
        notifyItemRemoved(position);
    }


    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        ViewModel item = items.get(position);
        holder.title.setText(item.getTitle());
        Picasso.with(holder.image.getContext()).load(item.getImage()).into(holder.image);
        holder.price.setText(item.getPrice());
        holder.credit.setText(item.getCredit());
        holder.description.setText(item.getDescription());

        holder.itemView.setTag(item);
    }


    @Override
    public int getItemCount() {
        return items.size();
    }


    @Override
    public void onClick(final View v) {
        // Give some time to the ripple to finish the effect
        if (onItemClickListener != null) {
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    onItemClickListener.onItemClick(v, (ViewModel) v.getTag());
                }
            }, 0);
        }
    }

    protected static class ViewHolder extends RecyclerView.ViewHolder {
        public ImageView image;
        public TextView price, credit, title, description;

        public ViewHolder(View itemView) {
            super(itemView);
            image = (ImageView) itemView.findViewById(R.id.image);
            price = (TextView) itemView.findViewById(R.id.price);
            credit = (TextView) itemView.findViewById(R.id.credit);
            title = (TextView) itemView.findViewById(R.id.title);
            description = (TextView) itemView.findViewById(R.id.description);
        }
    }

    public interface OnItemClickListener {

        void onItemClick(View view, ViewModel viewModel);

    }
}

我按照以下方式初始化RecyclerView

recyclerView = (RecyclerView) view.findViewById(R.id.recycler);
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 5));
adapter = new RecyclerViewAdapter(items);
adapter.setOnItemClickListener(this);
recyclerView.setAdapter(adapter);

那么,我该如何更新适配器数据以显示新接收到的项目?


问题在于 gridView 所在的布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:tag="catalog_fragment"
    android:layout_height="match_parent">

    <FrameLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"/>

        <ImageButton
            android:id="@+id/fab"
            android:layout_gravity="top|end"
            style="@style/FabStyle"/>

    </FrameLayout>
</LinearLayout>

然后我只需删除LinearLayout,并将FrameLayout作为父布局。


1
items.clear(); items.addAll(newItems); 这是一个丑陋的模式。如果你真的需要在这里进行防御性复制,那么 items = new ArrayList(newItems); 会更好看一些。 - Miha_x64
3
你是对的 - 这样会不那么丑陋。问题在于,这并行不起作用。适配器没有引用参考,它得到的是引用本身。因此,如果你构建一个新数据集,它有一个新的引用,而适配器仍然只知道旧的引用。 - The incredible Jan
16个回答

556

这是一个通用的答案。解释了更新适配器数据的各种方式。每次过程包括两个主要步骤:

  1. 更新数据集
  2. 通知适配器变更

插入单个项目

在索引2处添加“Pig”。

Insert single item
String item = "Pig";
int insertIndex = 2;
data.add(insertIndex, item);
adapter.notifyItemInserted(insertIndex);

插入多个项目

在索引2处插入三个动物。

Insert multiple items
ArrayList<String> items = new ArrayList<>();
items.add("Pig");
items.add("Chicken");
items.add("Dog");
int insertIndex = 2;
data.addAll(insertIndex, items);
adapter.notifyItemRangeInserted(insertIndex, items.size());

移除单个项目

从列表中移除“Pig”。

Remove single item
int removeIndex = 2;
data.remove(removeIndex);
adapter.notifyItemRemoved(removeIndex);

删除多个项目

从列表中删除“骆驼”和“羊”。

Remove multiple items
int startIndex = 2; // inclusive
int endIndex = 4;   // exclusive
int count = endIndex - startIndex; // 2 items will be removed
data.subList(startIndex, endIndex).clear();
adapter.notifyItemRangeRemoved(startIndex, count);

删除所有项目

清空整个列表。

删除所有项目
data.clear();
adapter.notifyDataSetChanged();

用新列表替换旧列表

清空旧列表,然后添加新列表。

Replace old list with new list
// clear old list
data.clear();

// add new list
ArrayList<String> newList = new ArrayList<>();
newList.add("Lion");
newList.add("Wolf");
newList.add("Bear");
data.addAll(newList);

// notify adapter
adapter.notifyDataSetChanged();

adapter 引用了 data,因此重要的是我没有将 data 设置为新对象。相反,我清除了旧项,然后添加了新项。

更新单个项目

更改"Sheep"项目,使其显示"我喜欢绵羊。"

Update single item
String newValue = "I like sheep.";
int updateIndex = 3;
data.set(updateIndex, newValue);
adapter.notifyItemChanged(updateIndex);

移动单个项目

将“Sheep”从位置3移动到位置1

移动单个项目
int fromPosition = 3;
int toPosition = 1;

// update data array
String item = data.get(fromPosition);
data.remove(fromPosition);
data.add(toPosition, item);

// notify adapter
adapter.notifyItemMoved(fromPosition, toPosition);

代码

以下是供您参考的项目代码。 RecyclerView 适配器的代码可以在此答案中找到。

MainActivity.java

public class MainActivity extends AppCompatActivity implements MyRecyclerViewAdapter.ItemClickListener {

    List<String> data;
    MyRecyclerViewAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // data to populate the RecyclerView with
        data = new ArrayList<>();
        data.add("Horse");
        data.add("Cow");
        data.add("Camel");
        data.add("Sheep");
        data.add("Goat");

        // set up the RecyclerView
        RecyclerView recyclerView = findViewById(R.id.rvAnimals);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
                layoutManager.getOrientation());
        recyclerView.addItemDecoration(dividerItemDecoration);
        adapter = new MyRecyclerViewAdapter(this, data);
        adapter.setClickListener(this);
        recyclerView.setAdapter(adapter);
    }

    @Override
    public void onItemClick(View view, int position) {
        Toast.makeText(this, "You clicked " + adapter.getItem(position) + " on row number " + position, Toast.LENGTH_SHORT).show();
    }

    public void onButtonClick(View view) {
        insertSingleItem();
    }

    private void insertSingleItem() {
        String item = "Pig";
        int insertIndex = 2;
        data.add(insertIndex, item);
        adapter.notifyItemInserted(insertIndex);
    }

    private void insertMultipleItems() {
        ArrayList<String> items = new ArrayList<>();
        items.add("Pig");
        items.add("Chicken");
        items.add("Dog");
        int insertIndex = 2;
        data.addAll(insertIndex, items);
        adapter.notifyItemRangeInserted(insertIndex, items.size());
    }

    private void removeSingleItem() {
        int removeIndex = 2;
        data.remove(removeIndex);
        adapter.notifyItemRemoved(removeIndex);
    }

    private void removeMultipleItems() {
        int startIndex = 2; // inclusive
        int endIndex = 4;   // exclusive
        int count = endIndex - startIndex; // 2 items will be removed
        data.subList(startIndex, endIndex).clear();
        adapter.notifyItemRangeRemoved(startIndex, count);
    }

    private void removeAllItems() {
        data.clear();
        adapter.notifyDataSetChanged();
    }

    private void replaceOldListWithNewList() {
        // clear old list
        data.clear();

        // add new list
        ArrayList<String> newList = new ArrayList<>();
        newList.add("Lion");
        newList.add("Wolf");
        newList.add("Bear");
        data.addAll(newList);

        // notify adapter
        adapter.notifyDataSetChanged();
    }

    private void updateSingleItem() {
        String newValue = "I like sheep.";
        int updateIndex = 3;
        data.set(updateIndex, newValue);
        adapter.notifyItemChanged(updateIndex);
    }

    private void moveSingleItem() {
        int fromPosition = 3;
        int toPosition = 1;

        // update data array
        String item = data.get(fromPosition);
        data.remove(fromPosition);
        data.add(toPosition, item);

        // notify adapter
        adapter.notifyItemMoved(fromPosition, toPosition);
    }
}

注意事项

  • 如果您使用notifyDataSetChanged(),则不会执行任何动画效果。此操作也可能是昂贵的,因此如果只更新单个项目或一系列项目,则不建议使用notifyDataSetChanged()
  • 如果您正在对列表进行大规模或复杂的更改,请查看DiffUtil

进一步学习


7
太棒了!如果要替换一项并将其结果显示为不同的视图类型,应该采取哪些步骤?只需要使用notifyItemChanged还是需要组合使用remove和insert呢? - Semaphor
当我们使用DiffUtils进行更改时,效果如何?它们不像手动调用.notify<Action>()那样流畅。 - GuilhE
2
在“更新单个项目”下真的失误了,应该是我喜欢海龟 - ardnew
嗨,我还是个新手。有没有办法更新在视图上显示的索引?我的问题是,我必须在RecyclerView中显示每个项目的索引。因此,当我进行删除或插入操作时,索引不会更新,例如,我有一个列表{"one", "two", "three", "four"},并且我在索引1处(包含元素“two”)进行了删除操作,我希望RecyclerView更新显示元素“three”,“four”等的索引视图。这是我不想发生的事情:https://i.imgur.com/rLjyp1W.png - LuckMan
1
@LuckMan,我建议你提出一个新问题。我现在主要使用Flutter,所以我有点忘记如何进行Android开发的细节了。 - Suragch
显示剩余9条评论

396
我正在使用RecyclerView,删除和更新都很顺利。
  1. 移除

    从 RecyclerView 中移除一个项有四个步骤

     list.remove(position);
     recycler.removeViewAt(position);
     mAdapter.notifyItemRemoved(position);
     mAdapter.notifyItemRangeChanged(position, list.size());
    

    这些代码对我有效。

  2. 更新所有数据

    我只需要做以下几件事:

     mAdapter.notifyDataSetChanged();
    

    这没有动画,因为它影响所有项。

你应该在管理“数据”的地方完成所有这些操作,比如在Activity/Fragment的代码中,除非你的RecyclerView的Adapter本身就是“数据管理器”(并且具有像“remove(int pos)”和“add(...)”这样的方法)。

58
你只需要这两行代码:list.remove(position); mAdapter.notifyItemRemoved(position); 它们的作用是从列表中移除指定位置的项并通知适配器刷新数据。 - feisal
6
对我来说,recycler.removeViewAt(position);(或者更确切地说是recycler.removeAllViews())这两行代码至关重要。 - MSpeed
3
避免在删除过程中出现烦人的故障的一个重要考虑因素: 您不需要调用此行 recycler.removeViewAt(position); - Angelo Nodari
16
如果你要处理非常长的列表,使用NotifyDataSetChanged会过度。为了几个项目重新加载整个列表?不用了,谢谢。此外,recycler.removeViewAt(position)不需要。也不需要同时调用"mAdapter.notifyItemRemoved(position)"和"mAdapter.notifyItemRangeChanged(position, list.size())"。 删除一些或删除一个。仅通知一次。只调用其中一个。 - Vitor Hugo Schwaab
2
RecyclerView在使用notifyItemRemovednotifyItemMoved时,会提供一个漂亮的默认动画来移除和移动项目。但是,如果使用notifyDataSetChanged,则会失去这个动画效果。 :( - Thupten
显示剩余10条评论

50

这是对我有用的方法:

recyclerView.setAdapter(new RecyclerViewAdapter(newList));
recyclerView.invalidate();

在创建包含更新列表的新适配器之后(在我的情况下,它是将数据库转换为ArrayList),并将其设置为适配器后,我尝试了recyclerView.invalidate(),它起作用了。


15
这会不会刷新整个视图,而不仅仅是更新已更改的项目? - TWilly
18
我并不会说中文,但我可以用中文回答您的问题。这是需要翻译的原始内容:“Instead of making a new adapter each time, what I did was to create a setter method in my custom adapter class to set the new list. After that, just call YourAdapter adapter = (YourAdapter) recyclerView.getAdapter(); adapter.yourSetterMethod(newList); adapter.notifyDataSetChanged(); That being said, it sounds like what the OP tried first (just adding this on as a comment so that it may help someone else, as that worked in my case).” - Kevin Lee
5
读者,不要满足于这个答案。这个答案是可行的,但有一种更有效的方法。而不是重新加载所有数据,最有效的方法是将新数据添加到适配器中。 - Sebastialonso
1
是的,我同意@TWilly的观点,它会在UI上重新绘制完整的视图。 - Rahul
如果这是您唯一可行的解决方案,那么您的代码中存在严重的错误。即使您不知道发生了什么变化,notifyDataSetChanged() 应该足以解决问题。如果这样做还不能解决问题,则您的适配器可能存在严重问题(很可能没有正确地重置视图)。同时,invalidate() 是无意义的;在 setAdapter 后它应该会自动发生。 - Ryan M

22

你有两个选项来完成这个任务:

从适配器刷新用户界面:

mAdapter.notifyDataSetChanged();

或者从recyclerView本身刷新:

recyclerView.invalidate();

16

另一种选择是使用DiffUtil。它将比较原始列表和新列表,并在有更改时使用新列表作为更新。

基本上,我们可以使用DiffUtil来比较旧数据和新数据,并让它代表你调用notifyItemRangeRemoved、notifyItemRangeChanged和notifyItemRangeInserted。

以下是使用diffUtil而不是notifyDataSetChanged的快速示例:

DiffResult diffResult = DiffUtil
                .calculateDiff(new MyDiffUtilCB(getItems(), items));

//any clear up on memory here and then
diffResult.dispatchUpdatesTo(this);

//and then, if necessary
items.clear()
items.addAll(newItems)

如果列表很大,我会使用AsyncListDiffer在主线程之外完成calculateDiff工作。


2
虽然这是正确的,但如何更新适配器中的数据呢?假设我在适配器中显示List<Object>,并且我获得了一个新的List<Object>更新。DiffUtil将计算差异并将其分派到适配器中,但它对原始或新列表(在您的情况下为getItems())什么也没做。您如何知道哪些来自原始列表的数据需要被删除/添加/更新? - vanomart
2
也许这篇文章可以帮到你。https://medium.com/@nullthemall/diffutil-is-a-must-797502bc1149#.9e7v4tsv8 - j2emanue
@vanomart,您只需像平常一样直接用新的列表值替换本地列表字段即可。这种方法的唯一区别在于,您不再使用notifyDataSetChanged(),而是使用DiffUtil来更新视图。 - carrizo
@j2emanue,你提到如果是一个大列表,可以在主线程中完成calculateDiff工作。那么这是否可以在适配器中完成,然后仍然正确地更新UI?你有任何示例展示如何在非主线程中完成吗? - AJW
@AJW 可以使用这个来进行异步工作,或者自己设计:https://developer.android.com/reference/android/support/v7/recyclerview/extensions/AsyncListDiffer - j2emanue
显示剩余5条评论

10

更新列表视图、网格视图和可滚动列表视图(recyclerview)的数据:

mAdapter.notifyDataSetChanged();
或者:
mAdapter.notifyItemRangeChanged(0, itemList.size());

这似乎直到用户开始滚动才更新我的数据。我已经到处找了,但不知道原因。 - SudoPlz
@SudoPlz 你可以使用自定义滚动监听器来检测滚动并执行通知操作,就像这个链接中的示例 https://dev59.com/ceo6XIcBkEYKwwoYFQHr#31174967 - Pankaj Talaviya
谢谢Pankaj,但这正是我想避免的。 我想要更新所有内容,而不需要用户触摸屏幕。 - SudoPlz

7

我用一种不同的方式解决了同样的问题。 我没有数据。 我正在等待来自后台线程的数据,因此从空列表开始。

mAdapter = new ModelAdapter(getContext(), new ArrayList<Model>());
// Then when I get data

mAdapter.update(response.getModelList());
// And update it in my adapter

public void update(ArrayList<Model> modelList){
    adapterModelList.clear();
    for (Product model: modelList) {
        adapterModelList.add(model);
    }
   mAdapter.notifyDataSetChanged();
}

就是这样。


1
我不理解。你的代码对我来说毫无意义。为什么你要循环遍历一个列表,将它的项添加到另一个列表中?而且为什么“Product”类与“Model”类兼容呢?为什么不直接调用“mAdapter = new ModelAdapter(getContext(),response.getModelList());”或一次性复制响应中的列表呢? - The incredible Jan
addAll中出现错误。 - YaMiN

7
我建议您探索 DiffUtil。它可以在处理列表更新时提高 RecyclerView 的性能。
  1. Define a variable inside your Adapter:

     differList = AsyncListDiffer(this, this.callback);     
     differList.submitList(list)
    

这里的列表可以是您最初的原始列表,也可以是一个空列表,只要您稍后会对其进行更新。

  1. Implementing callback:

    private val callback : DiffUtil.ItemCallback<Item> = object: DiffUtil.ItemCallback<Item>() {
    
       override fun areItemsTheSame(oldItem: Item, newItem: Item) =
         oldItem.id == newItem.id
    
       override fun areContentsTheSame(oldItem: Item, newItem: Item) =
         oldItem == newItem
     }
    
  2. Also, in the same adapter you will have some public function to set the list.

     fun setData(list: List<Item>) {
            differList.submitList(list)
            //yeah, that's it!
     }
    
  1. 现在,当你对列表进行任何更改(插入/更新/删除)后,只需从你的fragment/activity中调用setData(list)方法即可。

    mAdapter.setData(list)

很简单,对吧?


6

向现有数据添加新数据的最佳和最酷的方式是:

 ArrayList<String> newItems = new ArrayList<String>();
 newItems = getList();
 int oldListItemscount = alcontainerDetails.size();
 alcontainerDetails.addAll(newItems);           
 recyclerview.getAdapter().notifyItemChanged(oldListItemscount+1, al_containerDetails);

4
我看到很多人使用“notifyDataSetChanged”来更新列表,这不会太过分了吗?我是说,因为几个项目的更改或添加而重新加载整个列表...我喜欢这种方法,正在实现它,使用“notifyItemRangeInserted”方法。 - Vitor Hugo Schwaab

3
我发现一种非常简单的重新加载RecyclerView的方法只需要调用:
recyclerView.removeAllViews();

这将首先移除RecyclerView的所有内容,然后使用更新后的值再次添加。

9
请不要使用此功能,否则会招来麻烦。 - dell116

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接