Android ListView Adapter to hide items

This is an abstract class I wrote that derives from BaseAdapter that can be extended to easily allowing hiding and restoring of items in a ListView. You might want to use it with some cool animations for a really nice effect, like swiping items out and so on. I wanted to create an Adapter to presents a different view of the same data structure to the BaseAdapter. That is, if you have an array of [a, b, c, d] and hide the second item, as far as the BaseAdapter is concerned, it is dealing with an array of [a, c, d].

I wanted to achieve this without modifying the underlying data structure. Immutibility of data is generally considered a good thing, especially in multi threaded environments, and some languages even make it obligitory. Take the example where the List behind your Adapter is used in other places. If whilst filtering that data you remove the item from the List itself, and another piece of code accesses it expecting it to reflect exactly the server side, you might have problems that can be very tricky to solve. Now, I’m not going as far as to make the underlying structure immutable, but this code means you can hide and restore items without altering the data itself, which IMO is a better design as the visualisation of the model should not affect the model itself.

Use cases

Filtering

If you have a list that you wish to filter, perhaps with a TextView in the ActionBar, you can use this to hide and restore the items as they match the filter.

Temporary Deletion

This is actually the use case I had. I wanted to swipe items out of my list to delete. As I swiped them out, I sent a DELETE to the server. However, I didn’t want to update my model until the DELETE returned, so I needed this temporary hide. If the DELETE is succseful, I then delete the item from the model, and if not, I just call restore on the item in the list and maybe display a toast or something.

Code and Usage

This is the code for the class to be extended from. Your adapter will look exactly like a normal adapter except getCount() is now getTotalCount() and getView() is getItemView(). The abstract class handles the calling of these for the correct positions.

package com.steoware.adapters;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import java.util.SortedSet;
import java.util.TreeSet;

public abstract class HiddenItemAdapter extends BaseAdapter {

    private SortedSet<Integer> mHiddenItems = new TreeSet<>();

    public final void hideItem(int itemToHide) {
        mHiddenItems.add(itemToHide);
        notifyDataSetChanged();
    }

    public final void restoreItem(int itemToRestore) {
        mHiddenItems.remove(itemToRestore);
        notifyDataSetChanged();
    }

    @Override
    public final int getCount() {
        return getTotalCount() - mHiddenItems.size();
    }

    @Override
    public final View getView(final int position, View convertView, final ViewGroup parent) {
        return getItemView(getActualIndex(position), convertView, parent);
    }

    public final void clearHiddenItems() {
        mHiddenItems.clear();
    }

    /**
     * The external count provided by getCount is of an imaginary array of the hidden items.
     * We need to map indices of this array to indices in the real array of items. For example
     * if we have an array
     * [ "a", "b", "c", "d"]
     * and the second item, "b" is hidden, we return the count of this array as 3, to an imaginary
     * array of
     * [ "a", "c", "d"]
     * When the items at position 1 and 2 are requested however we need to map them to our real array
     * ie. 1 --> 2 and 2 --> 3 to give us "c" and "d"
     */
    private int getActualIndex(int indexToHiddenItems) {

        int position = indexToHiddenItems;

        for(Integer i : mHiddenItems) {
            if(i <= position) {
                position++;
            }
            else {
                //mHiddenItems is Ordered so anything higher won't affect us
                break;
            }
        }

        return position;
    }

    public abstract int getTotalCount();
    public abstract View getItemView(final int position, View convertView, final ViewGroup parent);
}


 

Performance and Implementation Considerations

THere are two ways to implement this. The one I have chosen involves keeping a list of the items that are hidden and then iterating over this in the getView call to see what item to return. Remember, that this itself is happening in a loop from the system, so in the very worst case we are looking at performance of O(N2). This certainly isn’t great, but there are 3 caveats which make it not *quite* so bad:

  • Android will only be showing a subset of your list at any one time so the outer loop isn’t as big as you might fear.
  • This is very worst case scenario – ie. all elements hidden.
  • In this implementation, the hidden elements are sorted which means we can break out of the iteration early once we have found our index. This means earlier elements in the list will be quicker than later ones.

In any event, the lists I am using are not large, so this is not a problem. If it does become a problem, the alternative is to make a mantain a copy of the original list and in there you can delete and restore items safely enough. This will use up more space though, so as ever, it’s a trade off.

Testing

This is an abstract class, and there is a school of thought that says you shouldn’t waste your time unit testing code that can’t exist on it’s own as you aren’t really testing a piece of software that will run. While I agree with that, I did it anyway :). I wrote this adapter using TDD. That is, I wrote the tests first. So if nothing else, the tests served as a crutch to help me write it, which is nice. Another argument against the testing of abstract classes is that the concrete class that extends from it can override methods and change the behaviour. I mitigated that in this case by making all the methods final so that should be ok. Note, the test code uses EasyMock.

package com.steoware.adapters.test;

import android.view.View;
import android.view.ViewGroup;

import com.steoware.adapters.HiddenItemAdapter;

import junit.framework.TestCase;

import org.easymock.EasyMock;

import java.util.ArrayList;
import java.util.List;

