Refreshing Android ListView data inside OnClickListener

Mike picture Mike · May 31, 2013 · Viewed 8.3k times · Source

I've been working on a personal project in Android and ran into a strange situation regarding an Activity which uses a ListView. The basics of the problem are that I have a list of items, each which have 2 buttons, edit and delete. Right now I'm working on implementing the Delete button, which works functionally, but doesn't update the ListView correctly. Instead, it puts what was just deleted on top of the list. It of course refreshes whenever I renavigate to that activity.

Right now the Delete button is detected inside the a custom BaseAdapter, and when I call the notifyDataSetChanged, the situation described above happens instead of removing the now deleted item. How can I correctly update the list inside the adapter class?

I realize that there have been some questions about this, but I haven't been able to integrate them, and solutions I think may work don't really explain how they work; I'm using this project to understand Android app development more, so I would prefer answers with some level of explanation, although any help is of course appreciated! Thanks!

Here's the relevent code. Note that this is an unfinished project, so there are some unused/incomplete things inside it. Please ignore these.

EditItemsActivity:

package com.example.mybudget;

import java.util.List;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;

public class EditItemsActivity extends Activity implements OnGestureListener{

    private DatabaseHandler db;
    private List<DataPoint> dpList;
    private EditItemsAdapter adapter;
    private ListView lv;
    private GestureDetector gestureDetector;

    @SuppressLint("NewApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit_items);
        // Show the Up button in the action bar.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 
            getActionBar().setDisplayHomeAsUpEnabled(true);
        db = new DatabaseHandler(this);
        dpList = db.allDataThisMonth();
        lv = (ListView) findViewById(R.id.edititems);
        adapter = new EditItemsAdapter(this, R.id.edititems, dpList);
        lv.setAdapter(adapter);
        gestureDetector = new GestureDetector(getBaseContext(), this);

//      buttonDelete.setVisibility(View.GONE);
    }

    public void refreshList()
    {
        adapter.notifyDataSetChanged();
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity_edit_items, menu);
        return true;
    }


    @Override
    public boolean onDown(MotionEvent e) {
        // TODO Auto-generated method stub
        return false;
    }

    public void onDelete()
    {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
            float velocityY) 
    {
//      Log.d("Swipe", "" + velocityX + ", " + velocityY);
//      if(velocityX > 200 && velocityY < 50 && velocityY > -50)
//      {
//          buttonEdit.setVisibility(View.GONE);
//          buttonDelete.setVisibility(View.VISIBLE);
//      }
//      else if(velocityX < -200 && velocityY < 50 && velocityY > -50)
//      {
//          buttonDelete.setVisibility(View.GONE);
//          buttonEdit.setVisibility(View.VISIBLE);
//      }
        return false;
    }


    @Override
    public void onLongPress(MotionEvent e) {
        // TODO Auto-generated method stub

    }


    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
            float distanceY) {
        // TODO Auto-generated method stub
        return false;
    }


    @Override
    public void onShowPress(MotionEvent e) {
        // TODO Auto-generated method stub

    }


    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        // TODO Auto-generated method stub
        return false;
    }

}

And here's the adapter class:

package com.example.mybudget;

import java.text.NumberFormat;
import java.util.List;

import android.app.Activity;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;

public class EditItemsAdapter extends BaseAdapter implements OnClickListener{
    private List<DataPoint> dpList;
    private Activity activity;
    private DatabaseHandler db;

    public EditItemsAdapter(Activity a)
    {
        activity = a;
    }

    public EditItemsAdapter(Activity a, int textViewResourceId, List<DataPoint> dpList)
    {
        super();
        this.dpList = dpList;
        activity = a;
        db = new DatabaseHandler(activity);
    }

    public static class ViewHolder
    {
        public TextView item1;
        public TextView item2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        View v = convertView;
        //ViewHolder holder;
        NumberFormat format = NumberFormat.getCurrencyInstance();

        if (v == null)
        {
//          LayoutInflater vi = 
//                  (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            LayoutInflater vi = activity.getLayoutInflater();
            v = vi.inflate(R.layout.edit_grid_items, null);
//          holder = new ViewHolder();
//          holder.item1 = (TextView) v.findViewById(R.id.edit_item_name);
//          holder.item2 = (TextView) v.findViewById(R.id.edit_item_cost);
//          v.setTag(holder);
            TextView tv1 = (TextView)v.findViewById(R.id.edit_item_name);
            TextView tv2 = (TextView)v.findViewById(R.id.edit_item_cost);
            Button edit = (Button)v.findViewById(R.id.edit_item_button);
            Button delete = (Button)v.findViewById(R.id.delete_item_button);
            final DataPoint dp = dpList.get(position);
            tv1.setText(dp.getName());
            tv2.setText(Float.toString(dp.getCost()));
            delete.setOnClickListener(new OnClickListener()
            {
                @Override
                public void onClick(View v)
                {
                    db.deleteRowByKey(dp);
                    ((EditItemsActivity) activity).refreshList();
                }
            });
        }
//      else
//          holder = (ViewHolder)v.getTag();
//      if(dp != null)
//      {
//          holder.item1.setText(dp.getName());
//          holder.item2.setText(format.format(dp.getCost()));
//      }
        return v;
    }

    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return dpList.size();
    }

    @Override
    public DataPoint getItem(int position) {
        // TODO Auto-generated method stub
        return dpList.get(position);
    }

    @Override
    public long getItemId(int position) {
        // TODO Auto-generated method stub
        return dpList.size();
    }

    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub

    }
}

EDIT: The solution involved the inflation explanation that Adam provided, but required a complete repopulation of dpList, the

dpList = db.allDataThisMonth();

that Anup suggested.

Answer

Adam S picture Adam S · May 31, 2013

In your getView method of your adapter, you're checking if (convertView == null) and if it's null you're inflating a new view. If it's not null, you're just returning the non-null view that was supplied.

The convertView supplied to the getView method is a cached view that has already been displayed. You're supposed to re-use this (if it's a valid view - you may have multiple different views in your list), rather than inflating a new one. You're forgetting to update the content of it for the corresponding position.

So, how to fix it? Simply close your if (v == null) after inflation:

if (v == null)
{
    LayoutInflater vi = activity.getLayoutInflater();
    v = vi.inflate(R.layout.edit_grid_items, null);
}

Edit: As Anup points out, you also need to update your dpList variable or it will keep returning the same values for the given position. You can do this in your click listener:

delete.setOnClickListener(new OnClickListener()
{
    @Override
    public void onClick(View v)
    {
        db.deleteRowByKey(dp);
        dpList.remove((Integer)v.getTag());
        ((EditItemsActivity) activity).refreshList();

    }
});
// Required so we know which index to remove from our dpList.
delete.setTag(position);