Monday, February 25, 2013

on 1 comment

Android: Simple Endless Lists with a Custom Footer



There are a lot of posts on stack overflow with people asking how to make a custom infinite list. There is even a library devoted entirely to completing this task. Normally I am first in line to grab the latest and greatest libraries and give it a go. This time however it occurred to me that creating a simple endless list for my purposes would not be that difficult and may not need the complexity of the library. The below example is somewhat crude but it is functional. I did not go through the trouble of handling caching, saving state, or cancelling async requests as each person my have their own preferences or needs for that approach.

GitHub Source

For my purposes I had a few requirements:
  •  I wanted a footer that would go from a default loading state to listing out the number of items in the list once it had reached the end. 
  • The load needed to start prior to the end of the list so it would feel seamless. 

The demo is fairly simple. We have one activity which loads a ListFragment. The ListFragment subclasses a BaseListFragment. The BaseListFragment is responsible for setting the state of the footer. 
 
public class BaseListFragment extends ListFragment {
 
 private ListViewFooter mFooter;
 
 @Override
 public void onActivityCreated(Bundle savedInstanceState) {
  // TODO Auto-generated method stub
  super.onActivityCreated(savedInstanceState);
  mFooter = new ListViewFooter(getActivity());
  getListView().addFooterView(mFooter);
 }
 
    /**
     * Hide the loading view and set the display message
     * @param message
     *    Message to display in the footer
     */
    public void loadComplete(String message){
     mFooter.hideLoadingView();
     mFooter.setLoadCompleteMessage(message);
    }
    
    /**
     * Show the loading view 
     */
    public void  loadStarted() {
     mFooter.showLoadingView();
    }
}

The footer is simply an xml layout.. It has to internal layouts. The first displays a non-determinate  progress bar. The second can be set with a custom message. The custom message is not visible by default.


The main list fragment in the demo implements a listener interface that allows our fake model to make a callback when the "data load" is complete. The data that is returned is added to our endless list adapter.
 
public class DemoListFragment extends BaseListFragment implements ITestDataLoadListener {

 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  View v = inflater.inflate(R.layout.fragment_demolist, container,false);
  return v;
 }
 
 @Override
 public void onResume() {
  super.onResume();
  DemoListManager.getInstance().registerListener(this);
  
  //load the test data
  if(getListView().getCount() == 0)
   DemoListManager.getInstance().getTestData(0);
 }

 @Override
 public void onPause() {
  super.onPause();
  DemoListManager.getInstance().unregisterListener(this);
 }

 @Override
 public void testDataLoadSuccessful(List data) {
  if(getListAdapter() == null){
   EndlessListAdapter adapter = new EndlessListAdapter(this.getActivity(), R.layout.list_row, data);
   setListAdapter(adapter);
  } else {
   if(data.isEmpty())
    loadComplete(getString(R.string.finishedLoading) + " " + getListAdapter().getCount() + " Names");
   
   ((EndlessListAdapter)getListAdapter()).add(data);
  }
 }

 @Override
 public void testDataLoadFailure() {
  Log.v(this.getTag(), "Well something went wrong.. that is not good...");
  
 }
}

The adapter, as you might expect, is where the "magic happens". There is a function that takes into account the position of the listview view that is being retrieved in getView and decides (based on some attributes that you can change to your liking) whether or not to request more data. For this example I just check for three things:
  1. Are we half way down the list?
  2. Are we currently trying to load data?
  3. Is there more data to load?
If all of these return true then we request more data. Depending on how your services are structured you will need to pass a starting point (which in this case is the size of our current data set). 


 
@Override
 public View getView(int position, View convertView, ViewGroup parent) {
  
  View v = convertView;
  ViewHolder viewHolder;
  
  //check to see if we need to load more data
  if(shouldLoadMoreData(data, position) )
   loadMoreData();
  
  if(v == null){
   LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   v = inflater.inflate(R.layout.list_row, null);
   
   viewHolder = new ViewHolder();
   viewHolder.tvTestString = (TextView)v.findViewById(R.id.tvTestString);
   v.setTag(viewHolder);
   
  } else {
   viewHolder = (ViewHolder) v.getTag();
  }
  
  String currentString = data.get(position);
  if(currentString != null){
   viewHolder.tvTestString.setText(currentString);
  }
  
  return v;
 }

 private boolean shouldLoadMoreData(List list, int position){
  boolean scrollRangeReached = (position > list.size()/2);
  return (scrollRangeReached && !isLoading && moreDataToLoad);
 }
 
 /**
  * Call the fake backend and tell it we need some more data 
  */
 private void loadMoreData(){
  isLoading = true;
  DemoListManager.getInstance().getTestData(data.size());
 }


That is pretty much the bulk of the work. If our service call returns no data we set a flag telling us that our last call reached the end of the set. Some services may actually return you a total count that you can base you current dataset size against.

For those curious as to how the model works for this demo, it is simply an async task with a Thread.sleep(1000) in the middle of it. Nowhere near realistic , but it worked for the purposes of this example. The data is just an array of names I pulled off of a data generation site.


 
/**
  * Our fake network request. A simple Async task with a sleep.
  * @author Trey Robinson
  *
  */
 private class FakeNetworkRequest extends AsyncTask {

  private int startingAt;

  public FakeNetworkRequest(int startingAt){
   this.startingAt = startingAt;
  }

  @Override
  protected String doInBackground(String... params) {
   try {
    Thread.sleep(THREAD_SLEEP_TIME);
   } catch (InterruptedException e) {
    notifyListenersError();
    e.printStackTrace();
   }

   return "Executed";
  }

  @Override
  protected void onPostExecute(String result) {

   if(startingAt > testList.size()){
    //pretend like the service did not return any data because we reached the end
    notifyListenersSuccess(new ArrayList());
   } else{
    //still have data so lets send it back
    int endIndex = (startingAt + PAGE_SIZE > testList.size())?testList.size():startingAt+PAGE_SIZE;
    
    //create a new list otherwise we will get issues with concurrent modifications. Again.. not 
    // an issue you will have in a normal environment
    List list = new ArrayList();
    list.addAll(testList.subList(startingAt, endIndex));
    notifyListenersSuccess(list);
   }
      
  }

  @Override
  protected void onPreExecute() {
  }

  @Override
  protected void onProgressUpdate(Void... values) {
  }
 }

I hope this helped you take your first steps into endless lists! The basic concept is really pretty straight forward. In a production app I would probably try to make the list calls as frequent as possible so that the user never saw our nifty loading footer unless they were scrolling really fast or had a bad network connection.

If you would like to see this example expanded please let me know! Depending on your needs it could be this simple or very complicated! I have a similar (more polished) implementation to what I have provided hitting some remote services and the implementation runs silky smooth!

Github Source
Powered by Blogger.