Batch loading list

About This Document

This document is a developer guide which discusses the implementation of UI pattern on Android, where the Adapter's backing data is uploaded gradually in small batches while the user is scrolling down a ListView.

This article assumes that you have a basic knowledge of the Android SDK.

Attachments

This article features the following attachments:

1. Loading List Example

It is the complete source of the example application. It requires the Loading List Library to be downloaded separately (see below). Caution: project.properties file sets library location to ../LoadingList.

2. Loading List Library

Several classes and interfaces developed while writing this article can form a simple library. The library can be used independently from the sample application to provide an easy-to-follow pattern when working with batch loading lists.

For information on how to link a library project to your project, please refer to the Android Developer Documentation:
http://developer.android.com/tools/projects/projects-eclipse.html

3. Other Dependencies

As the application utilizes the YouTube Data API v. 2.0, to be able to run this application you should download the youtube-gdata-client Java Client Library and copy the .jars into the /libs directory of the project.
YouTube Data API v. 2.0 .jars can be downloaded from this URL:
http://code.google.com/p/gdata-java-client/downloads/list

As the YouTube Data API v.2.0 is not fully compatible with Android you should also download three additional jars into the /libs directory. These jars can be found here:
http://code.google.com/p/javamail-android/downloads/list

Introduction

ListView and Adapter are some of the most used Android UI classes. They were designed and optimized for the common case of displaying large lists of similar items, and they do their job perfectly. Sometimes however, the designed Adapter - ListView workflow is not enough for your requirements. Consider the situation when you want to download data gradually as the user scrolls the list down. Of course, you could first check the size of the data to be fetched and then, as the user scrolls the list, your adapter could load it for each item when it appears on the screen. Although this solution would suffice in some cases, take closer look at the following issues:

  • Endless list. Sometimes you have no possibility of knowing in advance the amount of data which will be downloaded.

  • Minimizing the quantity of remote requests. If “fetching data” means (or may mean) getting it from a remote server, producing a HTTP request for every item is far from being optimal. Loading remote data should be done in batches of reasonable size.

A possible solution could be a wrapper around the ListView and Adapter classes which starts loading when the user reaches the end of already loaded data, and finally updates the adapter after a new portion of data is downloaded.

Loading new data can take a longer time, and the user should be aware that some new items are due to appear otherwise he could leave the activity. Some visual feedback to the user about what is going on is necessary.

The way data is loaded differs significantly between projects. This is also true about the visual feedback about the state of the current load task. Adapters are project specific as well, but in this case a simple interface for updating the adapter's data (not of a full custom implementation of the Adapter interface) is enough. It should only have methods to add a new portion of data when a new load is done and to reset the adapter when the sequence of loads has been prepared.

For the solution to be generic, components addressing the above mentioned functionality are defined as interfaces. The only solid class in the solution library binds these three interfaces together and manages interactions between them. It also observes the ListView instance to know when the user reaches its end while scrolling.

The Loading List Library Primer

The entry point for the library is the LoadingListWrapper<D, I> class. Where the <D> formal type parameter stands for data to be loaded in one portion. In most cases it will be a List but you are not limited to it. The second formal type parameter stands for data initialization (e.g.: the data type to describe the sequence of loads - for example a base query string for all pages). LoadingListWrapper<D, I> keeps references to loadingDelegate<D, I>, LoadingStateListener and UpdateableAdapterWrapper<D> objects.

  • loadingDelegate<D, I> - an interface responsible for loading data divided into parts.

  • LoadingStateListener - is notified whenever LoadingListWrapper<D, I> changes state (loading, success, error, etc.). Implement this interface to show the user what is happening under the scene. This can be implemented with toasts, views or dialogs or a combination of them.

  • UpdateableAdapterWrapper<D> - this interface exposes methods for updating the adapter with new data. You can make your adapter implement this interface next to the Adapter interface or you can wrap the existing adapter with this interface to make it modify the data source behind the existing adapter.

Above these core components the library also contains a few classes which implement the above mentioned interfaces (mainly the loadingDelegate interface). You can find extending them to be more convenient instead of implementing interfaces from scratch, as long as they match your use case. For example BgThreadloadingDelegate takes care of starting a thread for new loads and interrupting threads when cancelled.

For other convenient classes see the com.samsung.android.loadinglist.delegate and com.samsung.android.loadinglist.adapter packages.

As an example is often better than a dry description, let's write some code as an introduction.
First, the declarations of three main actors playing in this short example:

private UpdateableAdapterWrapper<List<String>> adapterWrapper;
private loadingDelegate<List<String>, String> loadingDelegate;
private LoadingStateListener loadingStateListener ;
private LoadingListWrapper<List<String>, String> wrapper;
[Code 1]

These classes are generic types. In this example the <D> type parameter is of the List<String> type which means that portions of data to be loaded gradually are lists of single string records. The type parameter is of String type, which means the sequence of loads is initialized with a single string parameter (for example a query string).

o make things easy at this time the discussion about the actual implementation of loadingDelegate, LoadingStateListener and UpdateableAdapterWrapper is left for some lines later. First focus on how these get coupled together.

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

    wrapper = new LoadingListWrapper<List<String>, String>(adapterWrapper, loadingDelegate);
    
    wrapper.setLoadingStateListener(loadingStateListener );

    wrapper.bindListView((ListView) findViewById(R.id.listView));
}
[Code 2]

As the loadingDelegate implementation may start new threads, or take use of the activity's Context, the ListView should be unbound from LoadingListWrapper in onDestroy:

@Override
protected void onDestroy() {
    super.onDestroy();
    wrapper.unbindListView();
}
[Code 3]

A call to LoadingListWrapper#unbindListView() cancels any running loads performed by LoadingDelagate. It is important that LoadingDelagate#cancelLoad() is implemented properly, especially that it interrupts any running thread which could rely on the activity’s Context.

As you see, LoadingListWrapper’s usage is very easy. Now is a proper time to return to the implementation of two missing components.

Now take a look at LoadingStateListener interface. The simplest implementation to provide the user with information about the state of the performed loads is probably the one which uses Toasts. Although in real applications you will probably want to make visual feedback fancier, this time it is sufficient to show what role this interface plays.

loadingStateListener  = new LoadingStateListener() {

    private Toast mToast;
    
    @Override
    public void showLoading(int pageNo) {
        showToast("Load started.", Toast.LENGTH_LONG);
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        showToast("Loaded successfully.", Toast.LENGTH_SHORT);
    }

    @Override
    public void showError(int pageNo, Throwable error) {
        showToast("Load failed.", Toast.LENGTH_LONG);
    }

    @Override
    public void showCancelled(int pageNo) {
        showToast("Load cancelled.", Toast.LENGTH_SHORT);
    }
    
    @Override
    public void resetState() {
        if(mToast != null) {
            mToast.cancel();
            mToast = null;
        }
    }

    protected void showToast(CharSequence message) {
        if(mToast == null) {
            mToast = Toast.makeText(MyActivity.this, message, duration);
        }
        else {
            mToast.setText(message);
            mToast.setDuration(duration);
        }
        mToast.show();
    }
};
[Code 4]

The next step is implementation of UpdateableAdapterWrapper. This interface has only three methods, two allowing modifications of Adapter's backing data (clearAdapterData() and addAdapterData()) and a method returning a reference to ListAdapter (or to itself if the class also implements the ListAdapter interface).

final ArrayList<String> mData = new ArrayList<String>();
final ArrayAdapter<String> mAdapter = new ArrayAdapter<String>(MyActivity.this, R.layout.list_item, mData);
adapterWrapper = new UpdateableAdapterWrapper<List<String>>() {

    @Override
    public ListAdapter asAdapter() {
        return mAdapter;
    }

    @Override
    public void clear() {
        mData.clear();
        mAdapter.notifyDataSetChanged();
    }

    @Override
    public void addAll(List<String> data) {
        mData.addAll(data);
        mAdapter.notifyDataSetChanged();
    }
};
[Code 5]

In many cases when you implement your adapter from scratch you may find it easier to make your adapter also implement UpdateableAdapterWrapper<D>. Moreover, when the actual argument type of the <D> parameter is List , you can just extend UpdateableListAdapter (this class comes also with the library). In this case this would look like this:

adapterWrapper = new UpdateableListAdapter
     
      () { @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent){ TextView textView = convertView == null ? new TextView(parent.getContext()) : (TextView) convertView; textView.setText(getItem(position)); return textView; } };
     
[Code 6]

As you see, the preparation of the adapter wrapper is really very easy.

The implementation of loadingDelegate is a bit harder. This is a bare interface which should match quite a general purpose of loading data independently from what its source is. Let’s look at its contract.

void prepare(I initializationData);
[Code 7]

After this method is called, a sequence of loads will be performed by sequential calls to #loadMore(int, DataLoadListener<D>). Here you should save the initializationData for later use, but do not perform any heavy operations here, as it is expected to run in a synchronous manner. If your sequence of loads requires some heavy preparation, do it in the first call of #loadMore(int, DataLoadListener<D>).

boolean isReadyForNewLoad();
[Code 8]

