7

I could not find something specifically relating to my exact issue, please read on to find out what that is.

I took great care to make sure that everywhere in my code, I am set up right to just call notifyDataSetChanged on the adapter, I initialize the itemList once, and pass that to the adapter, and don't re-initialize it ever.

It works like a charm, and the list view will update itself, but only for new items.

For existing items, the ListView will not update correctly.

For example if I have a listview that is displaying some custom items, and I need to update it I do this

public void updateList(List<item> newItems)
{
    if (adapter == null)
    {
        itemList.addAll(newItems);
        adapter = new SomeAdapter(layoutInflator, itemList);
        listView.setAdapter(adapter);
    } else
    {
        // lets find all the duplicates and do all the updating
        List<item> nonDuplicateItems = new ArrayList<item>();
        for (Item newItem : newItems)
        {
            boolean isDuplicate = false;
            for (Item oldItem : itemList)
            {
                // are these the same item?
                if (newItem.id == oldItem.id)
                {
                    isDuplicate = true;
                    // update the item
                    olditem.text1 = newItem.text1;
                    oldItem.text2 = newItem.text2;
                }
            }

            if (isDuplicate == false)
            {
                // add the new item
                nonDuplicateItems.add(newItem);
            }
        }

        // I have tried just adding these new ones to itemList, 
        // but that doesnt seem to make the listview update the
        // views for the old ones, so I thought thuis might help
        // by clearing, merging, and then adding back
        nonDuplicateItems.addAll(itemList);
        itemList.clear();
        itemList.addAll(nonDuplicateItems);

        // finally notify the adapter/listview
        adapter.notifyDataSetChanged();
    }
}

now the listview will always update to show new items, but it will not update the views on the existing items.

Here is the real kicker that tells me it is an issue with the views: if I call adapter.getItem(position); on a updated pre-existing item, the item returned will show the updated changes, (meaning text1 and text2 will hold their new values) even though it is not reflected in the listview!

If I call listView.invalidateViews(); then the list view will show the updates, but I have two problems with that, sometimes it flickers, and sometimes, just sometimes if I call it and it runs before the notifyDataSetChanged can finish getting through to the listview, I get a "List view not notified of data change" error!

Does anyone know anything about this?

@Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        ViewHolder viewHolder;
        if (convertView == null)
        {
            convertView = layoutInflator.inflate(R.layout.item_comment, null);
                    // when the holder is created it will find the child views
                    // it will then call refreshHolder() on itself
            viewHolder = new ViewHolder(convertView, position);
            convertView.setTag(viewHolder);
        } else
        {
            viewHolder = ((ViewHolder) convertView.getTag());
            viewHolder.refreshHolder(position);
        }
        return convertView;
    }

public void refreshHolder(int position)
{
    this.position = position;
    tvText1.setText(getItem(position).text1);
    tvText2.setText(getItem(position).text2);
}

I wonder if what I should do is re-instantiate all my items before adding the to the list, using a copy constructor. Perhaps when notifying the adapter, the adapter will assume there is no changes if the item is still the same reference, and so will not redraw that view? or perhaps the adapter only draws new views for new items when notified?

To add another detail, if I scroll down making the updated view go off screen, and then come back to it, it displays the correct info as the listview refreshes/remakes that view.

I guess I am needing the listview to refresh all its current views so, invalidateViews(); may be what I have to do.

Does anyone know more about this?

EDIT: As requested here is an adapter that would have this issue.

public class ItemAdapter extends BaseAdapter
{

    private final static int VIEWTYPE_PIC = 1;
    private final static int VIEWTYPE_NOPIC = 0;

    public List<Item> items;
    LayoutInflater layoutInflator;
    ActivityMain activity;

    public ItemAdapter(List<Item> items, LayoutInflater layoutInflator, ActivityMain activity)
    {
        super();
        this.items = new ArrayList<Item>();
        updateItemList(items);
        this.layoutInflator = layoutInflator;
        this.activity = activity;
    }

    public void updateItemList(List<Item> updatedItems)
    {
        if (updatedItems != null && updatedItems.size() > 0)
        {
            // FIND ALL THE DUPLICATES AND UPDATE IF NESSICARY
            List<Item> nonDuplicateItems = new ArrayList<Item>();
            for (Item newItem : updatedItems)
            {
                boolean isDuplicate = false;
                for (Item oldItem : items)
                {
                    if (oldItem.getId().equals(newItem.getId()))
                    {
                        // IF IT IS A DUPLICATE, UPDATE THE EXISTING ONE
                        oldItem.update(newItem);
                        isDuplicate = true;
                        break;
                    }
                }
                // IF IT IS NOT A DUPLICATE, ADD IT TO THE NON-DUPLICATE LIST
                if (isDuplicate == false)
                {
                    nonDuplicateItems.add(newItem);
                }
            }

            // MERGE
            nonDuplicateItems.addAll(items);
            // SORT
            Collections.sort(nonDuplicateItems, new Item.ItemOrderComparator());
            // CLEAR
            this.items.clear();
            // ADD BACK IN
            this.items.addAll(nonDuplicateItems);
            // REFRESH
            notifyDataSetChanged();
        }
    }

