Coding Lines

RSS

Android: Implementing a Dynamically Loading Adapter

This tutorial describes how to implement custom adapter with long or endless list with dynamic loading based on DynamicViewAdapter

GitHub repository:  https://github.com/yfranz/DynamicList.

Introduction

DynamicViewAdapter implements a basic routine of populating list with data, displaying progress, empty list or error state. It requests data from model page by page when last item (which indicates progress) about to become visible to the user.

example

DynamicViewAdapter extends BaseAdapter and can be used with ListView, GridView, Gallery and other “adapter consuming” controls. It is an abstract class and should be extended with the specific implementation. There are two reasons for that: 1) Data fetching process is model specific and should be implemented in inherited class; 2) XML Layout of list items should be also defined for the specific case.

Preparation

I’ve created test project with ListActivity and TestDynamicModel that simulates network request and fetch data by 20 items per request and up to 100 items. Also, I’ve created TestDynamicViewAdapter that extends DynamicViewAdapter:

        
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);

          setListAdapter(new TestDynamicViewAdapter(this, new TestDynamicModel()));
    }
        
    

There are couple things im model class which require an explanation: (Full version of code is here)

        
    public class TestDynamicModel
    {
    …
        private int _count = 0;

        private Handler _loadCompleted;

        public void loadMore()
        {
            _count += PageSize;

            (new BackgroundTask()).execute();
        }
    …

        public String getItem(int position)
        {
            return String.format("Item %d", position);
        }

        private class BackgroundTask extends AsyncTask
        {
            @Override
            protected Integer doInBackground(String... params)
            {
    …
                Thread.sleep(5000); //sleep for 5 seconds 
                // in order to simulate network latency
    …
            }

            @Override
            protected void onPostExecute(Integer result)
            {
    …
                _loadCompleted.sendEmptyMessage(0);
    …
            }
        }
    }
        
    

Adapter should call loadMore() every time it needs new page to load. Also, adapter has to provide loadCompleted handler to notify that data is ready. Model should be designed to perform all data operations in the separate thread.

TestDynamicViewAdapter

public class TestDynamicViewAdapter extends DynamicViewAdapter

In constructor: you have to define the layouts for three scenarios: progress, error/data unavailable and empty list. Also, you can re-use the layouts defined in the project (See res folder details).


    public TestDynamicViewAdapter(Context context, TestDynamicModel model)
      {
        super(context, R.layout.progress_item, R.layout.unavailable_item,
                    R.layout.empty_item);

        _model = model;

If you are not going to use empty and error/unvailable items then you can put zeros in constructor parameters. Also, you have to implement handler for data completion event from the model. It is not necessary to do it in the same way it shown above but it should be defined somewhere.


        _model.setLoadCompleted(new Handler()
        {
            @Override
            public void dispatchMessage(Message msg)
            {
                    setDataCompleted();
            }
        });
  	}

There are four methods need to be implemented:

isMoreToLoad()

This method should return true if there is more data to load. For instance it loads by 10 items, for list with 100 items it should return true until it riches (100th) last item in the list:


    @Override
  	protected boolean isMoreToLoad()
  	{
        return _model.getCount() < 100;
  	}

onDataLoad()

onDataLoad is called when data item needs to be loaded from the model to the adapter; Use addItem() method to load every item from the model.


    @Override
  	protected void onDataLoad()
  	{
        for (int i = 0; i < _model.getCount(); i++)
        {
            addItem(_model.getItem(i), null);
        }
  	}

submitDataRequest()

This method is called then progress bar item is visible to user so it is time load more.


    @Override
  	protected void submitDataRequest()
  	{
        _model.loadMore();
  	}

onDataView()

The following method is called when data item needs to be rendered. It is called from getView() (see more info getView()). The base class takes care of rendering progress and other control items and calls this function when actual data needs to be rendered.


    @Override
  	public View onDataView(int position, AdapterItem item, View convertView,
              	ViewGroup parent)
  	{
        //Item inflating and rendering code.
	    TextView textView;
        textView = (TextView)view.findViewById(R.id.itemText);
        textView.setText(item.value.toString());

    }

Optimization

Every View which get inflated from the XML resources will result in a memory object. Inflation operation will cost some resources as well. Android controls can recycle views which are no longer in visible area. Recycled view is passed as convertView parameter. Since, adapter can support different xml layouts as items of the same list we cannot reuse every given view. Use tryInflateView() to verify if view has expected layout. If it doesn’t then it will be re-created and xml layout will be re-inflated with id passed as second parameter of this function.


        //Item inflating and rendering code.
       convertView = tryInflateView(convertView, R.layout.test_item,
                    	parent);

As a second step your implementation can use the so-called “ViewHolder” pattern. The findViewById() method is an expensive operation, therefore we should avoid doing this operation if not necessary. See more information about this pattern here in “Performance Optimization” section. DynamicViewAdapter supports “ViewHolder” pattern already. In order to use it we need create a class inherited from ViewHolder and override render() method. Here is how it should look like:


    public class ItemViewHolder extends ViewHolder
    {
        public final static int ResourceId = R.layout.test_item;
        private TextView textView;

        public ItemViewHolder(View convertView)
        {
            super(convertView, ResourceId);

            textView = (TextView)view.findViewById(R.id.itemText);
        }

        public void render(AdapterItem item)
        {
            textView.setText(item.value.toString());
        }
    }

Then change onDataView to start using it:


    @Override
  	public View onDataView(int position, AdapterItem item, View convertView,
              	ViewGroup parent)
  	{
        //Item inflating and rendering code.
        convertView = tryInflateView(convertView, ItemViewHolder.ResourceId,
                    parent);
        ViewHolder viewHolder = (ViewHolder) convertView.getTag();
        if (viewHolder == null)
        {
            viewHolder = new ItemViewHolder(convertView);
        }
        viewHolder.render(item);
        return convertView;
  	}

What else?

In this article we didn’t explore empty and error states. I didn’t come up with a good example without complicating the test project. So it is up to you how to implement it. Use showUnavailable() and showEmpty() to display these to state. Also you can use showProgress() to append progress item to the list. Use the following functions to hide specific state tryEndProgress(),tryEndUnavailable() and tryEndEmpty().