return true if you are ready to accept a new load request. As the LoadingListWrapper does not support multiple loads at the same time, you return true only if there is no load task running. If you need to support loads running concurrently you must extend LoadingListWrapper. But as the purpose of LoadingDelagate is to supplement ListView data as the user scrolls to the end, this may be confusing to start another load before the former one has been completed.

void cancelLoad();
[Code 9]

It should cancel any running load. This should also trigger the onDataLoadCancelled() method on the listener object passed to the #loadMore(int, DataLoadListener<D>) call which triggered the running load task.

void loadMore(int loadIndex, DataLoadListener<D> listener);
[Code 10]

It should start the next load in sequence. The loadIndex argument is the index in sequence where 0 stands for the first load. LoadingListWrapper expects this method to be asynchronous. Use the listener argument to report the result of the load: listener.onDataLoadSuccess(D) or listener.onDataLoadCancelled() or listener.onDataLoadFailure(Throwable).

boolean isLoadCompleted();
[Code 11]

return true when all loads in the sequence have been processed , e.g. no more loads for the configuration set in loadingDelegate#prepare(I) will be issued.

Possible loadingDelegate implementation could look like the example below. It uses AsyncTask to perform load operations in the background but it is your choice in what way you would like to do your background operations.

loadingDelegate = new loadingDelegate<List<String>, String>() {

    private String baseQueryString;
    private AsyncTask<String, Void, Object> loadTask;
    boolean noMoreDataAvailable = true;

    @Override
    public void prepare(String initializationData) {
        this.baseQueryString = initializationData;
        noMoreDataAvailable = false;
    }

    @Override
    public boolean isReadyForNewLoad() {
        return loadTask == null;
    }

    @Override
    public void cancelLoad() {
        if(loadTask != null) {
            loadTask.cancel(true);
            loadTask = null;
        }
    }

    @Override
    public void loadMore(int loadIndex, final DataLoadListener<List<String>> listener) {
        loadTask = new AsyncTask<String, Void, Object>() {

            @Override
            protected Object doInBackground(String... params) {
                try {
                    String query = params[0];
                    List<String> result = null;
                    
                    //do load data using query...
                    
                    return result;
                }
                catch (Exception e) {
                    return e;
                }
            }
            
            @Override
            protected void onCancelled() {
                loadTask = null;
                listener.onDataLoadCancelled();
            }
            
            @Override
            protected void onPostExecute(Object result) {
                loadTask = null;
                if(result != null && result instanceof Exception) {
                    listener.onDataLoadFailure((Exception) result);
                }
                else {
                    @SuppressWarnings("unchecked")
                    List<String> data = (List<String>) result;
                    if(data == null) {
                        noMoreDataAvailable = true;
                    }
                    listener.onDataLoadSuccess(data);
                }
            }
            
        }.execute(baseQueryString + "&page=" + loadIndex);
    }

    @Override
    public boolean isLoadCompleted() {
        return noMoreDataAvailable;
    }
};
[Code 12]

However you will not always need to implement this interface directly. There are also some abstract implementations of loadingDelegate included in the library, which provide some “common case” functionality. For example if you need to switch to the background thread you can extend BgThreadloadingDelegate instead of implementing loadingDelegate from scratch.

This chapter should be just enough to grasp how this simple library works. Let's step forward to show some more advanced examples.

he Example Application

The application consists of four similar activities showing usage of the Loading List Library. These example activities share some simple UI. It consists of a Spinner which lists names of YouTube Standard Video Feeds, and a ListView which displays some basic info about videos in the selected feed. Feed items are loaded in small batches of 5. When the user scrolls to reach the last item, a new batch of data is loaded. However they differ from each other in both how the data is loaded and how the UI for information about the state of loads, is implemented. Starting with the simplest, the following examples gradually add more complicated features.

Before jumping into these examples, some words about data sources used in these activities is needed.

Preparing a Sample Data Source for the Application

Fetching data from the remote server, or client's database, caching and other issues are not a matter for this tutorial (although they possibly deserve a separate tutorial). Neither is an in-depth discussion of data source classes. A clear API definition for fetching data is all what is needed for further discussion.

The solution presented in this guide should be data source agnostic. This is why the presented examples utilize different ways to load data. These are also the ways which occur commonly in the Android world.

Three possibilities on how data is fetched are considered: bound service, started service and a background thread used directly from the activity code. Additionally a combined approach is presented: the cached data is fetched in the activity started background thread, but when the cache is missing, the activity starts service to fetch them from remote server.

YouTube Data API v. 2.0 serves as a remote data source. This is a public API, provided by a well established company which probably will be maintained over a long time. (Although the 2.0 version brings some inconvenience as well as Android compatibility issues, the newer 3.0 version is still marked as experimental and can change over time.)

Classes related to data in the Example Project can be found in the com.samsung.example.loadinglist.youtube package. The core class in this package is YtFeedUtil. It is a static utility with methods for fetching the feed data.

public static boolean hasValidCache(Context context, YtStandardFeed feed, int pageNo, int pageSize)
[Code 13]

Method checks if there is a local cache for the given arguments.

public static YtVideoInfo[] getFeedFromCache(Context context, YtStandardFeed feed, int pageNo, int pageSize) throws StreamCorruptedException, FileNotFoundException, IOException, ClassNotFoundException
[Code 14]

Method gets feed data from cache or returns null if the cache for this data is missing.

public static YtVideoInfo[] getFeedFromServer(Context context, YtStandardFeed feed, int pageNo, int pageSize) throws MalformedURLException, IOException, ServiceException
[Code 15]

Method gets data from the YouTube server. It does not however save it to a cache.

public static void saveFeedCache(Context context, YtStandardFeed feed, int pageNo, int pageSize, YtVideoInfo[] videoFeed) throws IOException
[Code 16]

Method saves the data as a cache file.
All above methods take the following arguments:

  • Context context - is needed for net and IO operations.

  • YtStandardFeed feed - enum type for standard YouTube feeds.

  • int pageNo, int pageSize - used to calculate the offset and limit.

The data is fetched as an array of YtVideoInfo. The YtVideoInfo class is our custom lightweight class containing information about the specific video on YouTube. This class implements Serializable (so it can be Intent's extra - this feature is showed in Example 3 activity). The array of YtVideoInfo is also an additional argument for the last method.

Apart from this utility there is also a service class called YtFeedService. This service can be used both in a started and bound manner. When used as a bound service it exposes a Binder object with a single method.

public Cancellable getFeedAsync(YtStandardFeed feed, int pageNo, int pageSize, YtDataCallback<YtVideoInfo[]> callback)
[Code 17]

As opposed to the utility class methods, this method runs asynchronously.

The first three arguments are analogue to those in the synchronous utility class. The last one is a callback for getting the result of the asynchronous task (through calls to its onSuccess, onFailure and onCancelled methods accordingly).

It returns Cancellable object which provides the capability to cancel a previously started asynchronous task if it is still running.

The method gets feed data from cache or from the server when cache is not available. After fetching data from the remote server it saves it as cache before executing the callback method. If canceled while data is being fetched from the server, the data is loaded and then saved to the cache, although the onCancelled callback method is called immediately after cancel.

The service can also be used as a started service. In this case it accepts intents with one out of two possible actions. In both cases you put information about the feed type, page number and page size in extras.

Starting the service with the YtFeedService.ACTION_GET_FEED_SOURCE action is analogue to calling getFeedAsync from the Binder object, except for the fact that the fetched data is sent back as serialized extra in the intent. The service is used this way in the Example 3 activity.

The service started with the YtFeedService.ACTION_IS_FEED_AVAILABLE action checks if there is a cache for the given feed and page, downloads it when is not available, and finally broadcasts a specific intent when the appropriate data can be loaded from cache (what the activity should do on its own). The service is used this way in the Example 4 activity.

Example 1

This is the simplest of the example activities. The YtFeedUtil class is used directly from the activity. UI for visual feedback about loading state is provided by a very simple LoadingStateListener implementation with Toasts.

You start with creating the activity whose content view contains both a Spinner and ListViev instance. The ListView object is the target for batch loads and the Spinner object is the control with which the user selects which YouTube Standard Feed is to be loaded. As you see the spinner is populated from an ArrayAdapter initialized with YtStandardFeed enum values.

public class Example1Activity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.example_activity);

		ListView listView = (ListView) findViewById(R.id.listView);

		Spinner spinner = (Spinner) findViewById(R.id.spinner);
		SpinnerAdapter spinnerAdapter = new ArrayAdapter<YtStandardFeed>(this, R.layout.spinner_item, YtStandardFeed.values());
		spinner.setAdapter(spinnerAdapter);
	}
}
[Code 18]

The layout for the activity:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true" />

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_below="@+id/spinner" />
</RelativeLayout>
[Code 19]

Now the ListView and Spinner instances should get connected to LoadingListWrapper. To do so, first declare a member field in Activity:

private LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed> mLoadingWrapper;
[Code 20]

As you can see, the type of the data portion that will be fetched in a single load is List<YtVideoInfo> and the type which will describe what is to be loaded in sequence is YtStandardFeed enum.

Then, in onCreate(), create the actual instance.

mLoadingWrapper = new LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed>(new VideoEntryListAdapter(), new loadingDelegateActivityLocalImpl(this));
[Code 21]

