RecyclerView上的notifyDataSetChanged无效

63

我从服务器获取数据,然后解析并存储在一个列表中。我在RecyclerView的适配器中使用这个列表,同时也使用了Fragment。

我正在使用装有KitKat系统的Nexus 5设备。我使用支持库来完成这个项目。这会有什么影响吗?

以下是我的代码:(针对问题使用了虚拟数据)

成员变量:

List<Business> mBusinesses = new ArrayList<Business>();

RecyclerView recyclerView;
RecyclerView.LayoutManager mLayoutManager;
BusinessAdapter mBusinessAdapter;

我的 onCreateView()

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

    // Getting data from server
    getBusinessesDataFromServer();

    View view = inflater.inflate(R.layout.fragment_business_list,
            container, false);
    recyclerView = (RecyclerView) view
            .findViewById(R.id.business_recycler_view);
    recyclerView.setHasFixedSize(true);

    mLayoutManager = new LinearLayoutManager(getActivity());
    recyclerView.setLayoutManager(mLayoutManager);

    mBusinessAdapter = new BusinessAdapter(mBusinesses);
    recyclerView.setAdapter(mBusinessAdapter);

    return view;
}
从服务器获取数据后,调用parseResponse()函数。
protected void parseResponse(JSONArray response, String url) {
    // insert dummy data for demo

    mBusinesses.clear();

    Business business;

    business = new Business();
    business.setName("Google");
    business.setDescription("Google HeadQuaters");
    mBusinesses.add(business);

    business = new Business();
    business.setName("Yahoo");
    business.setDescription("Yahoo HeadQuaters");
    mBusinesses.add(business);

    business = new Business();
    business.setName("Microsoft");
    business.setDescription("Microsoft HeadQuaters");
    mBusinesses.add(business);

    Log.d(Const.DEBUG, "Dummy Data Inserted\nBusinesses Length: "
            + mBusinesses.size());

    mBusinessAdapter = new BusinessAdapter(mBusinesses);
    mBusinessAdapter.notifyDataSetChanged();
}

我的 BusinessAdapter:

public class BusinessAdapter extends
    RecyclerView.Adapter<BusinessAdapter.ViewHolder> {

    private List<Business> mBusinesses = new ArrayList<Business>();

    // Provide a reference to the type of views that you are using
    // (custom viewholder)
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextViewName;
        public TextView mTextViewDescription;
        public ImageView mImageViewLogo;

        public ViewHolder(View v) {
            super(v);
            mTextViewName = (TextView) v
                    .findViewById(R.id.textView_company_name);
            mTextViewDescription = (TextView) v
                    .findViewById(R.id.textView_company_description);
            mImageViewLogo = (ImageView) v
                    .findViewById(R.id.imageView_company_logo);
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public BusinessAdapter(List<Business> myBusinesses) {

        Log.d(Const.DEBUG, "BusinessAdapter -> constructor");

        mBusinesses = myBusinesses;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public BusinessAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
            int viewType) {

        Log.d(Const.DEBUG, "BusinessAdapter -> onCreateViewHolder()");

        // create a new view
        View v = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.item_business_list, parent, false);

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element

        Log.d(Const.DEBUG, "BusinessAdapter -> onBindViewHolder()");

        Business item = mBusinesses.get(position);
        holder.mTextViewName.setText(item.getName());
        holder.mTextViewDescription.setText(item.getDescription());
        holder.mImageViewLogo.setImageResource(R.drawable.ic_launcher);

    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {

        Log.d(Const.DEBUG, "BusinessAdapter -> getItemCount()");

        if (mBusinesses != null) {
            Log.d(Const.DEBUG, "mBusinesses Count: " + mBusinesses.size());
            return mBusinesses.size();
        }
        return 0;
    }
}

但是我在视图中没有显示数据,我做错了什么?

这是我的日志,

07-14 21:15:35.669: D/xxx(2259): Dummy Data Inserted
07-14 21:15:35.669: D/xxx(2259): Businesses Length: 3
07-14 21:26:26.969: D/xxx(2732): BusinessAdapter -> constructor

在这之后我没有得到任何日志。适配器中的getItemCount()不应该再次被调用吗?

9个回答

84
在你的parseResponse()中,你创建了一个 BusinessAdapter 类的新实例,但是在任何地方都没有使用它,所以你的 RecyclerView 不知道新实例的存在。
你需要做以下其中之一:
  • 调用 recyclerView.setAdapter(mBusinessAdapter) 再次更新 RecyclerView 的适配器引用指向你的新适配器。
  • 或者只需移除 mBusinessAdapter = new BusinessAdapter(mBusinesses); 以继续使用现有的适配器。因为你没有改变 mBusinesses 的引用,适配器仍将使用该数组列表,并在调用notifyDataSetChanged()时正确更新。

谢谢你的帮助!!!明白了...只是删除了创建新实例的那一行。8分钟后会接受你的答案。 - Vamsi Challa
4
这对我没有用,我不得不重新创建适配器并将其设置到RecyclerView中。 - Jono
1
我正在按照你的第二点,通过创建新的BusinessAdapter(mBusinesses)来再次创建适配器。我需要以任何方式释放旧的适配器吗?如果我每次都创建一个新的适配器,这会导致内存泄漏吗? - waylonion
2
只要您没有静态引用任何内容或保留引用,就可以放弃旧引用。 - Bryan Herbst
嗨@Tanis.7x,我遇到了类似的问题。我也在stackoverflow上发布了一个问题http://stackoverflow.com/questions/43893288/how-do-i-refresh-a-recyclerview-in-android。希望你能帮助我。谢谢。 - anup
显示剩余2条评论

34

尝试这个方法:

List<Business> mBusinesses2 = mBusinesses;
mBusinesses.clear();
mBusinesses.addAll(mBusinesses2);
//and do the notification

可能会花费一些时间,但这应该能够起作用。


3
这个声明:List<Business> mBusinesses2 = mBusinesses; 并没有对列表进行深拷贝,因此你没有克隆该列表。这是第一点,第二点是我不明白这样做如何解决原来的问题... - kosiara - Bartosz Kosarzycki
1
其实我曾经遇到过类似的问题,我是这样解决的: 解决方案 - kosiara - Bartosz Kosarzycki
@kosiara-BartoszKosarzycki 但它确实做到了。 - NiVeR

16

作为其他答案的补充,我认为在这里没有人提到:notifyDataSetChanged() 应该在主线程上执行(当然,RecyclerView.Adapter 的其他notify<Something>方法也是如此)。

据我所知,由于您将解析过程和对notifyDataSetChanged()的调用放在同一块中,因此您要么从工作线程调用它,要么在主线程上进行JSON解析(这也是不可取的,我相信您知道)。因此,正确的方式应该是:

protected void parseResponse(JSONArray response, String url) {
    // insert dummy data for demo
    // <yadda yadda yadda>
    mBusinessAdapter = new BusinessAdapter(mBusinesses);
    // or just use recyclerView.post() or [Fragment]getView().post()
    // instead, but make sure views haven't been destroyed while you were
    // parsing
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        public void run() {
            mBusinessAdapter.notifyDataSetChanged();
        }
    });

PS 奇怪的是,我不认为你从IDE或运行时日志中可以得到任何关于主线程的指示。这只是从我的个人观察中得出的结论:如果我从工作线程调用notifyDataSetChanged(),我不会得到只有创建视图层次结构的原始线程才能触摸它的视图之类的必要消息,它只会默默地失败(在我的情况下,一个非主线程调用甚至可以防止后续的主线程调用正常工作,可能是因为某种竞争条件)。

此外,无论是RecyclerView.Adapter API参考还是相关的官方开发指南目前(该时刻是2017年),都没有明确提到主线程要求,而且Android Studio的lint检查规则似乎也没有涉及这个问题。

但是,作者本人在这里对此进行了解释


1
非常感谢!在我尝试的所有方法中,你的方法是完美无缺的。 - user7340499
@Maximus 很高兴我能帮到你,伙计。 - Ivan Bartsov
该死!我没想到notifyDatasetChange监听器需要从主线程调用,太完美了,+1。 - Bawa

4

我曾经也遇到过同样的问题。我通过在类的 onCreate 方法之前将 adapter 声明为公共变量来解决了这个问题。

PostAdapter postAdapter;

之后
postAdapter = new PostAdapter(getActivity(), posts);
recList.setAdapter(postAdapter);

最后我已经打电话了:
@Override
protected void onPostExecute(Void aVoid) {
    super.onPostExecute(aVoid);
    // Display the size of your ArrayList
    Log.i("TAG", "Size : " + posts.size());
    progressBar.setVisibility(View.GONE);
    postAdapter.notifyDataSetChanged();
}

