I’ve just started learning about Loaders, they’re a way to handle doing tasks off the UI thread. The benefits include having a listener that will inform your fragment/activity when the data your monitoring is changed. They will also account for orientation changes when loading.
A brief introduction is written here and it got me up to speed: An Intro to Loaders
This tutorial has extensive code but is pretty simple in concept. It follows on from the tutorial already written at https://developer.android.com/reference/android/content/AsyncTaskLoader.html. The Google tutorial shows you how to use an ASyncTaskLoader but with the post HoneyComb API. So I’ve taken this code and used it but coded against the support library.
There are two changes and one bug fix that need to be implemented to get this to work! I’ve also increased the OO concepts using correct packaging and class files.
- Changed out the HC ArrayAdapter and used BaseAdapter
- Removed the use of the ActionBar search function
- ASyncTask wont start unless you explicity call forceLoad()!
First off we can’t use the helper methods in extending HC ArrayAdapter, we extend BaseAdapter and do the data management ourselves. In the long run this is probably a better approach for control and customisation.
AppListAdapter.java
package com.blundell.asynctaskloader.ui.adapter; import java.util.List; import com.blundell.asynctaskloader.R; import com.blundell.asynctaskloader.domain.AppEntry; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; public class AppListAdapter extends BaseAdapter { private final LayoutInflater inflater; private List<AppEntry> data; public AppListAdapter(Context context) { inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } public void setData(List<AppEntry> data){ this.data = data; notifyDataSetChanged(); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view; if(convertView == null){ view = inflater.inflate(R.layout.list_item_icon_text, parent, false); } else { view = convertView; } AppEntry item = data.get(position); ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); ((TextView) view.findViewById(R.id.text)).setText(item.getLabel()); return view; } @Override public int getCount() { return data == null ? 0 : data.size(); } @Override public Object getItem(int position) { return data == null ? null : data.get(position); } @Override public long getItemId(int position) { return position; } }
I removed the ActionBar search functionality, I don’t think it should be part of this tutorial and just stops you seeing the wood for the trees as they say. So here’s the new fragment.
AppListFragment.java
package com.blundell.asynctaskloader.ui.fragment; import java.util.List; import com.blundell.asynctaskloader.domain.AppEntry; import com.blundell.asynctaskloader.ui.adapter.AppListAdapter; import com.blundell.asynctaskloader.ui.loader.AppListLoader; import android.os.Bundle; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.view.View; import android.widget.ListView; import android.widget.Toast; public class AppListFragment extends ListFragment implements LoaderCallbacks<List<AppEntry>> { private AppListAdapter adapter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText("No applications"); adapter = new AppListAdapter(getActivity()); setListAdapter(adapter); setListShown(false); // Prepare the loader // either reconnect with an existing one or start a new one getLoaderManager().initLoader(0, null, this); } @Override public void onListItemClick(ListView l, View v, int position, long id) { Toast.makeText(getActivity(), "Clicked: "+ id, Toast.LENGTH_SHORT).show(); } @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) { // This is called when a new loader needs to be created. // This sample only has one Loader with no arguments, so it is simple. return new AppListLoader(getActivity()); } @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) { adapter.setData(data); // The list should now be shown if(isResumed()){ setListShown(true); } else { setListShownNoAnimation(true); } } @Override public void onLoaderReset(Loader<List<AppEntry>> loader) { // Clear the data in the adapter adapter.setData(null); } }
Finally there is a bug when extending ASyncTaskLoader from the support library, the loader doesn’t actually start. It’s written up as an issue here. What you need to do is catch when the loader wants to start loading using onStartLoading(), then query if you have your data yet (because the loader could be restarted) if you have then deliver your data, else force it to start.
package com.blundell.asynctaskloader.ui.loader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.blundell.asynctaskloader.domain.AppEntry; import com.blundell.asynctaskloader.receiver.PackageIntentReceiver; import com.blundell.asynctaskloader.util.Comparator; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.support.v4.content.AsyncTaskLoader; public class AppListLoader extends AsyncTaskLoader<List<AppEntry>> { InterestingConfigChanges lastConfig = new InterestingConfigChanges(); public final PackageManager pm; List<AppEntry> apps; PackageIntentReceiver packageObserver; public AppListLoader(Context context) { super(context); // Retrieve the package manager for later use; note we don't // use 'context' directly but instead the safe global application // context returned by getContext(). pm = getContext().getPackageManager(); } @Override protected void onStartLoading() { super.onStartLoading(); // AsyncTaskLoader doesn't start unless you forceLoad https://code.google.com/p/android/issues/detail?id=14944 if(apps != null){ deliverResult(apps); } if(takeContentChanged() || apps == null){ forceLoad(); } } /** * This is where the bulk of the work. This function is called in a background thread * and should generate a new set of data to be published by the loader. */ @Override public List<AppEntry> loadInBackground() { // Retrieve all known applications List<ApplicationInfo> apps = pm.getInstalledApplications( PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS); if(apps == null){ apps = new ArrayList<ApplicationInfo>(); } final Context context = getContext(); // Create corresponding array of entries and load their labels List<AppEntry> entries = new ArrayList<AppEntry>(apps.size()); for (ApplicationInfo applicationInfo : apps) { AppEntry entry = new AppEntry(this, applicationInfo); entry.loadLabel(context); entries.add(entry); } Collections.sort(entries, Comparator.ALPHA_COMPARATOR); return entries; } /** * Called when there is new data to deliver to the client. The super class * will take care of delivering it; the implementation just adds a little more logic */ @Override public void deliverResult(List<AppEntry> apps) { if(isReset()){ // An async query came in while the loader is stopped. We don't need the result if(apps != null){ onReleaseResources(apps); } } List<AppEntry> oldApps = this.apps; this.apps = apps; if(isStarted()){ // If the loader is currently started, we can immediately deliver a result super.deliverResult(apps); } // At this point we can release the resources associated with 'oldApps' if needed; // now that the new result is delivered we know that it is no longer in use if(oldApps != null){ onReleaseResources(oldApps); } } @Override protected void onStopLoading() { // Attempts to cancel the current load task if possible cancelLoad(); } @Override public void onCanceled(List<AppEntry> apps) { super.onCanceled(apps); // At this point we can release the resources associated with 'apps' if needed onReleaseResources(apps); } /** * Handles request to completely reset the loader */ @Override protected void onReset() { super.onReset(); // ensure the loader is stopped onStopLoading(); // At this point we can release the resources associated with 'apps' if needed if(apps != null){ onReleaseResources(apps); apps = null; } // Stop monitoring for changes if(packageObserver != null){ getContext().unregisterReceiver(packageObserver); packageObserver = null; } } /** * Helper function to take care of releasing resources associated with an actively loaded data set */ private void onReleaseResources(List<AppEntry> apps){ // For a simple list there is nothing to do // but for a Cursor we would close it here } public static class InterestingConfigChanges { final Configuration lastConfiguration = new Configuration(); int lastDensity; protected boolean applyNewConfig(Resources res){ int configChanges = lastConfiguration.updateFrom(res.getConfiguration()); boolean densityChanged = lastDensity != res.getDisplayMetrics().densityDpi; if(densityChanged || (configChanges & (ActivityInfo.CONFIG_LOCALE|ActivityInfo.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0){ lastDensity = res.getDisplayMetrics().densityDpi; return true; } return false; } } }
That should do it, if you run up your app you’ll see a list of all the applications running on your device. I also changed the fragment to be instantiated in XML instead of with the fragment manager. This was only done in the Google example because it’s part of a bigger package of examples.
activity_main.xml
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:id="@+id/fragment_app_list" android:layout_width="fill_parent" android:layout_height="fill_parent" class="com.blundell.asynctaskloader.ui.fragment.AppListFragment" /> </RelativeLayout>
For the full source code see below:
Download link for the eclipse project source
Loaders is really helpful for our website making, its make us aware about our updates, we can resets and more options are available. Thank You for the informative article.
Pretty good tutorial. There’s something that’s bugging me, perhaps more than it should and that is why do we do this:
@Override
public void deliverResult(List apps) {
if(isReset()){
// An async query came in while the loader is stopped. We don’t need the result
if(apps != null){
onReleaseResources(apps);
}
}
…
}
Why would the Loader be in a reset status? 😕 I mean, it IS deliveringResults, right? as the method suggests. My question is, when does a Loader enters the reset status, why, and why is it that we don’t show data when we’re in it.
Reset means stopped, so imagine you’re activity asks to load some list but whilst it’s hitting the database the user navigates elsewhere, the loader is no longer needed and therefore the result is not needed either
Thanks for this, really helped me