The constructor expects two arguments: UpdateableAdapterWrapper<D> and loadingDelegate<D, I>. (By the way, in both of them <D> and <I> correspond to formal type parameters in the LoadingListWrapper<D, I> class). You see here two new classes, which are part of the example application. They will be discussed later soon, for now it suffices to know that they implement interfaces required by LoadingListWrapper's constructor.

Then the LoadingStateListener is set. As with the two components above, the custom implementation class will be discussed later.

mLoadingWrapper.setLoadingStateListener(new SimpleToastLoadingStateListener(this));
[Code 22]

The last thing to do with mLoadingWrapper in onCreate is binding a ListView object to it.

mLoadingWrapper.bindListView(listView);
[Code 23]

In the onDestroy method LoadingListWrapper gets unbound from ListView.

@Override 
protected void onDestroy() {
	mLoadingWrapper.unbindListView();
	super.onDestroy();
}
[Code 24]

A user starts the sequence of loads selecting the feed type from the spinner. To enable this, the activity implements Spinner.OnItemSelectedListener.

public class Example1Activity extends Activity implements OnItemSelectedListener {
[Code 25]

Then in onCreate, after the spinner has been found in the content view, the activity is set as OnItemSelectedListener to spinner.

spinner.setOnItemSelectedListener(this);
[Code 26]

The onItemSelected method of the interface is implemented to trigger the first load in sequence (the next ones will be triggered when the user scrolls to the end of list).

@Override 
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
	YtStandardFeed feed = (YtStandardFeed) parent.getAdapter().getItem(position);
	mLoadingWrapper.triggerFirstLoad(feed);
}

@Override 
public void onNothingSelected(AdapterView<?> parent) {}
[Code 27]

At this moment the skeleton of the Activity is ready. It will be more or less the moment from which the discussion on the other examples will begin too. The complete code for the activity is as below.

public class Example1Activity extends Activity implements OnItemSelectedListener {

	LoadingListWrapper
     
      , YtStandardFeed> mLoadingWrapper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.example_activity); ListView listView = (ListView) findViewById(R.id.listView); mLoadingWrapper = new LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed>(new VideoEntryListAdapter(), new loadingDelegateActivityLocalImpl(this)); mLoadingWrapper.setLoadingStateListener(new SimpleToastLoadingStateListener(this)); mLoadingWrapper.bindListView(listView); Spinner spinner = (Spinner) findViewById(R.id.spinner); SpinnerAdapter spinnerAdapter = new ArrayAdapter<YtStandardFeed>(this, R.layout.spinner_item, YtStandardFeed.values()); spinner.setAdapter(spinnerAdapter); spinner.setOnItemSelectedListener(this); } @Override protected void onDestroy() { mLoadingWrapper.unbindListView(); super.onDestroy(); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { YtStandardFeed feed = (YtStandardFeed) parent.getAdapter().getItem(position); mLoadingWrapper.triggerFirstLoad(feed); } @Override public void onNothingSelected(AdapterView<?> parent) {} }
     
[Code 28]

After having finished work with the activity it is time to return to discussing the implementation of classes which has been passed to LoadingListWrapper's constructor.

First, let’s discuss the implementation of UpdateableAdapterWrapper<D> i.e. the VideoEntryListAdapter class. It does not implement the interface directly but extends the UpdateableListAdapter<D extends List<?>> class (which is also shipped with the library). This abstract class extends BaseAdapter and implements UpdateableAdapterWrapper<D>, it additionally assumes List to be the formal parameter type for updating the adapter.

public class VideoEntryListAdapter extends UpdateableListAdapter<YtVideoInfo>  {

    private static class ViewTag {

        private TextView mTitleView;
        private TextView mDescrView;

        public ViewTag(View root) {
            root.setTag(this);
            mTitleView = (TextView) root.findViewById(R.id.itemTitle);
            mDescrView = (TextView) root.findViewById(R.id.itemDescr);
        }

        public void setVideoEntry(YtVideoInfo videoEntry) {
            mTitleView.setText(videoEntry.title);
            mDescrView.setText(videoEntry.webPlayerUrl);
        }
    }
    
    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final ViewTag tag;
        if(convertView == null) {
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.video_feed_item, parent, false);
            tag = new ViewTag(convertView);
        }
        else {
            tag = (ViewTag) convertView.getTag();
        }
        tag.setVideoEntry(getItem(position));
        return convertView;
    }
}
[Code 29]

There are only two abstract methods to be implemented and they both come from the Adapter interface not from the library interface. As the implementation does not differ from the typical BaseAdpater implementation, and the adapters themselves are not the topic of the article, the code itself is enough for explanation.

This class will serve as the Adapter and UpdateableAdapterWrapper also in the following examples.

The next component we will look at is LoadingStateListener. To let you focus on the other components - For now it is a simple, slightly improved toast version, from the introduction to library. You will encounter more “real life” implementations of this interface in the oncoming examples.

public class SimpleToastLoadingStateListener implements LoadingStateListener {

    private final Context mContext;
    private Toast mToast;

    public SimpleToastLoadingStateListener(Context context) {
        mContext = context;
    }
    
    @Override
    public void showLoading(int pageNo) {
        showToast("Load started. (page number: " + pageNo + ")", Toast.LENGTH_LONG);
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        showToast("Loaded successfully. (page number: " + pageNo + ", count after load: " + countAfterLoad + ", is last page: " + (isLastPage ? "yes" :"no") + ")", Toast.LENGTH_SHORT);
    }

    @Override
    public void showError(int pageNo, Throwable error) {
        showToast("Load failed. (page number: " + pageNo + ")", Toast.LENGTH_LONG);
    }

    @Override
    public void showCancelled(int pageNo) {
        showToast("Load cancelled. (page number: " + pageNo + ")", Toast.LENGTH_SHORT);
    }
    
    @Override
    public void resetState() {
        if(mToast != null) {
            mToast.cancel();
            mToast = null;
        }
    }

    protected void showToast(CharSequence message, int duration) {
        if(mToast == null) {
            mToast = Toast.makeText(mContext, message, duration);
        }
        else {
            mToast.setText(message);
            mToast.setDuration(duration);
        }
        mToast.show();
    }
}
[Code 30]

The most project specific and complex part is the loadingDelegate implementation. The contract of this interface has been discussed earlier. As mentioned before, in this example to load data portions, YtFeedUtil class is used directly from the activity (e.g. not using a service). As the methods from the YtFeedUtil class are synchronous, they should be used in a background thread. This could be implemented with the use of AsyncTask as in the Introduction section. However, because it is a very common use case the library comes with an abstract implementation matching this situation.

Instead of implementing LoadingDelagate from scratch it extends the BgThreadloadingDelegate class. There are only two abstract methods to implement.

public class loadingDelegateActivityLocalImpl extends BgThreadloadingDelegate<List<YtVideoInfo>, YtStandardFeed, Void> {
    
    @Override
    public boolean isLoadCompleted() {
        // TODO Auto-generated method stub
        return false;
    }
    
    @Override
    protected List<YtVideoInfo> loadMoreInBackground(int loadIndex, YtStandardFeed initializationData, Void state) throws InterruptedException, Exception {
        // TODO Auto-generated method stub
        return null;
    }
}
[Code 31]

The class starts a new thread for every load. The load operation should be done in the synchronous abstract method D loadMoreInBackground(int, I, S) which, as its name says is called in the background thread. This method should return a loaded portion of data or throw an Exception on failure. If it performed a sequence of heavy operations, it could be reasonable also to check if the loading thread has been interrupted (an Interrupt request is sent to the loading thread after cancelLoad() has been called). The class also calls appropriate methods of DataLoadListener which were passed to the loadMore method.

public static final int PAGE_SIZE = 5;

private final Context mContext;
public loadingDelegateActivityLocalImpl(Context context) {
    mContext = context;
}
    
@Override
protected List<YtVideoInfo> loadMoreInBackground(int loadIndex, YtStandardFeed initializationData, Void state)    throws InterruptedException, Exception {

    YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, initializationData, loadIndex, PAGE_SIZE);
    if(result == null) {
        if(Thread.currentThread().isInterrupted()) {
            throw new  InterruptedException();
        }
        result = YtFeedUtil.getFeedFromServer(mContext, initializationData, loadIndex, PAGE_SIZE);
        YtFeedUtil.saveFeedCache(mContext, initializationData, loadIndex, PAGE_SIZE, result);
    }
    return Arrays.asList(result);
}
[Code 32]

As you see the class member mContext is final and PAGE_SIZE is a constant value so they both can be referenced from the background thread method, but what if you needed to use a member field value which changes over time from background thread?

This is exactly what the class’s third formal type parameter <S> is for. Suppose the page size were not a constant but a member field, which is not final and can change between loads (maybe by a setter method called from the main thread) and it was not declared as volatile either. If then you wanted to use it in loadMoreInBackground but avoid the cost of a synchronized block, you could override the S onBeforeLoad(int, I) method to return its value as state and then use it as the state parameter in loadMoreInBackground. (The class’s actual type parameter for <S> should then Integer not Void as it is now).

private Integer mPageSize;

@Override
protected Integer onBeforeLoad(int loadIndex, YtStandardFeed initializationData) {
    return mPageSize;
}

