Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

notifydataSetChanged on Adapter will update with new items, but will not update the existing items

Tags:

java

android

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();
like image 835
WIllJBD Avatar asked May 14 '14 22:05

WIllJBD


1 Answers

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

ListView not refreshing already-visible items

like image 104
kupsef Avatar answered Oct 06 '22 01:10

kupsef