希望这能对你有所帮助。


2

虽然有点奇怪,但是notifyDataSetChanged如果没有为适配器设置新值,它实际上并不起作用。因此,您应该执行以下操作:

array = getNewItems();                    
((MyAdapter) mAdapter).setValues(array);  // pass the new list to adapter !!!
mAdapter.notifyDataSetChanged();       

这对我很有帮助。

1
清除旧的ViewModel并将新数据设置到适配器中,然后调用notifyDataSetChanged()

1
在我的情况下,强制在主UI线程中运行#notifyDataSetChanged将修复问题。
public void refresh() {
        clearSelection();
        // notifyDataSetChanged must run in main ui thread, if run in not ui thread, it will not update until manually scroll recyclerview
        ((Activity) ctx).runOnUiThread(new Runnable() {
            @Override
            public void run() {
                adapter.notifyDataSetChanged();
            }
        });
    }

0

我经常遇到这个问题,就是忘记了RecyclerView每次需要提供一个新的List实例给适配器。

List<X> deReferenced = new ArrayList(myList);
adapter.submitList(deReferenced);

拥有相同的列表(引用)意味着即使列表大小发生变化,也不需要声明“new”,因为对列表所做的更改也会传播到其他列表(当它们仅被声明为this.localOtherList = myList),重点在于关键字“=”,通常比较集合的组件会在事后复制结果并将其存储为“旧”的副本,但Android DiffUtil不会这样做。

因此,如果您的组件每次提交时都提供相同的列表,RecyclerView不会触发新的布局传递。 原因是...据我所记,在DiffUtil甚至尝试应用Mayers算法之前,有一行代码执行:

 if (newList == mList)) {return;}

我不确定在同一系统内解除引用多少次算是“好的实践”...

特别是因为预计差异算法将具有新(修订)与旧(原始)组件,理论上应该在过程结束后自行取消集合的引用,但是...谁知道呢...

但等等,还有更多...

使用new ArrayList()会取消List的引用,但出于某种原因,Oracle决定应该使用相同名称但具有不同功能的第二个“ArrayList”。

这个ArrayList位于Arrays类中。

/**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * <p>This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * <pre>
     *     List&lt;String&gt; stooges = Arrays.asList("Larry", "Moe", "Curly");
     * </pre>
     *
     * @param <T> the class of the objects in the array
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a); //Here
    }

这个写入操作很有趣,因为如果你:

Integer[] localInts = new Integer[]{1, 2, 8};
Consumer<List<Integer>> intObserver;

public void getInts(Consumer<List<Integer>> intObserver) {
    this.intObserver = intObserver;
    dispatch();
}

private void dispatch() {
    List<Integer> myIntegers = Arrays.asList(localInts);
    intObserver.accept(myIntegers);
}
    

稍后再说:

getInts(
    myInts -> {
    adapter.submitList(myInts); //myInts = [1, 2, 8]
    }
);
    

列表派发不仅在每次提交时遵循反引用,而且当localInts变量被改变时,

public void set(int index, Integer value) {
    localInts[index] = value;
    dispatch(); // dispatch again
}

...

myModel.set(1, 4) // localInts = [1, 4, 8]

这个更改也会传递到RecyclerView内的List中,这意味着在下一次提交时,(newList == mList)将返回“false”,从而触发DiffUtils来触发Mayers算法,但是当回调areContentsTheSame(@NonNull T oldItem, @NonNull T newItem)ItemCallback<T>接口到达索引1时,它将抛出一个“true”。基本上,它说“RecyclerView内的索引1(在之前的版本中应该是2)始终为4”,并且布局仍然不会执行。

因此,在这种情况下,正确的方法是:

List<Integer> trulyDereferenced = new ArrayList<>(Arrays.asList(localInts));
adapter.submitList(trulyDereferenced);

0

针对那些使用反向布局(例如聊天列表)的用户的解决方案

在您的适配器类中添加此函数。

fun updateChatList(msgData: DataDetails) { // DataDetails is custom model class
    var tmpdata = ArrayList<DataDetails>()
    tmpdata.add(msgData)
    tmpdata.addAll(data)  //data is your data define in adapter
    data.clear()
    data.addAll(tmpdata)
    this.notifyDataSetChanged()
}

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