@Override
protected List<YtVideoInfo> loadMoreInBackground(int loadIndex, YtStandardFeed initializationData, Integer pageSize) throws InterruptedException, Exception {
    YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, initializationData, loadIndex, pageSize);
    //...
[Code 33]

The second method that must be implemented is isLoadCompleted() defined in the loadingDelegate interface which has been already discussed in the introduction to the library on page five.

First a flag mIsCompleted is declared. When the loaded portion of data is smaller than the specified page size the flag is set to true, the flag is returned by isLoadCompleted().

private boolean mIsCompleted;

@Override 
public boolean isLoadCompleted() {
    return mIsCompleted;
}
[Code 34]

As the isLoadCompleted() method is supposed to be called from the main thread, the flag should not be modified in loadMoreInBackground. However, any of the DataLoadListener's method calls can be easily intercepted.

@Override 
public void onDataLoadSuccess(List<YtVideoInfo> data) {
    if(data == null || data.size() < PAGE_SIZE) {
        mIsCompleted = true;
    }
    super.onDataLoadSuccess(data);
}
[Code 35]

The flag should also be reset to false every time a new sequence of loads is started.

@Override 
public void prepare(YtStandardFeed initializationData) {
    mIsCompleted = true;
    super.prepare(initializationData);
}
[Code 36]

This way of checking if the loading delegate is fully loaded looks like a common scenario, doesn’t it? For this reason an extended version of BgThreadloadingDelegate<D, I, S> has been added to the library. The DetectCompletedBgThreadloadingDelegate<D extends List<?>, I, S> class detects if the sequence of loads is completed exactly in the way showed in the code above.

public class loadingDelegateActivityLocalImpl extends DetectCompletedBgThreadloadingDelegate<List<YtVideoInfo>, YtStandardFeed, Void> {

    private final Context mContext;
    
    public loadingDelegateActivityLocalImpl(Context context) {
        this(context, 5);
    }
    
    public loadingDelegateActivityLocalImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    protected List<YtVideoInfo> loadMoreInBackground(int pageNo, YtStandardFeed feed, Void state) throws InterruptedException, Exception {
        YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
        if(result == null) {
            if(Thread.currentThread().isInterrupted()) {
                throw new  InterruptedException();
            }
            result = YtFeedUtil.getFeedFromServer(mContext, feed, pageNo, getPageSize());
            YtFeedUtil.saveFeedCache(mContext, feed, pageNo, getPageSize(), result);
        }
        return Arrays.asList(result);
    }
}
[Code 37]

Example 2

This example is a little more complicated. The loadingDelegate implementation class uses the bound service's Binder object for loading data. A custom LoadingStateListener implementation will be used as the ListView object's footer view (so it has to be a View descendant, too).

The activity class is very similar to the one from the first example. Create it by copying the code from the first example and rename it as follows.

public class Example2Activity extends Activity implements OnItemSelectedListener {
[Code 38]

Then add only a few modifications in onCreate.

//...    
ListView listView = (ListView) findViewById(R.id.listView);

mLoadingWrapper = new LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed>(new VideoEntryListAdapter(), new loadingDelegateBoundServiceImpl(this));
            
FooterViewLoadingStateListener loadingStateListener = FooterViewLoadingStateListener.inflateInstance(this, null);
listView.addFooterView(loadingStateListener);
loadingStateListener.setLoadingWrapper(mLoadingWrapper);

mLoadingWrapper.bindListView(listView);
mLoadingWrapper.setLoadingStateListener(loadingStateListener);

Spinner spinner = (Spinner) findViewById(R.id.spinner);
//...
[Code 39]

In the code above you see the new loadingDelegate implementation class loadingDelegateBoundServiceImpl.

Other important changes regard to LoadingStateListener implementation. This is a class extending LinearLayout (in order to let its instances to be set as a ListView’s footer view) which implements the LoadingStateListener interface at the same time. It requires a reference to the LoadingListWrapper instance as well.

The other methods which have been taken from Example1Activity remain untouched.
The activity's code does not differ significantly from the previous one. Let’s look closer at the new classes.

This time loadingDelegate implementation will take use of the bound service (which starts a background thread on its own) so there is no need to extend the BgThreadloadingDelegate class. You could implement the loadingDelegate interface directly, but this time you will need to know of another abstract loadingDelegate implementation: the DetectCompletedloadingDelegate class. This class can be useful if your loads are lists of records of the same size. It can then take over the responsibility for checking if a sequence of loads is completed. The class itself implements the DataLoadListener interface and wraps over the DataLoadListener instance passed in loadMore. Instead of implementing loadMore(int, DataLoadListener) from loadingDelegate you implement the doLoadMore(int) method. The resulting load is reported by calling methods like onDataLoadSuccess and so on, directly on the DetectCompletedloadingDelegate class.

public class loadingDelegateBoundServiceImpl extends DetectCompletedloadingDelegate<List<YtVideoInfo>, YtStandardFeed> {
    
    public loadingDelegateBoundServiceImpl(int pageSize) {
        super(pageSize);
    }
    
    @Override
    protected void doLoadMore(int loadIndex) {
        //here we should do load
        //and call onDataLoadSuccess(result) on this.
    }
}
[Code 40]

The loading delegate also implements the ServiceConnection interface. It will useful for a connection with a bound service. A Context reference constructor's argument is also needed to connect to the service.

public class loadingDelegateBoundServiceImpl extends DetectCompletedloadingDelegate<List<YtVideoInfo>, YtStandardFeed>
    implements ServiceConnection {
    
    private final Context mContext;
    private int mPageNo;
    private YtStandardFeed mFeed;
    
    public loadingDelegateBoundServiceImpl(Context context) {
        this(context, 5);
    }
    
    public loadingDelegateBoundServiceImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    public void prepare(YtStandardFeed initializationData) {
        super.prepare(initializationData);
        mFeed = initializationData;
    }
    
    @Override
    protected void doLoadMore(int loadIndex) {
        mPageNo = loadIndex;
        mContext.bindService(new Intent(mContext, YtFeedService.class), this, Context.BIND_AUTO_CREATE);
    }
    
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        YtFeedBinder binder = ((YtFeedBinder) service);
        binder.getFeedAsync(mFeed, mPageNo, getPageSize(), new YtDataCallback<YtVideoInfo[]>() {
            
                @Override
                public void onSuccess(YtVideoInfo[] data) {
                    onDataLoadSuccess(data == null ? null : Arrays.asList(data));
                }
                    
                @Override
                public void onFailure(Exception e) {
                    onDataLoadFailure(e);
                }
                    
                //onDataLoadCancelled is called in cancelLoad
                @Override
                public void onCancelled() {}
            });
    }
    
    @Override
    public void onServiceDisconnected(ComponentName name) {}
}
[Code 41]

The code is straightforward. The doLoadMore method stores the page number for later use in a member field, and binds to YtFeedService passing “self-object” as a ServiceConnection to the bindService method. Then in onServiceConnected a reference to the binder object is passed as an argument and the binder’s method for loading the feed asynchronously is called. In the callback passed to this asynchronous method the corresponding method of DataLoadListener (which is also implemented by this loadingDelegate class) is called.

Seems to be done? But it isn't. There is still some cleaning up to do.

The cancelLoad method should really cancel the task which has been started with the binder method. Otherwise, it will call any of the passed callback methods when the task is completed. The binder method provides an easy means to cancel it by returning a Cancellable object. All that is to be done is to store it for later use. So, declare a member field…

private Cancellable mLastLoad;
[Code 42]

…then in onServiceConnected store the returned reference to the Cancellable object…

