Technology

Delete a Row From a ListView Asynchronously


As I’ve said before sometimes the simplest things in Android are the most complicated. I followed “basic principles” to implement a swipe to delete action on a ListView. For some reason I kept getting an annoying flicker—the deleted row kept flashing on the screen.

This is what I learned in the epic quest to delete a ListView row asynchronously.

Implement a Touch Listener

Make sure the touch listener works before writing the code to delete the row. Otherwise the listview won’t scroll and you’ll think that the row deletion code isn’t working and you’ll end up spinning your wheels. You don’t want that.

The trick is to make sure the touch listener returns true or false at the right moment.

You can use Chet’s code, the mTouchListener in the ListViewAnimations class, for a fully working touch listener.

One of the interesting things Chet does is use getScaledTouchSlop. Try adding a touch listener and dump the x/y positions and velocities to the log. Depending on your phone, you’ll notice “false” events—on my LG Motion, for example, you swipe down and you get back “up” events.

That’s where getScaledTouchSlop comes in.

If a dx or dy is less than the “touch slop”, you can safely ignore it.

Use a ViewTreeObserver

One of the reasons for the flicker was that the list view was redrawing itself with the old row as I was trying to delete it. Use the OnPreDrawListener of the ViewTreeObserver to prevent that.

While inside the pre-draw listener, the list view will still redraw itself but using only the old data. You can then delete the row while inside the listener and even add additional animations (like animating the rows under the deleted row to move up).

final ViewTreeObserver observer = listView.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
    public boolean onPreDraw()
    {
        observer.removeOnPreDrawListener(this);
        //animate the rows under the deleted row to move up
        //delete the row from the list view
        //delete the row from the database
    }
}

Asynchronous Database Calls Are Tricky

Let’s talk about deleting the row. It’s actually a two step process: (1) delete the row from the database. (2) delete the row from the list view. Normally, you don’t worry about this process because the framework handles it for you transparently.

However, it’s an issue for us.

To see why, look back at the code for the pre-draw listener. The code to asynchronously delete the row doesn’t run in the pre-draw listener anymore. (That’s why its asynchronous. Doh!) That means if you don’t delete the row from the list view first, the list view will redraw itself with the old data before the row gets deleted for real in the database. (Hence, the other reason for the flicker.)

There are two approaches to solve this dilemma:

  1. Mark the row to be deleted as “stained” and don’t show it until the database operation completes.
  2. Somehow remove the row from the list view before removing it from the database.

The problem with Approach 1 is that the the code to track the “stained” row is complex. You don’t really know how long the database operation will take. The problem with Approach 2 is that a list view backed by a Cursor has no remove method.

The “aha!” moment comes by realizing that you don’t really need to remove the row from the list view. All you have to do is wrap the cursor with another cursor (or proxy) that ignores the deleted row. When you swap this “wrapper” cursor into the list view, the list view will think the row was deleted. You can then delete the row for real in the database.

Here’s some sample code for the wrapper cursor (or checkout the gist for a full implementation):

public class CursorDeleteProxy extends AbstractCursor {

private Cursor cursor;
private int posToIgnore;

public CursorDeleteProxy(Cursor cursor, int posToRemove)
{
    this.cursor = cursor;
    this.posToIgnore = posToRemove;
}

@Override
public boolean onMove(int oldPosition, int newPosition)
{
    if (newPosition < posToIgnore)
    {
        cursor.moveToPosition(newPosition);
    }
    else
    {
        cursor.moveToPosition(newPosition+1);
    }
    return true;
}

//make sure to override all methods in AbstractCursor appropriately
//Ex:

@Override
public int getCount()
{
    return cursor.getCount() - 1;
}

@Override
public String[] getColumnNames()
{
    return cursor.getColumnNames();
}
//..
}

The final code looks like this:

final ViewTreeObserver observer = listView.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
    public boolean onPreDraw()
    {
        observer.removeOnPreDrawListener(this);
        //animate the rows under the deleted row to move up
        //"delete" the row from the listview
        CursorDeleteProxy newCursor = new CursorDeleteProxy(cursor, rowPositionToDelete);
        //tell the list view to use the new cursor
        adapter.swapCursor(newCursor);
        //delete the row from the database
    }
}

You can check out everything in action over at the newAlarmSystem branch for Reminderer.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s