AsyncTaskLoader: Populate a Static List View

One of the first things I tried to do while developing an Android app is asynchronously populate a statically defined list view. This turned out to be a much more challenging task than I anticipated, and apparently nobody else on the entire world wide web is attempting to do this (or I just didn’t google right 0_o).

Specifically, I wanted to define a ListView (or Spinner or whatever) in a layout XML file, and populate it via data from a restful web service. Initially I tried to do this in the onCreate(), but got theĀ android.os.NetworkOnMainThreadException. So, obviously I needed to pull the data from a restful web service asynchronously. This post is going to explain how I did that.

I’m going to accomplish this with an AsyncTaskLoader, and I’ll use a back to front approach, starting at the service layer and work towards the UI. The example app is an extremely simple app to list some blog posts.

First, the BlogPost model object:

public class BlogPost implements BlogPostRow {
    public DateTime date;
    public String title;
    public String content;
 
    public BlogPost(String title, String content, DateTime date) {
        this.title = title;
        this.content = content;
        this.date = date;
    }
}

Obviously the model is extremely simple for the sake of example. No getters/setters because 1) I don’t want the code bloat in a blog example, and 2) I’ve been using getters/setters for 15 years and I still don’t know what the point of them is. (Not really true.)

The service:

public class BlogPostService {
 
    public List<BlogPost> getPosts() {
        //TODO: Could you imagine if this was a real service and actually hit a restful data service endpoint?
        return Arrays.asList(
                new BlogPost("Android with maven", "blah blah blah maven rocks blah blah android this that the other thing."),
                new BlogPost("Dynamic ListView loading static ListView", "Whole lotta talking, not saying much.")
        );
    }
}

Also incredibly simple. But just pretend it’s making an HTTP request and converting a JSON response into those BlogPost objects with some library like Jackson.

Now the AsyncLoaderTask implementation, which leverages our supposed rest service:

public class BlogPostLoader extends AsyncTaskLoader<List<BlogPost>> {
    private BlogPostService service = new BlogPostService();
 
    public BlogPostLoader(Context context) {
        super(context);
    }
 
    @Override public List<BlogPost> loadInBackground() {
        return service.getPosts();
    }
}

Extending the AsyncTaskLoader class requires only an implementation of loadInBackground() and a constructor that provides a Context. Our code will not call loadInBackground() however, that is the responsibility of a Loader implementation. For this, we’ve made it to the Activity:

ListBlogsActivity:

public class ListBlogsActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
 
      final BlogListAdapter blogListAdapter = new BlogListAdapter(this, new ArrayList<BlogPost>());
      ListView blogPostListView = (ListView) findViewById(R.id.blogposts);
 
      blogPostListView.setAdapter(blogListAdapter);
 
      getLoaderManager().initLoader(0, savedInstanceState,
        new LoaderManager.LoaderCallbacks<List<BlogPost>>() {
          @Override public Loader<List<BlogPost>> onCreateLoader(int id, Bundle args) {
            return new BlogPostLoader(ListBlogsActivity.this);
          }
 
          @Override public void onLoadFinished(Loader<List<BlogPost>> loader, List<BlogPost> data) {
            blogListAdapter.setData(data);
          }
 
            @Override public void onLoaderReset(Loader<List<BlogPost>> loader) {
            blogListAdapter.setData(new ArrayList<BlogPost>());
          }
      }
    ).forceLoad();
  } 
}

A few points to note:

  • The first parameter passed to initLoader() is an integer used to uniquely identify the loader. I’ll be honest, I don’t really know why this is necessary or why the caller has to provide this. But I can tell you that if you have two loaders and give them both the same ID, you will get some unexpected behavior (like, only one of the loader’s will be used, for both of the purposes you created them for). So just make sure each call to initLoader() provides a different integer ID as the first parameter.
  • The examples I found of how to use an AsyncTaskLoader did NOT include calling the forceLoad() method on the loader object returned by initLoader(). However, nothing worked until I called this.
  • As I’ll show in the next block of code, it’s critical that the Adapter method notifyDataSetChanged() is called when the data in the adapter is updated. Not calling this results in nothing changing in the UI.

The adapter:

public class BlogListAdapter extends BaseAdapter {
    private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("MM/dd");
    private static final int MAX_SUMMARY_LEN = 100;
    private LayoutInflater inflater;
    private List<BlogPost> blogPostRows = new ArrayList<BlogPost>();
 
    public BlogListAdapter(Context context, List<BlogPost> blogPostRows) {
        this.blogPostRows = blogPostRows;
        inflater = LayoutInflater.from(context);
    }
 
    public void setData(List<BlogPost> data) {
        if (blogPostRows != null) {
            blogPostRows.clear();
        } else {
            blogPostRows = new ArrayList<BlogPost>();
        }
        if (data != null) {
            blogPostRows.addAll(data);
        }
        notifyDataSetChanged();
    }
 
    @Override
    public View getView(int i, View view, ViewGroup parent) {
        BlogPost post = (BlogPost) getItem(i);
        if (view == null) {
            view = inflater.inflate(R.layout.blogpostdetail, null);
        }
        TextView blogDate = (TextView) view.findViewById(R.id.blogdate);
        blogDate.setText(dtf.print(post.date));
 
        TextView blogTitle = (TextView) view.findViewById(R.id.blogtitle);
        blogTitle.setText(post.title);
 
        TextView blogSummary = (TextView) view.findViewById(R.id.blogsummary);
        String summary = post.content.substring(0, Math.min(MAX_SUMMARY_LEN, post.content.length()));
        blogSummary.setText(summary);
 
        return view;
    }
 
    @Override
    public int getCount() {
        return blogPostRows.size();
    }
 
    @Override
    public Object getItem(int i) {
        return blogPostRows.get(i);
    }
 
    @Override
    public long getItemId(int i) {
        return i;
    }
}

Nothing particularly interesting here, other than the call to notifyDataSetChanged(). Nothing happens if this method is not called! Don’t forget it.

Finally, I’ll include the layout XML.

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="vertical"
              a:layout_width="fill_parent"
              a:layout_height="fill_parent">
 
    <ListView
        a:id="@+id/blogposts"
        a:paddingRight="0dp"
        a:layout_marginRight="0px"
        a:width="0px"
        a:layout_weight="2"
        a:layout_height="0dp"
        a:layout_width="match_parent"/>
</LinearLayout>

blogpostdetail.xml:

<?xml version="1.0" encoding="utf-8"?>
 
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="horizontal"
              a:layout_width="match_parent"
              a:layout_height="match_parent">
 
    <TextView
        a:id="@+id/blogdate"
        a:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"/>
 
    <TextView
        a:id="@+id/blogtitle"
        a:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"/>
 
    <TextView
        a:id="@+id/blogsummary"
        a:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"/>
 
</LinearLayout>