    public void removeItem(Item item)
    {
        items.remove(item);
        notifyDataSetChanged();
    }

    @Override
    public int getCount()
    {
        if (items == null)
            return 0;
        else
            return items.size();
    }

    @Override
    public Item getItem(int position)
    {
        if (items == null || position > getCount())
            return null;
        else
            return items.get(position);
    }

    @Override
    public long getItemId(int position)
    {
        return getItem(position).hashCode();
    }

    @Override
    public int getItemViewType(int position)
    {
        Item item = getItem(position);
        if (item.getPhotoURL() != null && URLUtil.isValidUrl(item.getPhotoURL()) == true)
        {
            return VIEWTYPE_PIC;
        }
        return VIEWTYPE_NOPIC;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        ItemHolder itemHolder;
        if (convertView == null)
        {
            if (getItemViewType(position) == VIEWTYPE_PIC)
            {
                convertView = layoutInflator.inflate(R.layout.item_pic, null);
            } else
            {
                convertView = layoutInflator.inflate(R.layout.item, null);
            }
                    // THIS CONSTRUCTOR ALSO CALLS REFRESH ON THE HOLDER FOR US
            itemHolder = new ItemHolder(convertView, position);
            convertView.setTag(itemHolder);
        } else
        {
            itemHolder = ((ItemHolder) convertView.getTag());
            itemHolder.refreshHolder(position);
        }
        return convertView;
    }

    @Override
    public int getViewTypeCount()
    {
        return 2;
    }

    @Override
    public boolean hasStableIds()
    {
        return false;
    }

    @Override
    public boolean isEmpty()
    {
        return (getCount() < 1);
    }

    @Override
    public boolean areAllItemsEnabled()
    {
        return true;
    }

    @Override
    public boolean isEnabled(int position)
    {
        return true;
    }
}

Ok I have now tried this

    @Override
    public boolean hasStableIds()
    {
        return true;
    }

    @Override
    public long getItemId(int position)
    {
        return getItem(position).hashCode();
    }

and this

    @Override
    public boolean hasStableIds()
    {
        return false;
    }

    @Override
    public long getItemId(int position)
    {
        return getItem(position).hashCode();
    }

where my hashcode is a reflection builder from apache used like so (Should work cause the hash changes based on values)

    @Override
    public int hashCode()
    {
        return HashCodeBuilder.reflectionHashCode(this);
    }

and it didn't work. From what I can tell stableIds is doing nothing.

EDIT:

none of these work either, in any combination of stable Ids. Once again, and the same as always, you have to scroll the view offscreen and then back on in order for it to be updated.

