ListView not refreshing already-visible items
Asked Answered
W

3

17

I'm displaying a list of contacts (name + picture) using the ListView. In order to make the initial load fast, I only load the names first, and defer picture loading. Now, whenever my background thread finishes loading a picture, it schedules my adapter's notifyDataSetChanged() to be called on the UI thread. Unfortunately, when this happens the ListView does not re-render (i.e. call getView() for) the items that are already on-screen. Because of this, the user doesn't see the newly-loaded picture, unless they scroll away and back to the same set of items, so that the views get recycled. Some relevant bits of code:

private final Map<Long, Bitmap> avatars = new HashMap<Long, Bitmap>();

// this is called *on the UI thread* by the background thread
@Override
public void onAvatarLoaded(long contactId, Bitmap avatar) {
    avatars.put(requestCode, avatar);
    notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // snip...
    final Bitmap avatar = avatars.get(contact.id);
    if (avatar != null) {
        tag.avatar.setImageBitmap(avatar);
        tag.avatar.setVisibility(View.VISIBLE);
        tag.defaultAvatar.setVisibility(View.GONE);
    } else {
        tag.avatar.setVisibility(View.GONE);
        tag.defaultAvatar.setVisibility(View.VISIBLE);
        if (!avatars.containsKey(contact.id)) {
            avatars.put(contact.id, null);
            // schedule the picture to be loaded
            avatarLoader.addContact(contact.id, contact.id);
        }
    }
}

AFAICT, if you assume that notifyDataSetChanged() causes the on-screen items to be re-created, my code is correct. However, it seems that is not true, or maybe I'm missing something. How can I make this work smoothly?

Washedup answered 28/10, 2013 at 12:47 Comment(8)
@Washedup ... better take a look on some ImageLoader source ...1. set ID to the tag of ImageView 2. run loader with ImageView(as week ref) and ID as params 3. when loader finish dowloading and decode bitmap pass ImageView, Id and bitmap to some function runned on UI thread 4. compare Tag from ImageView with ID (since ImageView could be reused) if they are the same set ImageView with Bitmap ... you can also try to call ListView.invalidateViews() for update visible LV itemsInflexible
@MohanKrishna I'm using a class derived from BaseAdapter, and I'm already calling notifyDataSetChanged() manually. So that answer does not apply to me.Washedup
Maybe try calling notifyDataSetInvalidated()Mix
@Inflexible I also thought about keeping track of my ImageViews and changing their bitmap directly, without relying on getView. But it seems rather messy. Especially since my actual code is slightly more complicated as I'm not changing just the ImageView, I'm changing other stuff as well (multiple data sources). I'd rather have all my item-render code in getView.Washedup
@AndrewSchuster have you read the JavaDoc on that? Here's what it says: Notifies the attached observers that the underlying data is no longer valid or available. Once invoked this adapter is no longer valid and should not report further data set changes. -- I'd like to keep using my adapter.Washedup
Now that I think about it, I remember having similar issues regarding the update of a list. What version of Android are you using?Mix
@AndrewSchuster 4.3 (Nexus 4). I just tried calling invalidateViews() on the ListView after notifyDataSetChanged(), but it seems the JavaDoc is confusing, as it still doesn't actually redraw the already on-screen items.Washedup
@Washedup This issue seems very similar to what I have (post here: #25243841 ) . Do you also have the notifyDataSetChanged update all views except for the view that got a touch event ?Snowbird
W
22

Here I go answering my own question with a hackaround that I've settled on. Apparently, notifyDataSetChanged() is only to be used if you are adding / removing items. If you are updating information about items that are already displayed, you might end up with visible items not updating their visual appearance (getView() not being called on your adapter).

Furthermore, calling invalidateViews() on the ListView doesn't seem to work as advertised. I still get the same glitchy behavior with getView() not being called to update on-screen items.

At first I thought the issue was caused by the frequency at which I called notifyDataSetChanged() / invalidateViews() (very fast, due to updates coming from different sources). So I've tried throttling calls to these methods, but still to no avail.

I'm still not 100% sure this is the platform's fault, but the fact that my hackaround works seems to suggest so. So, without further ado, my hackaround consists in extending the ListView to refresh visible items. Note that this only works if you're properly using the convertView in your adapter and never returning a new View when a convertView was passed. For obvious reasons:

public class ProperListView extends ListView {

    private static final String TAG = ProperListView.class.getName();

    @SuppressWarnings("unused")
    public ProperListView(Context context) {
        super(context);
    }

    @SuppressWarnings("unused")
    public ProperListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @SuppressWarnings("unused")
    public ProperListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();

            refreshVisibleViews();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();

            refreshVisibleViews();
        }
    }

    private DataSetObserver mDataSetObserver = new AdapterDataSetObserver();
    private Adapter mAdapter;

    @Override
    public void setAdapter(ListAdapter adapter) {
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
        mAdapter = adapter;

        mAdapter.registerDataSetObserver(mDataSetObserver);
    }

    void refreshVisibleViews() {
        if (mAdapter != null) {
            for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition(); i ++) {
                final int dataPosition = i - getHeaderViewsCount();
                final int childPosition = i - getFirstVisiblePosition();
                if (dataPosition >= 0 && dataPosition < mAdapter.getCount()
                        && getChildAt(childPosition) != null) {
                    Log.v(TAG, "Refreshing view (data=" + dataPosition + ",child=" + childPosition + ")");
                    mAdapter.getView(dataPosition, getChildAt(childPosition), this);
                }
            }
        }
    }

}
Washedup answered 29/10, 2013 at 10:42 Comment(3)
After doing all the above stuffs you mentioned, I came to this page and this solution did work. And it did save some of my hairs.Pathic
Update: this solution was little inconsistent but when i posted (post >> runnable >> refreshVisibleViews) it became stable.Pathic
It was tough to understand adapters at first. One thing i didn't understand when using adapters is how they work in the space after it is created. You create one, with the list of items that fill up the list for isntance. It references arrayLists or variables and when you make changes to them - like adding a new student object to example - you need to tell it using notifyDataSetChanged() that you did. From there it rebuilds itself (parts of itself, correct me here if its the entire adapter or it knows exactly what changed) to be up to date.Slimsy
F
4

Add the following line to onResume() listview.setAdapter(listview.getAdapter());

Faceharden answered 3/12, 2014 at 16:43 Comment(1)
This approach helped me with recycler view. I basically did recyclerview.setAdapter(adapterObj);Forint
W
1

According to the documentation:

void notifyDataSetChanged ()

Notify any registered observers that the data set has changed. ... LayoutManagers will be forced to fully rebind and relayout all visible views...

In my case, the items were not visible (then whole RecycleView was outside the screen), and later on when it animated in, the item views didn't refresh either (thus showing the old data).

Workaround in the Adapter class:

public void notifyDataSetChanged_fix() {
    // unfortunately notifyDataSetChange is declared final, so cannot be overridden. 
    super.notifyDataSetChanged();
    for (int i = getItemCount()-1; i>=0; i--) notifyItemChanged(i);
}

Replaced all calls of notifyDataSetChanged() to notifyDataSetChanged_fix() and my RecyclerView happily refreshing ever since...

Wilonah answered 17/9, 2017 at 23:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.