mLastLoad = binder.getFeedAsync(mFeed, mPageNo, getPageSize(), //...
[Code 43]

…and finally override cancelLoad to make use of it.

@Override
public void cancelLoad() {
    if(mLastLoad != null) {
        mLastLoad.cancel();
    }
    super.cancelLoad();
}
[Code 44]

As the LoadingDeleagete binds context to the service for every load, it must be unbound after the load is done (including failure and cancel scenarios). To do so, declare a member field flag…

private boolean mConnected;
[Code 45]

…set the flag appropriately in ServiceConnection methods…

@Override
public void onServiceDisconnected(ComponentName name) {
    mConnected = false;
}

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    mConnected = true;
    //...
[Code 46]

…create a method to unbind if it is connected. This method will be executed on finishing the load task, so it is a convenient place to clear a reference to the last loads Cancellable object as well…

private void commonCleanUp() {
    mLastLoad = null;
    if(mConnected) {
        mContext.unbindService(this);
        mConnected = false;
    }
}
[Code 47]

…finally intercept calls to the wrapped DataLoadListener to execute the clean-up method before the listener methods.

@Override
public void onDataLoadSuccess(List<YtVideoInfo> data) {
    commonCleanUp();
    super.onDataLoadSuccess(data);
}
    
@Override
public void onDataLoadFailure(Throwable error) {
    commonCleanUp();
    super.onDataLoadFailure(error);
}
    
@Override
public void onDataLoadCancelled() {
    commonCleanUp();
    super.onDataLoadCancelled();
}
[Code 48]

Now it is actually done, but there is a place for some improvement. Notice, the callback passed to the binder is created on each call, but it is always the same and does not need to access any of the locally declared final variables. You can make the class either implement YtDataCallback or move it to a final member field. For more readability select the second option. The complete code should look as presented below.

public class loadingDelegateBoundServiceImpl extends DetectCompletedloadingDelegate<List<YtVideoInfo>, YtStandardFeed>
implements ServiceConnection {

    private final Context mContext;
    private int mPageNo;
    private YtStandardFeed mFeed;
    private Cancellable mLastLoad;
    private boolean mConnected;
    private final YtDataCallback<YtVideoInfo[]> mBinderCallback = new YtDataCallback<YtVideoInfo[]>() {
        
            @Override
            public void onSuccess(YtVideoInfo[] data) {
                onDataLoadSuccess(data == null ? null : Arrays.asList(data));
            }
        
            @Override
            public void onFailure(Exception e) {
                onDataLoadFailure(e);
            }
        
            @Override
            public void onCancelled() {
                //onDataLoadCancelled is called in cancelLoad
            }
        };

    public loadingDelegateBoundServiceImpl(Context context) {
        this(context, 5);
    }
    
    public loadingDelegateBoundServiceImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    public void prepare(YtStandardFeed initializationData) {
        super.prepare(initializationData);
        mFeed = initializationData;
    }

    @Override
    protected void doLoadMore(int loadIndex) {
        mPageNo = loadIndex;
        mContext.bindService(new Intent(mContext, YtFeedService.class), this, Context.BIND_AUTO_CREATE);
    }
    
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mConnected = true;
        YtFeedBinder binder = ((YtFeedBinder) service);
        mLastLoad = binder.getFeedAsync(mFeed, mPageNo, getPageSize(), mBinderCallback);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mConnected = false;
    }
    
    @Override
    public void cancelLoad() {
        if(mLastLoad != null) {
            mLastLoad.cancel();
        }
        super.cancelLoad();
    }
    
    @Override
    public void onDataLoadSuccess(List<YtVideoInfo> data) {
        commonCleanUp();
        super.onDataLoadSuccess(data);
    }
    
    @Override
    public void onDataLoadFailure(Throwable error) {
        commonCleanUp();
        super.onDataLoadFailure(error);
    }
    
    @Override
    public void onDataLoadCancelled() {
        commonCleanUp();
        super.onDataLoadCancelled();
    }

    private void commonCleanUp() {
        mLastLoad = null;
        if(mConnected) {
            mContext.unbindService(this);
            mConnected = false;
        }
    }
}
[Code 49]

Another new class in this example is FooterViewLoadingStateListener. It should provide information about the loading state to the user interface according to the LoadingStateListener interface contract. As this will be set as a footer view of the ListView it must extend any of the View classes too.

The layout template for the footer view contains three elements: ProgressBar - to be displayed when loading is being performed, TextView to display short text information about the actual state and a Button to allow the user to retry a load after the previous one has failed or has been canceled.

<com.samsung.android.example.loadinglist.FooterViewLoadingStateListener 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/loader_bg"
    android:orientation="vertical"
    android:padding="10dp" >

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:ellipsize="end"
        android:maxLines="7"
        android:singleLine="false"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textIsSelectable="true" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:layout_marginTop="10dp"
        android:text="@string/retry_load" />
</com.samsung.android.example.loadinglist.FooterViewLoadingStateListener>
[Code 50]

The FooterViewLoadingStateListener class extends the LinearLayout view group class. The view content is inflated from the template. Member fields store references to inflated sub-views. As one of these views is a button, the class also implements View. OnClickListener and passes the self-instance as a listener to the button in onFinishInflate.

public class FooterViewLoadingStateListener extends LinearLayout implements LoadingStateListener, View.OnClickListener {

    public static FooterViewLoadingStateListener inflateInstance(Context context, ViewGroup root) {
        return (FooterViewLoadingStateListener) inflate(context, R.layout.loading_control_view, null);
    }
    
    private View mLoadingView;
    private View mRetryButton;
    private TextView mTextInfo;
    
    public FooterViewLoadingStateListener(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    protected void onFinishInflate() {
        mTextInfo = (TextView) findViewById(R.id.textView);
        mLoadingView = findViewById(R.id.progressBar);
        mRetryButton = findViewById(R.id.button);
        mRetryButton.setOnClickListener(this);
        resetState();
    }
[Code 51]

The next step is the implementation of LoadingStateListener methods with the utilization of inflated views.

@Override
    public void showLoading(int pageNo) {
        mRetryButton.setVisibility(GONE);
        mLoadingView.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        mTextInfo.setText("Loading...");
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        resetState();
    }

@Override
    public void showError(int pageNo, Throwable error) {
        mLoadingView.setVisibility(GONE);
        mRetryButton.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        StringBuilder errorMessage = new StringBuilder("Load has failed.");
        //top up with information from exception - skipped for clarity...
        mTextInfo.setText(errorMessage);
    }

    @Override
    public void showCancelled(int pageNo) {
        mLoadingView.setVisibility(GONE);
        mRetryButton.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        mTextInfo.setText("Load has been cancelled.");
    }

    @Override
    public void resetState() {
        mRetryButton.setVisibility(GONE);
        mLoadingView.setVisibility(GONE);
        mTextInfo.setVisibility(GONE);
        mTextInfo.setText("");
    }

    @Override
    public void onClick(View v) {
        
    }
}
[Code 52]

The footer view now looks as expected but there is still something to fix. To make the retry button actually cause a retry of a failed load, a reference to LoadingListWrapper is needed.

private LoadingListWrapper<?, ?> mLoadingWrapper;

public void setLoadingWrapper(LoadingListWrapper<?, ?> loadingWrapper) {
    mLoadingWrapper = loadingWrapper;
}
[Code 53]

Having this reference the onClick method can do its job.

@Override
public void onClick(View v) {
    mLoadingWrapper.triggerLoad();
}
[Code 54]

There is another issue which is related to the fact that this view will be used as the footer view of a list. If any of the “show state” methods make the view grow in size, the ListView will not automatically scroll down to show the whole view. This should be done manually by another method which enforces the list, to which mLoadingWrapper is bound, to scroll down.

private void scrollListToEnd() {
    if(mLoadingWrapper.isBoundToListView()) {
        ListView listView = mLoadingWrapper.getListView();
        listView.setSelection(listView.getCount() - 1);
    }
}
[Code 55]

This is then called from within methods that can potentially make the list grow, e.g.: showLoading, showError, showCancelled.

Finally the class looks like on the listing below.

public class FooterViewLoadingStateListener extends LinearLayout implements LoadingStateListener, View.OnClickListener {
    
    public static FooterViewLoadingStateListener inflateInstance(Context context, ViewGroup root) {
        return (FooterViewLoadingStateListener) inflate(context, R.layout.loading_control_view, null);
    }
    
    private View mLoadingView;
    private View mRetryButton;
    private TextView mTextInfo;
    private LoadingListWrapper<?, ?> mLoadingWrapper;

    public FooterViewLoadingStateListener(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    protected void onFinishInflate() {
        mTextInfo = (TextView) findViewById(R.id.textView);
        mLoadingView = findViewById(R.id.progressBar);
        mRetryButton = findViewById(R.id.button);
        mRetryButton.setOnClickListener(this);
        resetState();
    }

    @Override
    public void showLoading(int pageNo) {
        mRetryButton.setVisibility(GONE);
        mLoadingView.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        mTextInfo.setText("Loading...");
        scrollListToEnd();
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        resetState();
    }

    @Override
    public void showError(int pageNo, Throwable error) {
        mLoadingView.setVisibility(GONE);
        mRetryButton.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        StringBuilder errorMessage = new StringBuilder("Load has failed.");
        if (error != null) {
            errorMessage.append("\nException: ").append(error.getClass().getName());
            if (error.getMessage() != null) {
                errorMessage.append("\nMessage: ").append(error.getMessage());
            }
            StackTraceElement[] st = error.getStackTrace();
            if (st != null && st.length > 0) {
                errorMessage.append("\nStack trace: ");
                for (int i = 0; i < Math.min(st.length, 5); i++) {
                    errorMessage.append("\n").append(st[i].toString());
                }
            }
        }
        mTextInfo.setText(errorMessage);
        scrollListToEnd();
    }

    @Override
    public void showCancelled(int pageNo) {
        mLoadingView.setVisibility(GONE);
        mRetryButton.setVisibility(VISIBLE);
        mTextInfo.setVisibility(VISIBLE);
        mTextInfo.setText("Load has been cancelled.");
        scrollListToEnd();
    }

    @Override
    public void resetState() {
        mRetryButton.setVisibility(GONE);
        mLoadingView.setVisibility(GONE);
        mTextInfo.setVisibility(GONE);
        mTextInfo.setText("");
    }

    @Override
    public void onClick(View v) {
        mLoadingWrapper.triggerLoad();
    }

    public void setLoadingWrapper(LoadingListWrapper<?, ?> loadingWrapper) {
        mLoadingWrapper = loadingWrapper;
    }
    
    private void scrollListToEnd() {
        if(mLoadingWrapper.isBoundToListView()) {
            ListView listView = mLoadingWrapper.getListView();
            listView.setSelection(listView.getCount() - 1);
        }
    }
}
[Code 56]

Example 3

In this example loads are processed with a simple started service. The visual feedback about the loading state is presented to the user by means of views that are activity member fields, inflated from the activity's XML view template.

The activity’s layout template used in previous examples is modified to include new member views.

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/spinner"
        android:layout_marginBottom="5dp"
        android:background="#FF777777"
        android:padding="10dp"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textColor="#FFDDDDDD"
        android:textIsSelectable="true" />

