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 l
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);
}
}
}
}
}
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...
Add the following line to onResume()
listview.setAdapter(listview.getAdapter());