I’m going to extend the previous Blog Post List example to:
- Implement the new/bind patter and ViewHolder pattern by using a BindingAdapter
- Add a header row to our blog post list
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
- 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:
- Inflating the appropriate view, if necessary
- Using a ViewHolder for to avoid unnecessary calls to the costly findViewById
- 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:
- newView() for inflating the appropriate view
- buildViewHolder() for creating the ViewHolder
- bindView() for binding the model data to the view