    <RelativeLayout
        android:id="@+id/bottomViews"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true" >

        <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/retry_load"
            android:visibility="gone" />

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="5dp"
            android:background="@color/loader_bg"
            android:padding="10dp"
            android:visibility="gone" />
    </RelativeLayout>

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/bottomViews"
        android:layout_below="@+id/textView" />
</RelativeLayout>
[Code 57]

As in the previous example, the activity class is created by copying the code from the first example and renamed appropriately. It also implements the following two additional interfaces: LoadingStateListener, OnClickListener.

Additional fields are declared to keep references to new views from the template. The content view’s template is changed to the one with additional views and inflated views are assigned to member fields. You see also, a new implementation of loadingDelegate e.g.: loadingDelegateStartedServiceSimpleImpl class (which is discussed later on). The activity is set as LoadingStateListener to mLoadingWrapper and as OnClickListener to mRetryButton. Later in the code onClick is implemented to trigger a new load, and LoadingStateListener methods to provide visual feedback when the loading state changes.

public class Example3Activity extends Activity implements OnItemSelectedListener, LoadingStateListener, OnClickListener {

    private LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed> mLoadingWrapper;
    private View mLoadingView;
    private TextView mStatusView;
    private View mRetryButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.example_activity_3);

        mLoadingView = findViewById(R.id.progressBar);
        mStatusView = (TextView) findViewById(R.id.textView);
        mRetryButton = findViewById(R.id.button);
        mRetryButton.setOnClickListener(this);

        ListView listView = (ListView) findViewById(R.id.listView);

        mLoadingWrapper = new LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed>(new VideoEntryListAdapter(), new loadingDelegateStartedServiceSimpleImpl(this));

        mLoadingWrapper.setLoadingStateListener(this);
        mLoadingWrapper.bindListView(listView);

        Spinner spinner = (Spinner) findViewById(R.id.spinner);
        SpinnerAdapter spinnerAdapter = new ArrayAdapter<YtStandardFeed>(
        this, R.layout.spinner_item, YtStandardFeed.values());
        spinner.setAdapter(spinnerAdapter);
        spinner.setOnItemSelectedListener(this);
    }

    @Override
    protected void onDestroy() {
        mLoadingWrapper.unbindListView();
        super.onDestroy();
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        YtStandardFeed feed = (YtStandardFeed) parent.getAdapter().getItem(position);
        mLoadingWrapper.triggerFirstLoad(feed);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {}

    @Override
    public void showLoading(int pageNo) {
        mRetryButton.setVisibility(View.GONE);
        mLoadingView.setVisibility(View.VISIBLE);
        setStatusText("Loading...");
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        mLoadingView.setVisibility(View.GONE);
        setStatusText(isLastPage ? "No more data to load. " : "Next loads available. ");
    }

    @Override
    public void showError(int pageNo, Throwable error) {
        mLoadingView.setVisibility(View.GONE);
        mRetryButton.setVisibility(View.VISIBLE);
        setStatusText("Last load failed.");
    }

    @Override
    public void showCancelled(int pageNo) {
        mLoadingView.setVisibility(View.GONE);
        mRetryButton.setVisibility(View.VISIBLE);
        setStatusText("Last load cancelled.");
    }

    @Override
    public void resetState() {
        mLoadingView.setVisibility(View.GONE);
        mRetryButton.setVisibility(View.GONE);
        setStatusText(null);
    }

    @Override
    public void onClick(View v) {
        mLoadingWrapper.triggerLoad();
    }

    private void setStatusText(String statusMsg) {
        StringBuilder sb = new StringBuilder("Loaded count: ").append(mLoadingWrapper.getLoadedCount()).append(".");
        if (!TextUtils.isEmpty(statusMsg)) {
            sb.append(" ").append(statusMsg);
        }
        mStatusView.setText(sb);
    }
}
[Code 58]

It looks quite similar to the previous example with a separate View serving as LoadingStateListener. It is even easier to implement. But it is rather a matter of UI requirements that decide which approach to use.

Let's step to the implementation of loadingDelegate. It extends the DetectCompletedloadingDelegate class, which you already know from the previous example. As it starts the service for loading, it needs a Context reference, too.

public class loadingDelegateStartedServiceSimpleImpl extends DetectCompletedloadingDelegate<List<YtVideoInfo>, YtStandardFeed> {

    final Context mContext;
    
    public loadingDelegateStartedServiceSimpleImpl(Context context) {
        this(context, 5);
    }
    
    public loadingDelegateStartedServiceSimpleImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    protected void doLoadMore(int loadIndex) {
        
    }
}
[Code 59]

In doLoadMore a service is started with the YtFeedService.ACTION_GET_FEED_SOURCE action. Invoked with this action, the service loads data either from the cache or from a server and broadcasts serialized feed data in the intent. The answer can be caught by BroadcastReceiver which is also registered in doLoadMore.

Before coming back to doLoadMore, please focus on broadcast receiver related issues for a moment.

An intent filter for the receiver is declared as a static field.

private static final IntentFilter sIntentFilter = new IntentFilter(ACTION_GET_FEED_SOURCE);
[Code 60]

The receiver is then declared as a final member field initialized with an anonymous inner class, extending BroadcastReceiver.

final BroadcastReceiver mReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
        if(matchesMyData(intent)) {
            killReceiverIfIsAlive();
            if(hasWrappedListener()) {
                try {
                    boolean success = intent.getbooleanExtra(EXTRA_SUCCESS, false);
                    if(!success) {
                        String errorMessage = intent.getStringExtra(EXTRA_ERROR_MSG);
                        throw new  IllegalStateException(
                            errorMessage == null ? "Unknown error in service" : errorMessage);
                        }
                        @SuppressWarnings("unchecked")
                        List<YtVideoInfo> data = (ArrayList<YtVideoInfo>)intent.getSerializableExtra(EXTRA_FEED_DATA);
                        onDataLoadSuccess(data);
                    }
                catch (Exception e) {
                    onDataLoadFailure(e);
                }
            }
        }
    }

    private boolean matchesMyData(Intent intent) {
        if(mFeed != (YtStandardFeed) intent.getSerializableExtra(EXTRA_YT_FEED_TYPE)) {
            return false;
        }
        if(mPageNo != intent.getIntExtra(EXTRA_PAGE_NO, 0)) {
            return false;
        }
        if(getPageSize() != intent.getIntExtra(EXTRA_PAGE_SIZE, -1)) {
            return false;
        }
        return true;
    }
};
[Code 61]

The receiver checks if the extras identifying feed, page number and page size match the current values of the corresponding member fields of the enclosing class. So two additional fields (the getter for the page size getPageSize() is declared in the superclass) should be declared in loadingDelegate class. There is also one more member field mIsReceiverAlive, whose meaning will be revealed in the next few lines.

YtStandardFeed mFeed;
int mPageNo;
boolean mIsReceiverAlive;
[Code 62]

