Thursday, 26 May 2016

Lazily Loaded ListView in Android

We'll  create a lazily loaded ListView. That is , as the user scrolls and nears the end of the list, we'll append more data to the ListView.

This is how its going to look like :




To do this, we have 4 components :
  • MainActivity - The Activity of course !
  • FetchItemsTask - An AsyncTask to load fresh data in the background.
  • ResponseListener - Interface for the AsyncTask to return the fresh data back to the Activity.
  • LazyLoader - The Main Component !! Basically an OnScrollListener . When                                                    the List is scrolled, this listener does some basic calculations to check if the                                user is nearing the end of the List. If yes, then a loadMore() method is called.

So here's the code. :

MainActivity.java

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewGroup.LayoutParams;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    private ArrayAdapter<String> adapter ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // create the ListView
        ListView listView = new ListView(this);

        // add a ProgressBar as ListView's footer to indicate data load
        listView.addFooterView(new ProgressBar(this));

        // create an adapter
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);

        // plug the adapter to the ListView
        listView.setAdapter(adapter);

        // set the ListView as the activity's content
        setContentView(listView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));

        // CRUCIAL PART !! Add the LazyLoader as the onScrollListener for the ListView.
        listView.setOnScrollListener(new LazyLoader() {

            // This method is called when the user is nearing the end of the ListView
            // and the ListView is ready to add more items.
            @Override
            public void loadMore(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                
                // The ListView needs more data. So Fetch !!   
                loadItems();
            }
        });
    }

    // Called by the LazyLoader when the ListView is ready for fresh data.
    private void loadItems() {

        // Index is required to fetch the next set of items
        int startIndex = adapter.getCount();

        // Fetch more items Asynchronously.
        new FetchItemsTask(startIndex, responseListener).execute();
    }

    // The FetchItemsTask delivers the new data to this listener in the main thread.
    private ResponseListener responseListener = new ResponseListener() {
        @Override
        public void onResponse(List<String> newItems) {

            // append the fresh data to the ListView.
            adapter.addAll(newItems);
        }
    };
}

In the onCreate() method, we create a ListView, set an adapter to it AND set an OnScrollListener. The scrollListener's loadMore() method is called when the ListView is nearing its end and needs more data.
In loadMore(), an AsyncTask fetches more data and appends it to the ListView.


Here's the Magical OnScrollListener :


LazyLoader.java

import android.widget.AbsListView;

public abstract class LazyLoader implements AbsListView.OnScrollListener {
    
    private boolean loading = true  ;
    private int previousTotal = 0 ;
    private int threshold = 10;

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
                         int visibleItemCount, int totalItemCount) {
        if(loading) {
            if(totalItemCount > previousTotal) {
                // the loading has finished
                loading = false ;
                previousTotal = totalItemCount ;
            }
        }

        // check if the List needs more data
        if(!loading && ((firstVisibleItem + visibleItemCount ) >= (totalItemCount - threshold))) {
            loading = true ;
            
            // List needs more data. Go fetch !!
            loadMore(view, firstVisibleItem,
                    visibleItemCount, totalItemCount);
        }
    }

    // Called when the user is nearing the end of the ListView
    // and the ListView is ready to add more items.
    public abstract void loadMore(AbsListView view, int firstVisibleItem,
                                  int visibleItemCount, int totalItemCount);
}


The onScroll() method of the onScrollListener is called when the user scrolls the ListView.
In this method, we do calculations to check if the List is nearing its end. If yes, the loadMore() method is called. We have implemented the loadMore() method in the MainActivity, which will append the ListView with more data.

Here's the FetchItemsTask, if you're interested :

FetchItemsTask.java


import android.os.AsyncTask;

import com.androidcocktail.lazyloader.utils.ResponseListener;

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

public class FetchItemsTask extends AsyncTask<Void, Void, List<String>> {

    private int startIndex;
    private ResponseListener responseListener;

    public FetchItemsTask(int startIndex, ResponseListener listener) {
        this.startIndex = startIndex;
        this.responseListener = listener;
    }

    @Override
    protected List<String> doInBackground(Void... params) {

        try {
            //In this example, we are fetching dummy data locally;
            // but in a real world app its usually a network or database request which takes time.
            // So FAKE that time delay here.
            Thread.sleep(3000);

        } catch (InterruptedException e) {

        }

        // Fetch a maximum of 50 new items.
        int end = startIndex + 50 ;

        // Get the data.
        List<String> dummyItems = new ArrayList<>();
        for(int i = startIndex ; i < end ; i++) {
            String item = "Item " + i ;
            dummyItems.add(item);
        }
        return dummyItems;
    }

    @Override
    protected void onPostExecute(List<String> newItems) {
        // give the new items back to the activity
        responseListener.onResponse(newItems);
    }
}


The ResponseListener :


import java.util.List;

public interface ResponseListener {
    public void onResponse(List<String> responseItems);
}

I've broken some coding conventions for the sake of simplicity. You can find the proper, full source code at Github :) .