listview.refreshDrawableState();
listview.requestLayout();
listview.invalidateViews();
WIllJBD
  • 6,144
  • 3
  • 34
  • 44
  • Please post the code of the Adpter. The `ViewHolder`is a common pattern used enhance performance. It does not have inpact on your issue. – AlexS May 18 '14 at 20:00
  • I know very well what a ViewHolder is, the point of me showing that is because as I have said before, the issue lies in when I call notifyDataSetChanged, the currently visible items do not get updated until you scroll down, and then back up. The underlying data changes, and everything works as expected except for this. I showed the ViewHOlder, because one might ask why is my item not getting visually updated? and I wanted to eliminate my way of implementing getView() as an issue. It has a very impactful purpose when you ask why a view is not getting updated with the new underlying information. – WIllJBD May 19 '14 at 03:52
  • You can try to change the viewHolder that contains every child view of the convertView. I think the viewHolder prevents the view from re-inflate the entier view instead of its children. – Sripathi May 19 '14 at 12:44
  • @Sripathi Why should that be? The viewholder only optimizes the search for childviews by "caching" the references. – AlexS May 20 '14 at 07:01
  • Yes, but using holder for the children will prevent re-inflating the children views not entire view. But now you are using holder for the entire view so there may be a chance to prevent re-inflating the entire row view right? Thats the problem in your case I believe. Try and let me know your result. – Sripathi May 20 '14 at 07:15
  • That would be interesting, I will give it a try (I'm not to sure about it though, but if it works I will eat my words), as well as the ids solution from AlexS. – WIllJBD May 21 '14 at 02:31
  • @WIllJBD I am facing a similar issue in my case I am highlight an item after user interaction. The highlight works for the current item but another item down in the list at random position is also highlighted for your case I guess invalidating the listview first then setting the new adapter and notifydatasetchanged will certainly work. – Illegal Argument May 22 '14 at 01:23
  • @WIllJBD I think you forgot to set stableIds to true in your update: The first two examples are identical. – AlexS May 22 '14 at 06:44
  • oh haha, I forgot to change it, I was just copy pasting – WIllJBD May 22 '14 at 15:34
  • @WIllJBD Check my answer pls, I hope it works for you, despite the downvotes. – kupsef May 24 '14 at 14:52

8 Answers8

7

There is a similar issue here with a solution that may work:

ListView not refreshing already-visible items

Community
  • 1
  • 1
kupsef
  • 3,357
  • 1
  • 21
  • 31
  • Could you please tell me the reason of the downvotes? – kupsef May 24 '14 at 12:34
  • @WIllJBD I guess I can do nothing about the downvotes, so I ask you to consider my answer in case it solves your problem. It's the same as Ginger Head's, and I posted it hours before him. I hope it's just a coincidence that his answer got upvoted, and in the same time mine got downvoted, despite the fact the two answer is basically same. – kupsef May 24 '14 at 14:51
  • I am sorry that this happened, is is basically the same, not sure why it got downvoted, I gave you an upvote to offset the unfairness. Although in your example you did the same thing I did and told me to do different by the comments, maybe they overlooked that? – WIllJBD May 24 '14 at 19:23
  • the method refreshHolder() is called inside the constructor of the ItemHolder, this is why I give it the position in the constrcutor. Unfortunately this isn't it either. :( The issue is not with my code, it is with the way ListView and the Adapter interact. I have been going through the code in the SDK, and I believe that notifyDataSetChanged, although necessary to let the listview know the dataset has changed, does not actually make the listview refresh its current drawable state. This is my issue, and I believe I should be able to fix is by calling refreshdrawablestate. – WIllJBD May 24 '14 at 19:24
  • Well, I found this: http://stackoverflow.com/questions/19635220/listview-not-refreshing-already-visible-items – kupsef May 24 '14 at 19:47
  • That looks very promising, make that your answer. I'll try it out, and if it ends up working you'll have the answer ;) – WIllJBD May 25 '14 at 00:20
  • Alternatively, you could try to remove all items from the list, notify, then add the new items to the list, and finally notify again. Its much simpler to imlement, and should work. – kupsef May 25 '14 at 14:53
  • You need to make you answer the referral. As it stands your answer is wrong, but the comment worked. I would like to accept your comment as the answer (the one referring me to build a custom listview.) – WIllJBD May 25 '14 at 21:31
  • Well, I had to accept your edit, and I did it. I don't understand the reason of the rollback. Now I rolled back to your edit. – kupsef May 25 '14 at 21:50
3

With "unstable IDs" everything should be fine if calling notifyDatasetChanged (), but it seems your ListView doesn't know that some existing items have to be updated.

Perhaps you can try to implement stable ids and misuse them in a way, that the id changes on item updates.

@Override
public long getItemId(int position) {
    return getItem(position).getId();
}

@Override
public boolean hasStableIds() {
    return true;
}

Another "brute force" approach would be to build a new Adapter and set the new adapter for the ListView.

AlexS
  • 5,295
  • 3
  • 38
  • 54
  • I tried having stable id's set to true. My understanding of this is that the stable id should tell the listview when a item is the same item and does not need to be updated. So if the item at position 1 has a stable id that does not change, item 1 will not be refreshed. if the stable id does change, it will be refreshed because a new item is tehere according to the new id. is this correct? – WIllJBD May 19 '14 at 17:59
  • @WIllJBD I think as well that this is the way it should work. Unfortunately this is how you do it right now, so I would try to "misuse" the stable ids as a test. – AlexS May 19 '14 at 20:37
  • Unfortunately if you see my updated answer it didn't work. I am not sure stableIds is actually even doing anything at all. I even tried to break it. No luck. – WIllJBD May 22 '14 at 02:47
  • @WIllJBD Have you tried to use a new adapter like I also suggested in my answer? – AlexS May 22 '14 at 06:45
  • I haven't, but in all the documentation and google videos I've seen they say not to do that xD, although that is how I used to do it a while ago. If I was being lazy I would do it with a new adapter, but I am trying to do it the 'right' way. ;) – WIllJBD May 22 '14 at 15:33
2

I analyzed the code and the problem you are into and I came to the following conclusion:

The refreshHolder method is only invoked when the convertView object has a value in the heap.

That implies that when convertView is not assigned to any value, no update will happen.

A solution for this is to move itemHolder.refreshHolder(position) out of the if-else condition block that you have inserted into.

GingerHead
  • 8,130
  • 15
  • 59
  • 93
1

the adapter.notifyDataSetChanged() should do the JOB, what you need to make sure is if the List itemsList you are manipulating outside the Adapter is the exact same instance that the Adapter is holding internally. If the notifyDataSetChanged() isn't working for you, that is definitely the case.

Your adapter might also be holding a 'copy' of the list you provided in the constructor, so your changes on the original list won't be reflected...maybe you can introduce a method to the adapter like: adapter.setItems(List items) to ensure the items are really set

You don't need to call invalidateViews() on a ListView...all you need to do is make sure the Adapter has the correct list to display and trigger notifyDataSetChanged().

Alécio Carvalho
  • 13,481
  • 5
  • 68
  • 74
  • 1
    Thankyou for the reply, The List is managed inside the adapter. It is created one time in the constructor, and any time the List is modified, it is through `.add()` or `.addAll()` or `.clear()` or `.remove()` the notifydatasetchanged does work as I said above, but it does not update the views currently visible in the list view, until you scroll away and then back. This is for updated existing items, for new or additional items that did not exist before, they are created and show/behave just fine. – WIllJBD May 15 '14 at 16:13
1

instead of :

@Override
    public int getViewTypeCount()
    {
        return 2;
    }

Try :

@Override
        public int getViewTypeCount()
        {
            return getCount();
        }

and instead of putting

ViewHolder viewHolder;

in a getivew() method, try to put it in a starting of class before class constructor

Jigar
  • 791
  • 11
  • 21
  • Hmmm, this particular function is referencing the number of the type of different views (or different layouts that are being inflated), in the list view. For example if you have items in a listview look one way when they include a picture, and a different way when they don't, then you should have 2 different layouts that represent this. The list view asks for this so it can know how to better manage the convertview that you get inside of the getView() function of the adapter. Unfortunately this isn't the answer to my issue :( – WIllJBD May 24 '14 at 06:41
  • the `getViewTypeCount` suggestion would f**k the view recycler completely! – rupps Dec 26 '14 at 01:05
0

Instead of using convertView.set/getTag() , why not directly update the views

refreshHolder(convertView, position);

void refreshHolder(View v, int position)
{
     ((TextView)v.findViewById(R.id.Text1)).setText(getItem(position).text1);
     ((TextView)v.findViewById(R.id.Text2)).setText(getItem(position).text2);
}

setTag/getTag will not be consistent on convertView as you are reusing the views, and same view will be reused when a view scrolled out of view and will return wrong ViewHolder. So most of the times the view is not updated

BlackBeard
  • 145
  • 6
  • Actually android takes this into account, and will always return the correct recycled view, based upon the view you inflated last for that item type, and the item type of the next view. – WIllJBD May 21 '14 at 16:36
  • ViewHolder is one of the standard patterns used in android to smooth scrolling of ListViews by reducing the findById-calls to a minimum. See [Android ViewHolder](http://developer.android.com/training/improving-layouts/smooth-scrolling.html#ViewHolder) – AlexS May 22 '14 at 06:41
0

After much trial and error, this is what worked.

public class AutoRefreshListView extends ListView
{
    public AutoRefreshListView(Context context)
    {
        super(context);
    }

    public AutoRefreshListView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    public AutoRefreshListView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

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

    class AdapterDataSetObserver extends DataSetObserver
    {
        @Override
        public void onChanged()
        {
            super.onChanged();
            Log.d("AutoRefreshListView", "onChanged");
            refreshVisibleViews();
        }

        @Override
        public void onInvalidated()
        {
            super.onInvalidated();
            Log.d("AutoRefreshListView", "onInvalidated");
            refreshVisibleViews();
        }
    }

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

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

    public void refreshVisibleViews()
    {
        Log.d("AutoRefreshListView", "refresh");
        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.d("AutoRefreshListView", "onInvalidated -> Refreshing view (data=" + dataPosition + ",child=" + childPosition + ")");
                    mAdapter.getView(dataPosition, getChildAt(childPosition), AutoRefreshListView.this);
                }
            }
        }
    }
}

the solution is from here

ListView not refreshing already-visible items

found by Kupsef

Community
  • 1
  • 1
WIllJBD
  • 6,144
  • 3
  • 34
  • 44
0

or you can do it in this way:listadapter.clear(); listadapter.addAll(yourData);

JAPS
  • 250
  • 4
  • 15