After the receiver checks that the intent matches the feed type and the current page settings, it retrieves from the intent status of load. If the status stands for success it retrieves feed data and calls the onDataLoadSuccess method from the DataLoadListener interface, otherwise it calls onDataLoadError. (If you don't know why this class implements the DataLoadListener interface please refer to the description of the loadingDelegate implementation in Example 2, page 23).

Just before calling one of these methods it unregisters itself calling killReceiverIfIsAlive() from the enclosing class.

void killReceiverIfIsAlive() {
    if(mIsReceiverAlive) {
        mContext.unregisterReceiver(mReceiver);
        mIsReceiverAlive = false;
    }
}
[Code 63]

Now it's time look at the doLoadMore implementation. It registers a receiver, sets the mIsReceiverAlive flag to true and starts the service, stores the current page number value in a member field, and finally starts the service with the YtFeedService.ACTION_GET_FEED_SOURCE action for the current feed and page settings.

@Override 
public void doLoadMore(int pageNo) {
    Intent startIntent = new Intent(mContext, YtFeedService.class);
    startIntent.setAction(ACTION_GET_FEED_SOURCE);
    startIntent.putExtra(EXTRA_YT_FEED_TYPE, mFeed);
    startIntent.putExtra(EXTRA_PAGE_NO, mPageNo = pageNo);
    startIntent.putExtra(EXTRA_PAGE_SIZE, getPageSize());
    mContext.registerReceiver(mReceiver, sIntentFilter);
    mIsReceiverAlive = true;
    mContext.startService(startIntent);
}
[Code 64]

The mFeed field is set to a proper value when the sequence of loads is started.

@Override 
public void prepare(YtStandardFeed feed) {
    super.prepare(feed);
    mFeed = feed;
}
[Code 65]

The last thing, not addressed yet, relates to the canceling of loads and this time it’s really straightforward. The only thing to do is to unregister the receiver.

@Override 
public void cancelLoad() {
    killReceiverIfIsAlive();
    super.cancelLoad();
}
[Code 66]

Now, it’s done. The complete code for the loadingDelegateStartedServiceSimpleImpl class is listed below.

public class loadingDelegateStartedServiceSimpleImpl extends DetectCompletedloadingDelegate<List<YtVideoInfo>, YtStandardFeed> {

    private static final IntentFilter sIntentFilter = new IntentFilter(ACTION_GET_FEED_SOURCE);
    
    final BroadcastReceiver mReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if(matchesMyData(intent)) {
                killReceiverIfIsAlive();
                if(hasWrappedListener()) {
                    try {
                        boolean success = intent.getbooleanExtra(EXTRA_SUCCESS, false);
                        if(!success) {
                            String errorMessage = intent.getStringExtra(EXTRA_ERROR_MSG);
                            throw new  IllegalStateException(errorMessage == null ? "Unknown error in service" : errorMessage);
                        }
                        @SuppressWarnings("unchecked")
                        List<YtVideoInfo> data = (ArrayList<YtVideoInfo>)intent.getSerializableExtra(EXTRA_FEED_DATA);
                        onDataLoadSuccess(data);
                    }
                    catch (Exception e) {
                        onDataLoadFailure(e);
                    }
                }
            }
        }

        private boolean matchesMyData(Intent intent) {
            if(mFeed != (YtStandardFeed) intent.getSerializableExtra(EXTRA_YT_FEED_TYPE)) {
                return false;
            }
            if(mPageNo != intent.getIntExtra(EXTRA_PAGE_NO, 0)) {
                return false;
            }
            if(getPageSize() != intent.getIntExtra(EXTRA_PAGE_SIZE, -1)) {
                return false;
            }
            return true;
        }
    };
    final Context mContext;
    YtStandardFeed mFeed;
    int mPageNo;
    boolean mIsReceiverAlive;
    
    public loadingDelegateStartedServiceSimpleImpl(Context context) {
        this(context, 5);
    }
    
    public loadingDelegateStartedServiceSimpleImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }

    @Override
    public void prepare(YtStandardFeed feed) {
        super.prepare(feed);
        mFeed = feed;
    }
    
    @Override
    public void cancelLoad() {
        killReceiverIfIsAlive();
        super.cancelLoad();
    }

    @Override
    public void doLoadMore(int pageNo) {
        Intent startIntent = new Intent(mContext, YtFeedService.class);
        startIntent.setAction(ACTION_GET_FEED_SOURCE);
        startIntent.putExtra(EXTRA_YT_FEED_TYPE, mFeed);
        startIntent.putExtra(EXTRA_PAGE_NO, mPageNo = pageNo);
        startIntent.putExtra0(EXTRA_PAGE_SIZE, getPageSize());
        mContext.registerReceiver(mReceiver, sIntentFilter);
        mIsReceiverAlive = true;
        mContext.startService(startIntent);
    }

    void killReceiverIfIsAlive() {
        if(mIsReceiverAlive) {
            mContext.unregisterReceiver(mReceiver);
            mIsReceiverAlive = false;
        }
    }
}
[Code 67]

Example 4

In this example loads are processed with both started services as well as a local thread. As to LoadingStateListener implementation it presents a mix of already discussed approaches.

As in the previous example the activity will serve as DataLoadListener to the LoadingListWrapper instance and as in the previous example the activity’s view template example_activity.xml is modified. This time the modification is small - just one view, a ProgressBar is added.

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Spinner
        android:id="@+id/spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:prompt="@string/app_name" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="5dp"
        android:background="@color/loader_bg"
        android:padding="10dp"
        android:visibility="gone" />

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/progressBar"
        android:layout_below="@+id/spinner" />
</RelativeLayout>
[Code 68]

As you are already familiar with the contract of the LoadingStateListener the code below offers a sufficient illustration. Suffice to say that next to ProgressView we also use Toast for some messages and an AlertDialog for displaying the error message and a retry suggestion.

public class Example4Activity extends Activity implements OnItemSelectedListener, LoadingStateListener, DialogInterface.OnClickListener {

    private LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed> mLoadingWrapper;
    private Dialog mAlert;
    private Toast mToast;
    private View mLoadingView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.example_activity_4);

        mLoadingView = findViewById(R.id.progressBar);

        mLoadingWrapper = new LoadingListWrapper<List<YtVideoInfo>, YtStandardFeed>(new VideoEntryListAdapter(), new LoadingDelegateStartedServiceAndLocalImpl(this));

        ListView listView = (ListView) findViewById(R.id.listView);
        mLoadingWrapper.setLoadingStateListener(this);
        mLoadingWrapper.bindListView(listView);

        Spinner spinner = (Spinner) findViewById(R.id.spinner);
        SpinnerAdapter spinnerAdapter = new ArrayAdapter<YtStandardFeed>(this, R.layout.spinner_item, YtStandardFeed.values());
        spinner.setAdapter(spinnerAdapter);
        spinner.setOnItemSelectedListener(this);
    }

    @Override
    protected void onDestroy() {
        mLoadingWrapper.unbindListView();
        super.onDestroy();
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        YtStandardFeed feed = (YtStandardFeed) parent.getAdapter().getItem(position);
        mLoadingWrapper.triggerFirstLoad(feed);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {}

    @Override
    public void showLoading(int pageNo) {
        mLoadingView.setVisibility(View.VISIBLE);
    }

    @Override
    public void showSuccess(int pageNo, boolean isLastPage, int countBeforeLoad, int countAfterLoad) {
        mLoadingView.setVisibility(View.GONE);
        showToast(
            new StringBuilder("Load success. Total count: ").append(countAfterLoad).append(". ").append(isLastPage ? "No more data to load. " : " Next loads available. "), Toast.LENGTH_LONG);
    }

    @Override
    public void showError(int pageNo, Throwable error) {
        mLoadingView.setVisibility(View.GONE);
        StringBuilder errorMessage = new StringBuilder();
        if (error != null) {
            errorMessage.append("\nException: ").append(error.getClass().getName());
            if (error.getMessage() != null) {
                errorMessage.append("\nMessage: ").append(error.getMessage());
            }
            StackTraceElement[] st = error.getStackTrace();
            if (st != null && st.length > 0) {
                errorMessage.append("\nStack trace: ");
                for (int i = 0; i < Math.min(st.length, 5); i++) {
                    errorMessage.append("\n").append(st[i].toString());
                }
            }
        }
        dismissAlert();
        mAlert = new AlertDialog.Builder(this).setTitle("Load failed.").setMessage(errorMessage).setPositiveButton("Retry load", this).setNegativeButton("Cancel", this).create();
        mAlert.setOwnerActivity(this);
        mAlert.show();
    }

    @Override
    public void showCancelled(int pageNo) {
        dismissAlert();
        mLoadingView.setVisibility(View.GONE);
        showToast("Load has been cancelled!", Toast.LENGTH_SHORT);
    }

    @Override
    public void resetState() {
        dismissAlert();
        mLoadingView.setVisibility(View.GONE);
        if (mToast != null) {
            mToast.cancel();
            mToast = null;
        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        dialog.dismiss();
        if (which == DialogInterface.BUTTON_POSITIVE) {
            mLoadingWrapper.triggerLoad();
        }
    }

    private void dismissAlert() {
        if (mAlert != null) {
            mAlert.dismiss();
            mAlert = null;
        }
    }

    private void showToast(CharSequence message, int duration) {
        if(mToast == null) {
            mToast = Toast.makeText(this, message, duration);
        }
        else {
            mToast.setText(message);
            mToast.setDuration(duration);
        }
        mToast.show();
    }
}
[Code 69]

Consequently as in the preceding examples, this example introduces another implementation of the loadingDelegate interface. And this time it will be much fancier! Although at first glimpse it can resemble the one you know from the previous example. In fact it merges the approaches presented in the first and third examples.

Loads from the local cache are done in the activity's background thread, when the cached data for the current load is not available, loadingDelegate starts a service to fetch data from the remote server, and waits until the service has done its job. The service writes downloaded data to the cache folder and broadcasts an intent with the status of the job. After receiving the broadcast, loadingDelegate notifies the background thread to stop waiting and either fetch data from the cache once again and call onDataLoadSuccess (this when the service has sent an intent with status meaning success) or otherwise call onDataLoadFailure.

Let’s get our hands dirty and write the class.

Start with copying the loadingDelegate implementation from the first example but remove the part of code in loadMoreInBackground responsible for downloading data from the remote server (and caching it after download). For this task it will use the started service.

public class LoadingDelegateStartedServiceAndLocalImpl extends DetectCompletedBgThreadLoadingDelegate<List<YtVideoInfo>, YtStandardFeed, Void> {

    private final Context mContext;
    
    public LoadingDelegateStartedServiceAndLocalImpl(Context context) {
        this(context, 5);
    }
    
    public LoadingDelegateStartedServiceAndLocalImpl(Context context, int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    protected List<YtVideoInfo> loadMoreInBackground(int pageNo, YtStandardFeed feed, Void state) throws InterruptedException, Exception {
    
        YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
        if(result == null) {
    
            //here its time to start service and wait
            //until it tells us data is ready
    
        }
        return Arrays.asList(result);
    }
}
[Code 70]

The receiver part, in turn will strongly resemble the code written in Example 3.

Declare a static IntentFilter field.

private static final IntentFilter sIntentFilter = new IntentFilter(ACTION_IS_FEED_AVAILABLE);
[Code 71]

Notice that the action in IntentFilter is different from the one in the previous example.

Copy the BroadcastReceiver member field from Example 3, but as with loadMoreInBackground remove the code responsible for handling the matching intent.

final BroadcastReceiver mReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
        if(matchesMyData(intent)) {
            boolean success = intent.getbooleanExtra(EXTRA_SUCCESS, false);
            String errorMessage = intent.getStringExtra(EXTRA_ERROR_MSG);

            //here it is time to return back to background thread

        }
    }

    private boolean matchesMyData(Intent intent) {
        if(mFeed != (YtStandardFeed) intent.getSerializableExtra(EXTRA_YT_FEED_TYPE)) {
            return false;
        }
        if(mPageNo != intent.getIntExtra(EXTRA_PAGE_NO, 0)) {
            return false;
        }
        if(getPageSize() != intent.getIntExtra(EXTRA_PAGE_SIZE, -1)) {
            return false;
        }
        return true;
    }
};
[Code 72]

