Extending the BlogPost example with BindingAdapter

blog post

I’m going to extend the previous Blog Post List example to:

  1. Implement the new/bind patter and ViewHolder pattern by using a BindingAdapter
  2. Add a header row to our blog post list

I’m not actually going to explain the ViewHolder pattern other than to say that for performance reasons it’s common practice in rendering Android views. You can google it to find out more about it.
The BindingAdapter is a class originally developed by Jake Wharton to introduce the new/bind pattern, then later advanced by Patrick Hammond to include the ViewHolder pattern. I made a slight tweak to Patrick Hammond’s implementation to accommodate for more than one type of row (in this case, a header row and a detail row).

First, to add a header row, I need a way for the ListView’s adapter to distinguish a detail row from a header row. The header row only has static data defined in its XML layout; the adapter only needs to inflate the layout for a header row, whereas for a detail row, the adapter also needs to apply the model data to the row.Here is the header layout xml:




    

    

    


So in our list of blog post rows, we really just need a dummy placeholder entry for the header. I accomplish this by introducing a new interface:

public interface BlogPostRow {}

Then I make the BlogPost class implement BlogPostRow:

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;
  }
}

Finally in the activity class, I add a static function to prepend the list of rows returned by our BlogPostService with a single dummy row for the header:

public class ListBlogsActivity extends Activity {

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    final BlogListAdapter blogListAdapter = new BlogListAdapter(this, prependHeader(new ArrayList()));
    ListView blogPostListView = (ListView) findViewById(R.id.blogposts);

    blogPostListView.setAdapter(blogListAdapter);

    getLoaderManager().initLoader(0, savedInstanceState,
        new LoaderManager.LoaderCallbacks<List>() {
          @Override public Loader<List> onCreateLoader(int id, Bundle args) {
            return new BlogPostLoader(ListBlogsActivity.this);
          }

          @Override public void onLoadFinished(Loader<List> loader, List data) {

            blogListAdapter.setData(prependHeader(data));
          }

          @Override public void onLoaderReset(Loader<List> loader) {
            blogListAdapter.setData(prependHeader(new ArrayList()));
          }
        }
    ).forceLoad();
  }

  private static List prependHeader(List blogPosts) {
    List blogPostRows = new ArrayList();
    blogPostRows.add(new BlogPostRow() {});
    blogPostRows.addAll(blogPosts);
    return blogPostRows;
  }
}

Now my adapter has a way of distinguishing header vs detail rows: a detail row is an instance of BlogPost, and a header row is not.

Finally, the adapter:

public class BlogListAdapter exte
public class BlogListAdapter extends BindingAdapter<BlogPostRow, BlogListAdapter.DetailViewHolder> {
  private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("MM/dd");
  private static final int MAX_SUMMARY_LEN = 100;
  private List blogPostRows = new ArrayList();

  public BlogListAdapter(Context context, List blogPostRows) {
    super(context);
    this.blogPostRows = blogPostRows;
  }

  public void setData(List data) {
    if (blogPostRows != null) {
      blogPostRows.clear();
    } else {
      blogPostRows = new ArrayList();
    }
    if (data != null) {
      blogPostRows.addAll(data);
    }
    notifyDataSetChanged();
  }

  @Override public View newView(LayoutInflater inflater, int position, ViewGroup container) {
    return (getItem(position) instanceof BlogPost) ?
        inflater.inflate(R.layout.blogpostdetail, null) : inflater.inflate(R.layout.blogpostheader, null);
  }

  @Override public DetailViewHolder buildViewHolder(View view, int position) {
    return (getItem(position) instanceof BlogPost) ? new DetailViewHolder(view) : null;
  }

  @Override public void bindView(BlogPostRow item, int position, View view, DetailViewHolder vh) {
    if (item instanceof BlogPost) {
      BlogPost post = (BlogPost) item;
      vh.blogDate.setText(dtf.print(post.date));

      vh.blogTitle.setText(post.title);

      String summary = post.content.substring(0, Math.min(MAX_SUMMARY_LEN, post.content.length()));
      vh.blogSummary.setText(summary);
    }
  }

  @Override public int getItemViewType(int position) {
    return (getItem(position) instanceof BlogPost) ? ViewType.DETAIL_ITEM.ordinal() : ViewType.HEADER_ITEM.ordinal();
  }

  @Override public int getViewTypeCount() {
    return ViewType.values().length;
  }

  @Override
  public int getCount() {
    return blogPostRows.size();
  }

  @Override
  public BlogPostRow getItem(int i) {
    return blogPostRows.get(i);
  }

  @Override
  public long getItemId(int i) {
    return i;
  }

  public enum ViewType {
    DETAIL_ITEM, HEADER_ITEM
  }

  static class DetailViewHolder {
    TextView blogDate;
    TextView blogTitle;
    TextView blogSummary;

    DetailViewHolder(View view) {
      blogDate = (TextView) view.findViewById(R.id.blogdate);
      blogTitle = (TextView) view.findViewById(R.id.blogtitle);
      blogSummary = (TextView) view.findViewById(R.id.blogsummary);
    }
  }
}

Enabling a Header Row

We add support for a header row to the adapter by adding:
  • The ViewType enum, which defines the types of views a row can be
  • getViewTypeCount(), which provides the number of possible view types for a row
  • getItemViewType(), which defines what type of view a particular row is

Extending BindingAdapter

Next, we extend the BindingAdapter to introduce the new/bind and ViewHolder patterns. The typical adapter implements a getView() method, which handles the following purposes for each row:

  1. Inflating the appropriate view, if necessary
  2. Using a ViewHolder for to avoid unnecessary calls to the costly findViewById
  3. Binding the data to the view

This is bothersome from the standpoint that it violates the single responsibility principal. Extending the BindingAdapter breaks the getView() method into three separate methods:

  1. newView() for inflating the appropriate view
  2. buildViewHolder() for creating the ViewHolder
  3. bindView() for binding the model data to the view
In our case, because we have two types of rows, each of these methods will do slightly different things depending on the row type. For header rows, only the newView() actually does anything. Since the model has no data for header rows, there is no reason for a ViewHolder (so we return null), and also nothing to do in binding the model to the view.