import static org.easymock.EasyMock.createMockBuilder;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;

/**
 * This adapter acts as a wrapper around the items in a standard adapter, manipulating the getCount
 * and getView to only be invoked for items that are not hidden
 */
public class HiddenAdapterTest extends TestCase {

    private static final int ITEMS_IN_ADAPTER = 10;
    private HiddenItemAdapter mAdapterUnderTest;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        mAdapterUnderTest = createMockBuilder(HiddenItemAdapter.class).
                addMockedMethod("getTotalCount").
                addMockedMethod("getItemView").
                createMock();

        expect(mAdapterUnderTest.getTotalCount()).andReturn(ITEMS_IN_ADAPTER).anyTimes();
    }

    /**
     * Tests the adapters getCount() method is correctly manipulated to return the count of only non-hidden
     * items
     */
    public void testHiddenItemCount() {

        replay(mAdapterUnderTest);

        assertEquals("Before hiding any items, getCount should return the same number of items as in the list",
                ITEMS_IN_ADAPTER, mAdapterUnderTest.getCount());

        mAdapterUnderTest.hideItem(3);
        assertEquals("With 1 item hidden, getCount should return 1 less item that total in list",
                ITEMS_IN_ADAPTER -1, mAdapterUnderTest.getCount());

        mAdapterUnderTest.hideItem(5);
        assertEquals("With 2 items hidden, getCount should return 2 less item that total in list",
                ITEMS_IN_ADAPTER - 2, mAdapterUnderTest.getCount());

        mAdapterUnderTest.hideItem(5);
        assertEquals("Hiding the same item twice shouldn't change the hidden number in the list",
                ITEMS_IN_ADAPTER - 2, mAdapterUnderTest.getCount());

        verify(mAdapterUnderTest);
    }

    /**
     * Tests that the getItemView of the client (ie. any class that extends from HiddenItemAdapter)
     * is only called for those items who are not hidden. These tests call getCount and getView in
     * the same way the System would
     */
    public void testGetViewWithNothingHidden() {
        testGetViewWithHiddenItems(new ArrayList<Integer>());
    }

    public void testGetViewWithHiddenItemsInOrder() {

        List<Integer> hiddenItems = new ArrayList<>();
        hiddenItems.add(3);
        hiddenItems.add(5);
        hiddenItems.add(6);

        testGetViewWithHiddenItems(hiddenItems);
    }

    public void testGetViewWithItemsNotInOrder() {

        List<Integer> hiddenItems = new ArrayList<>();
        hiddenItems.add(6);
        hiddenItems.add(2);
        hiddenItems.add(4);

        testGetViewWithHiddenItems(hiddenItems);
    }

    public void testGetViewWithItemsRepeatedlyHidden() {

        List<Integer> hiddenItems = new ArrayList<>();
        hiddenItems.add(6);
        hiddenItems.add(2);
        hiddenItems.add(2);

        testGetViewWithHiddenItems(hiddenItems);
    }

    public void testRestoringItem() {

        int hiddenItem = 3;
        int expectedIndexWhenHidden = 4;
        int expectedIndexWhenRestored = 3;

        //First expect a call to the 4th item, when the 3rd is hidden. When the 3rd is restored
        //a call to the 3rd is expected
        expect(mAdapterUnderTest.getItemView(eq(expectedIndexWhenHidden),
                EasyMock.<View>isNull(), EasyMock.<ViewGroup>isNull())).andReturn(null).once();

        expect(mAdapterUnderTest.getItemView(eq(expectedIndexWhenRestored),
                EasyMock.<View>isNull(), EasyMock.<ViewGroup>isNull())).andReturn(null).once();

        replay(mAdapterUnderTest);

        mAdapterUnderTest.hideItem(hiddenItem);
        assertEquals(ITEMS_IN_ADAPTER - 1, mAdapterUnderTest.getCount());
        mAdapterUnderTest.getView(hiddenItem, null, null);

        mAdapterUnderTest.restoreItem(hiddenItem);
        assertEquals(ITEMS_IN_ADAPTER, mAdapterUnderTest.getCount());
        mAdapterUnderTest.getView(hiddenItem, null, null);

        verify(mAdapterUnderTest);
    }

    private void testGetViewWithHiddenItems(List<Integer> hiddenItems) {

        for(int i = 0; i < ITEMS_IN_ADAPTER; i++) {
            //Should call getItemView in child class for each item that is not hidden
            if(!hiddenItems.contains(i)) {
                expect(mAdapterUnderTest.getItemView(eq(i),
                        EasyMock.<View>isNull(), EasyMock.<ViewGroup>isNull())).andReturn(null).once();
            }
        }

        replay(mAdapterUnderTest);

        for(int i = 0; i < hiddenItems.size(); i++) {
            mAdapterUnderTest.hideItem(hiddenItems.get(i));
        }

        int count = mAdapterUnderTest.getCount();
        for(int i = 0; i < count; i++) {
            mAdapterUnderTest.getView(i, null, null);
        }

        verify(mAdapterUnderTest);
    }
}

Advertisements

One thought on “Android ListView Adapter to hide items

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