As in the previous example create a field to keep the feed type and page number for the receiver.

int mPageNo;
YtStandardFeed mFeed;
[Code 73]

The way these values are saved for use in the receiver’s methods is slightly different than in the previous example. They cannot be saved in loadMoreInBackground which runs in the background thread, while the Receiver will access these fields from the main thread. But there is the onBeforeLoad method which is called in main thread.

@Override 
protected Void onBeforeLoad(int pageNo, YtStandardFeed feed) {
    mPageNo = pageNo;
    mFeed = feed;
    return null;
}
[Code 74]

Also copy the flag telling if the receiver has been received and the method to unregister the receiver if there is any.

boolean mIsReceiverAlive;

void killReceiverIfIsAlive() {
    if(mIsReceiverAlive) {
        mContext.unregisterReceiver(mReceiver);
        mIsReceiverAlive = false;
    }
}
[Code 75]

Finally create a method for registering the receiver and starting the service, which much resembles the doLoadMore method's body from Example 3 (notice also that the Intent has different an action than in Example 3).

void initServiceAndReceiver(YtStandardFeed feed, int pageNo) {
    Intent startIntent = new Intent(mContext, YtFeedService.class);
    startIntent.setAction(ACTION_IS_FEED_AVAILABLE);
    startIntent.putExtra(EXTRA_YT_FEED_TYPE, feed);
    startIntent.putExtra(EXTRA_PAGE_NO, pageNo);
    startIntent.putExtra(EXTRA_PAGE_SIZE, getPageSize());
    mContext.registerReceiver(mReceiver, sIntentFilter);
    mContext.startService(startIntent);
    mIsReceiverAlive = true;
}
[Code 76]

After the skeleton of the class has been built from familiar chunks let's move on to what is new.

Another member field is needed to pass the exception (in case the operation done by the service was not successful) between the receiver and the background thread. This field will be accessed only from synchronized blocks with a lock on the loading thread.

Exception mLoadError;
[Code 77]

After in loadMoreInBackground has checked that the local data for the current load is not available it starts a service and registers a receiver, then it lets the background thread wait.

@Override 
protected List<YtVideoInfo> loadMoreInBackground(int pageNo, YtStandardFeed feed, 
    Void state) throws InterruptedException, Exception {

    YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
    if(result == null) {
        initServiceAndReceiver(feed, pageNo);
        Thread loadingThread = Thread.currentThread();
        synchronized (loadingThread) {
            try {
                loadingThread.wait();

                //after receiver called notify() on this thread
                //and exited synchronized block!
            }
            finally {
                killReceiverIfIsAlive();
            }
            if(mLoadError != null) {
                throw mLoadError;
            }
        }
        result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
    }
    return Arrays.asList(result);
}
[Code 78]
@Override 
public void onReceive(Context context, Intent intent) {
    if(matchesMyData(intent)) {
        boolean success = intent.getbooleanExtra(EXTRA_SUCCESS, false);
        String errorMessage = intent.getStringExtra(EXTRA_ERROR_MSG);
        Thread loadingThread = getLoadingThread();
        if(loadingThread != null) {
            synchronized (loadingThread) {
                loadingThread.notify();
                if(!success) {
                    mLoadError = new IllegalStateException(
                        errorMessage == null ? "Unknown error in service" : errorMessage);
                }

                //lock with notify() call gets released
                //we return back to loading thread
            }
        }
    }
}
[Code 79]

When the receiver receives an appropriate intent it notifies the loading thread to wake up. If the service operation status was not “success” it also saves the error exception in the member field before releasing the intrinsic lock of the thread object. After that the background thread executes lines just after the line where loadingThread.wait() has been called. The receiver gets unregistered. If mLoadError is set, it is thrown here (which causes onDataLoadFailure to be called) otherwise the background thread will try to load data from cache once again.

At first glimpse you may wonder why cancelLoad() has not been overridden to unregister the receiver. It is not needed because when onDataLoadCancelled() from BgThreadloadingDelegate is called it sends an interrupt request to the loading thread. If a service is started and the receiver is registered, then the loading thread can only be waiting. If it gets interrupted then wait() will throw an InterruptedException - this is why a call to killReceiverIfIsAlive has been put in the finally block.

So putting it all together, here is the complete LoadingDelegate implementation.

public class LoadingDelegateStartedServiceAndLocalImpl extends DetectCompletedBgThreadloadingDelegate<List<YtVideoInfo>, YtStandardFeed, Void> {

    static final IntentFilter sIntentFilter = new IntentFilter(ACTION_IS_FEED_AVAILABLE);
    
    final BroadcastReceiver mReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if(matchesMyData(intent)) {
                boolean success = intent.getbooleanExtra(EXTRA_SUCCESS, false);
                String errorMessage = intent.getStringExtra(EXTRA_ERROR_MSG);
                Thread loadingThread = getLoadingThread();
                if(loadingThread != null) {
                    synchronized (loadingThread) {
                        loadingThread.notify();
                        if(!success) {
                            mLoadError = new IllegalStateException(errorMessage == null ? "Unknown error in service" : errorMessage);
                        }
                    }
                }
            }
        }

        private boolean matchesMyData(Intent intent) {
            if(mFeed != (YtStandardFeed) intent.getSerializableExtra(EXTRA_YT_FEED_TYPE)) {
                return false;
            }
            if(mPageNo != intent.getIntExtra(EXTRA_PAGE_NO, 0)) {
                return false;
            }
            if(getPageSize() != intent.getIntExtra(EXTRA_PAGE_SIZE, -1)) {
                return false;
            }
            return true;
        }
    };
    final Context mContext;
    int mPageNo;
    YtStandardFeed mFeed;
    boolean mIsReceiverAlive;
    Exception mLoadError;

    public LoadingDelegateStartedServiceAndLocalImpl(Context context) {
        this(context, 5);
    }
    
    public LoadingDelegateStartedServiceAndLocalImpl(Context context, 
        int pageSize) {
        super(pageSize);
        mContext = context;
    }
    
    @Override
    protected Void onBeforeLoad(int pageNo, YtStandardFeed feed) {
        mPageNo = pageNo;
        mFeed = feed;
        return null;
    }

    @Override
    protected List<YtVideoInfo> loadMoreInBackground(int pageNo, YtStandardFeed feed, Void state) throws InterruptedException, Exception {
        YtVideoInfo[] result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
        if(result == null) {
            initServiceAndReceiver(feed, pageNo);
            Thread loadingThread = Thread.currentThread();
            synchronized (loadingThread) {
                try {
                    loadingThread.wait();
                }
                finally {
                    killReceiverIfIsAlive();
                }
                if(mLoadError != null) {
                    throw mLoadError;
                }
            }
            result = YtFeedUtil.getFeedFromCache(mContext, feed, pageNo, getPageSize());
        }
        return Arrays.asList(result);
    }
    
    void initServiceAndReceiver(YtStandardFeed feed, int pageNo) {
        Intent startIntent = new Intent(mContext, YtFeedService.class);
        startIntent.setAction(ACTION_IS_FEED_AVAILABLE);
        startIntent.putExtra(EXTRA_YT_FEED_TYPE, feed);
        startIntent.putExtra(EXTRA_PAGE_NO, pageNo);
        startIntent.putExtra(EXTRA_PAGE_SIZE, getPageSize());
        mContext.registerReceiver(mReceiver, sIntentFilter);
        mContext.startService(startIntent);
        mIsReceiverAlive = true;
    }
    
    void killReceiverIfIsAlive() {
        if(mIsReceiverAlive) {
            mContext.unregisterReceiver(mReceiver);
            mIsReceiverAlive = false;
        }            
    }
}
[Code 80]

Summary

The tutorial has discussed examples of how to cope with loading an Adapter’s data in small batches accordingly to user behavior. It also introduces a simple library, which can be used independently to facilitate work with batch loading lists. After completing this tutorial you should be able to easily implement batch loading list scenario to any custom data source.