Thursday, October 24, 2013

Blog List: Adding Fragments and List Sorting

So far, the blog list example has assumed a single layout for all devices and orientations. Of course, we know that some phones are much larger than others, and tablets are larger than phones, and landscape vs portait orientations provide for very different amounts of screen real estate to work with. Having a single layout accounts for none of this, and inevitably forces the design to the lowest common denominator (a small phone in portrait orientation).

Providing different layouts means having reusable UI components becomes very important. In Android, these are called fragments. The Blog List app follows the typical example for which fragments are useful: a list of items that provide additional detail about a selected item.

In the Blog List app, so far we have just had two separate activities (and thus two separate screens): the blog list, and the blog detail view. On a large tablet in landscape mode, a better design would be to split the screen in half and display the list on the left, and the details for the selected item on the right.

NOTE: the Blog List app is now on github, so you can download the source and see how it evolves as I learn android.

NOTE 2: I primarily used two tutorials in developing/learning fragments. They are:
http://developer.android.com/guide/components/fragments.html
http://www.vogella.com/articles/AndroidFragments/article.html

In addition to introducing fragments in this version of the Blog List, I'm going to make the list sortable by each of the column headings. This required me to redo the way I was providing the header row in a way that I think it a lot simpler and less hacky.

Let's start by looking at the XML in the res folder:

I've added a new layout file for the detail fragment at layout/detailviewfrag.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="vertical"
              a:layout_width="match_parent"
              a:layout_height="match_parent">


    <LinearLayout
        a:orientation="horizontal"
        a:layout_height="wrap_content"
        a:layout_width="match_parent">

        <TextView
            a:id="@+id/blogviewdate"
            a:textSize="11sp"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"/>

        <EditText
            a:id="@+id/blogviewtitle"
            a:textSize="11sp"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"
            a:inputType="text"
            a:background="#FFFFFF"
            a:textColor="#222222"
            a:textAlignment="center"
            a:gravity="center"
            a:selectAllOnFocus="true"/>
    </LinearLayout>

    <EditText
        a:id="@+id/blogviewcontent"
        a:textSize="11sp"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:background="#FFFFFF"
        a:textColor="#222222"
        a:layout_margin="2dp"
        a:inputType="textMultiLine"
        a:scrollbars="vertical"
        a:lines="8"
        a:maxLines="200"
        a:gravity="top"
        a:selectAllOnFocus="true"/>

    <LinearLayout
        a:orientation="horizontal"
        a:layout_height="wrap_content"
        a:layout_width="wrap_content">

        <Button
            a:id="@+id/saveButton"
            a:layout_height="wrap_content"
            a:layout_width="wrap_content"
            a:text="@string/save"/>
    </LinearLayout>

</LinearLayout>


But...in portrait mode, we want to only display the list and display an item without the list when an item is clicked - no split screen. Given the blogviewfrag.xml, how could we get back to the list without a back button?

Android accounts for this with a directory structure in the res directory that essentially prioritizes what resource file gets loaded. The layout folder is actually the fallback default folder for layout resources. We can override that for specific situations. In our case, we want an override for when in portrait mode so the detail view can have a back button:

layout-port/blogviewfrag.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="vertical"
              a:layout_width="match_parent"
              a:layout_height="match_parent">


    <LinearLayout
        a:orientation="horizontal"
        a:layout_height="wrap_content"
        a:layout_width="match_parent">

        <TextView
            a:id="@+id/blogviewdate"
            a:textSize="11sp"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"/>

        <EditText
            a:id="@+id/blogviewtitle"
            a:textSize="11sp"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"
            a:inputType="text"
            a:background="#FFFFFF"
            a:textColor="#222222"
            a:textAlignment="center"
            a:gravity="center"
            a:selectAllOnFocus="true"/>
    </LinearLayout>

    <EditText
        a:id="@+id/blogviewcontent"
        a:textSize="11sp"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:background="#FFFFFF"
        a:textColor="#222222"
        a:layout_margin="2dp"
        a:inputType="textMultiLine"
        a:scrollbars="vertical"
        a:lines="8"
        a:maxLines="200"
        a:gravity="top"
        a:selectAllOnFocus="true"/>

    <LinearLayout
        a:orientation="horizontal"
        a:layout_height="wrap_content"
        a:layout_width="wrap_content">

        <Button
            a:id="@+id/backButton"
            a:layout_height="wrap_content"
            a:layout_width="wrap_content"
            a:text="@string/back"/>

        <Button
            a:id="@+id/saveButton"
            a:layout_height="wrap_content"
            a:layout_width="wrap_content"
            a:text="@string/save"/>
    </LinearLayout>

</LinearLayout>

As you can see, the only difference between this blogviewfrag.xml and the one in the layout directory is the inclusion of the back button.

layout/blogview.xml just includes the blog view fragment:

<?xml version="1.0" encoding="utf-8"?>

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

    <fragment
        android:id="@+id/blogDetailFrag"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        class="org.kevinmrohr.android_blog.fragments.BlogDetailFragment"/>
</LinearLayout>

The BlogViewActivity class now barely does anything (it is not even used in landscape oriention). It just passes the BlogPost passed into it via an Intent to the detail fragment:

public class BlogViewActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.blogview);

    BlogPost blogPost = (BlogPost) getIntent().getSerializableExtra("blogPost");

    BlogDetailFragment detailFragment = (BlogDetailFragment) getFragmentManager().findFragmentById(R.id.blogDetailFrag);
    if (detailFragment != null && detailFragment.isInLayout()) {
      detailFragment.update(blogPost);
    } else {
      throw new RuntimeException("Something is very wrong. How can the detail activity not have the detail fragment!?!? Busted!");
    }
  }
}

Now the BlogDetailFragment handles all the work the BlogViewActivity used to handle:

public class BlogDetailFragment extends Fragment {
  public static final DateTimeFormatter dtf = DateTimeFormat.forPattern("MM/dd/yyyy hh:mm:ss");
  @InjectView(R.id.blogviewtitle) EditText title;
  @InjectView(R.id.blogviewdate) TextView date;
  @InjectView(R.id.blogviewcontent) EditText content;

  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.blogviewfrag, container, false);

    Views.inject(this, view);

    return view;
  }

  public void update(BlogPost blogPost) {
    title.setText(blogPost.content);
    date.setText(dtf.print(blogPost.date));
    content.setText(blogPost.content);
  }

  @OnClick(R.id.saveButton)
  public void onSaveClick(View view) {
    Log.d("onSaveClick", "Saved blog!");
  }

  @Optional
  @OnClick(R.id.backButton)
  public void onBackClick(View view) {
    Intent i = new Intent(view.getContext(), ListBlogsActivity.class);
    startActivity(i);
    getActivity().finish();
  }
}

Note the use of the @Optional annotation  above the @OnClick annotation for the onBackClick. This tells Butterknife that we don't know if this button will exist or not (because in landscape mode, it won't; everything will be presented on one screen with nothing to go back to). Without the @Optional tag, this would throw an error in landscape orientation, which might look something like this:

Caused by: butterknife.Views$UnableToInjectException: Unable to inject views for BlogDetailFragment{a50e8f08 #1 id=0x7f050008}


Now for the listview. For landscape mode, the list and detail views are now combined by including the list and detail fragments.

layout/main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="horizontal"
              a:layout_width="fill_parent"
              a:layout_height="fill_parent"
              a:baselineAligned="false">

    <fragment
        a:id="@+id/blogListFrag"
        a:layout_height="match_parent"
        a:layout_width="0dp"
        a:width="0dp"
        a:layout_weight="1"
        class="org.kevinmrohr.android_blog.fragments.BlogListFrag"/>

    <fragment
        a:id="@+id/blogDetailFrag"
        a:layout_height="match_parent"
        a:layout_width="0dp"
        a:width="0dp"
        a:layout_weight="1"
        class="org.kevinmrohr.android_blog.fragments.BlogDetailFragment"/>
</LinearLayout>

The list view for portrait only includes the list fragment.

layout-port/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">

    <fragment
        a:id="@+id/blogListFrag"
        a:layout_height="match_parent"
        a:layout_width="match_parent"
        class="org.kevinmrohr.android_blog.fragments.BlogListFrag"/>
</LinearLayout>

And the layout for the list view:

layout/bloglistfrag.xml:
<?xml version="1.0" encoding="utf-8"?>

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

    <LinearLayout
        a:id="@+id/listheader"
        a:orientation="horizontal"
        a:layout_width="match_parent"
        a:layout_height="wrap_content">

        <TextView
            a:id="@+id/dateHeading"
            a:textSize="11sp"
            a:width="0px"
            a:layout_weight="3"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"
            a:textStyle="bold"
            a:text="@string/date"/>

        <TextView
            a:id="@+id/titleHeading"
            a:textSize="11sp"
            a:width="0px"
            a:layout_weight="3"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"
            a:textStyle="bold"
            a:text="@string/title"/>

        <TextView
            a:id="@+id/summaryHeading"
            a:textSize="11sp"
            a:width="0px"
            a:layout_weight="3"
            a:layout_width="wrap_content"
            a:layout_height="wrap_content"
            a:layout_margin="2dp"
            a:textStyle="bold"
            a:text="@string/summary"/>

    </LinearLayout>

    <ListView
        a:layout_below="@id/listheader"
        a:id="@+id/blogposts"
        a:layout_height="wrap_content"
        a:layout_width="match_parent"/>
</RelativeLayout>

Note that the header row no longer is a part of the list view, and instead is it's own static thing external to the list view. I had to do this for sorting; a list view only gives you what row you clicked, not which column. A positive side effect of this change is that the BlogPostRow interface is no longer needed, and our List blogPosts no longer have to have dummy header row data in them. Much preferred.

In landscape mode, our list fragment somehow has to update our detail fragment. To do this, the BlogListActivity becomes a intermediary between the two fragments.

However, the list fragment should not be aware of what activity is including it. So how can it talk to it's parent activity in a custom way without know what kind of activity it is? The list fragment defines and interface that any Activity that wants to include it must implement:

public class BlogListFrag extends Fragment {
  @Inject ObjectMapper mapper;
  @InjectView(R.id.blogposts) ListView blogPostsListView;

  public static final String SORT_COL_KEY = "sortCol";
  public static final String SORT_ASC_KEY = "sortAsc";
  public static final String SELECTED_IDX_KEY = "selectedBlogIndex";
  private RequestQueue mRequestQueue;
  private BlogClickListener listener;

  private static final Map<SortColumn, Comparator<BlogPost>> blogSorters = new HashMap<SortColumn, Comparator<BlogPost>>();
  private boolean sortAsc = true;
  private SortColumn sortCol = DATE;

  private int selectedBlogIndex = 0;
  private List<BlogPost> blogPosts;
  private BlogListAdapter blogListAdapter;

  static {
    blogSorters.put(DATE, new DateComparator());
    blogSorters.put(SUMMARY, new ContentComparator());
    blogSorters.put(TITLE, new TitleComparator());
  }

  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.bloglistfrag, container, false);

    ObjectGraph.create(new ObjectMappingModule()).inject(this);
    Views.inject(this, view);

    if (savedInstanceState != null && savedInstanceState.get(SORT_COL_KEY) != null) {
      sortCol = (SortColumn) savedInstanceState.getSerializable(SORT_COL_KEY);
    }
    if (savedInstanceState != null && savedInstanceState.get(SORT_ASC_KEY) != null) {
      sortAsc = savedInstanceState.getBoolean(SORT_ASC_KEY);
    }
    if (savedInstanceState != null && savedInstanceState.get(SELECTED_IDX_KEY) != null) {
      selectedBlogIndex = savedInstanceState.getInt(SELECTED_IDX_KEY);
    }

    //Initiate the Volley request queue
    mRequestQueue = Volley.newRequestQueue(getActivity());

    blogListAdapter = new BlogListAdapter(this.getActivity(), new ArrayList<BlogPost>());
    blogPostsListView.setAdapter(blogListAdapter);

    initBlogListClickListener();
    getBlogPosts();
    return view;
  }

  @Override public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (activity instanceof BlogClickListener) {
      listener = (BlogClickListener) activity;
    } else {
      throw new RuntimeException("activity must be a BlogClickListener!");
    }
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable(SORT_COL_KEY, sortCol);
    outState.putBoolean(SORT_ASC_KEY, sortAsc);
    outState.putInt(SELECTED_IDX_KEY, selectedBlogIndex);
  }

  @Override public void onStop() {
    super.onStop();
    mRequestQueue.cancelAll(this);
  }

  @OnClick(R.id.dateHeading)
  public void sortByDate(View view) {
    sortBlogs(DATE);
  }

  @OnClick(R.id.titleHeading)
  public void sortByTitle(View view) {
    sortBlogs(TITLE);
  }

  @OnClick(R.id.summaryHeading)
  public void sortBySummary() {
    sortBlogs(SUMMARY);
  }

  private void getBlogPosts() {
    mRequestQueue.add(new JsonArrayRequest(getString(R.string.rest_base_url) + "/BlogPosts.json",
        new Response.Listener<JSONArray>() {
          @Override public void onResponse(JSONArray response) {
            try {
              blogPosts = mapper.readValue(response.toString(), new TypeReference<List<BlogPost>>() {});

              sort(blogPosts, sortAsc ? blogSorters.get(sortCol) : reverseOrder(blogSorters.get(sortCol)));
              blogListAdapter.setData(blogPosts);
              if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
                blogPostsListView.setSelection(selectedBlogIndex);
                listener.onClick(blogPosts.get(selectedBlogIndex));
              }
            } catch (Exception e) {
              throw new RuntimeException("Failed!", e);
            }
          }
        },
        new Response.ErrorListener() {
          @Override public void onErrorResponse(VolleyError error) {
            Log.e("ListBlogsActivity.onCreate()", "Volley failed to get BlogPosts! " + error.toString());
          }
        }
    ));
  }

  private void initBlogListClickListener() {
    blogPostsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        //Position 0 is the header. Don't do anything if that is clicked.
        Log.d("ItemClickListener", view.toString());

        //subtract 1 because the header is the first one, which is not accounted for in the blogPosts list.
        BlogPost bp = blogPosts.get(position);

        listener.onClick(bp);
      }
    });
  }

  public interface BlogClickListener {
    void onClick(BlogPost blogPost);
  }

  private void sortBlogs(SortColumn selectedColumn) {
    if (sortCol == selectedColumn) {
      //reverse the sort
      sortAsc = !sortAsc;
    } else {
      sortCol = selectedColumn;
      sortAsc = true;
    }
    sort(blogPosts, sortAsc ? blogSorters.get(sortCol) : reverseOrder(blogSorters.get(sortCol)));
    blogListAdapter.setData(blogPosts);
  }

  public static enum SortColumn {
    DATE, TITLE, SUMMARY
  }

  static class DateComparator implements Comparator<BlogPost> {
    @Override public int compare(BlogPost lhs, BlogPost rhs) {
      return lhs.date.compareTo(rhs.date);
    }
  }

  static class TitleComparator implements Comparator<BlogPost> {
    @Override public int compare(BlogPost lhs, BlogPost rhs) {
      return lhs.title.compareTo(rhs.title);
    }
  }

  static class ContentComparator implements Comparator<BlogPost> {
    @Override public int compare(BlogPost lhs, BlogPost rhs) {
      return lhs.content.compareTo(rhs.content);
    }
  }
}

The BlogClickListener is the interface that gets called when a blog is clicked in the list. The ListBlogsActivity must now implement this interface:

public class ListBlogsActivity extends Activity implements BlogListFrag.BlogClickListener {

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

  @Override public void onClick(BlogPost blogPost) {
    BlogDetailFragment detailFrag = (BlogDetailFragment) getFragmentManager().findFragmentById(R.id.blogDetailFrag);
    if (detailFrag != null && detailFrag.isInLayout()) {
      detailFrag.update(blogPost);
    } else {
      Intent i = new Intent(this, BlogViewActivity.class);
      i.putExtra("blogPost", blogPost);
      startActivity(i);
      finish();
    }
  }
}

Most of the functionality of this class has moved to the BlogListFrag class, and it is now just serving as an intermediary between the list fragment and the detail fragment. Note that if it cannot find the detail fragment when a blog entry is clicked, it starts the BlogViewActivity. If it can find the detail fragment, it just updates it with the selected blog entry.

The most interesting thing about the sorting implementation was that I removed the dummy header rows from the blog post list and separated it from the ListView. This simplified the adapter for the list view quite a bit.

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

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

  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 newView(LayoutInflater inflater, int position, ViewGroup container) {
    return inflater.inflate(R.layout.blogpostdetail, null);
  }

  @Override public DetailViewHolder buildViewHolder(View view, int position) {
    return new DetailViewHolder(view);
  }

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

    vh.blogTitle.setText(item.title);

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

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

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

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

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

Tuesday, October 22, 2013

Blog List: Adding a Blog View Activity, Butterknife, and Dagger

Today I'll be modifying the Blog List example to add a blog view activity, and utilizing Dagger for dependency injection and Butterknife for view injection.

Also, if you've been following along with my Blog List series, you'll noticed the code has gone through a bit of refactoring in this iteration. Specifically, I've gotten rid of the VolleyBlogPostRequest and BlogPostListener in favor of anonymous inner classes. This allowed me to keep the list of blog posts available for the ListView's onItemClickedListener.

So, being introduced today:
  1. Navigating from one activity to another
  2. Butterknife
  3. Dagger

A few notes on Butterknife and Dagger

Dagger and Butterknife are frameworks built by the guys at square.

Dagger is a dependency injection framework specifically targeted at Android applications and the limitations of mobile devices. It does more at compile time than most DI frameworks, and less at run-time. It is not as full-featured as Spring or Guice, but instead focuses on speed. For more information, here is a nice dagger presentation.

Butterknife is a view injection framework, built around the same concepts as Dagger.

VS Roboguice

Coming from a JEE/Spring background, I wanted to do dependency injection right away with my Android development. I first discovered Roboguice and went down that path. However, the Roboguice 2.0 documentation was/is very incomplete. I was able to get things working with Roboguice, but in general felt like it was more complicated than it should have been. Additionally, it does not do as much at compile time as the Square frameworks, and thus incurs a larger run-time cost.

Additional pom.xml dependencies

Just three new maven dependencies for butterknife and dagger:
<dependency>
    <groupId>com.jakewharton</groupId>
    <artifactId>butterknife</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.dagger</groupId>
    <artifactId>dagger</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>com.squareup.dagger</groupId>
    <artifactId>dagger-compiler</artifactId>
    <version>1.1.0</version>
    <optional>true</optional>
</dependency>

The Blog View Activity

I'll start by showing the new Blog View Activity. It's not very practical, but it accomplishes what I'm trying to demonstrate.

blogview.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
              a:orientation="vertical"
              a:layout_width="match_parent"
              a:layout_height="match_parent">


    <LinearLayout a:orientation="horizontal" a:layout_height="wrap_content" a:layout_width="match_parent">

        <TextView a:id="@+id/blogviewdate" a:textSize="11sp" a:layout_width="wrap_content" 
                  a:layout_height="wrap_content" a:layout_margin="2dp"/>

        <EditText a:id="@+id/blogviewtitle" a:textSize="11sp" a:layout_width="wrap_content" 
                  a:layout_height="wrap_content" a:layout_margin="2dp" a:inputType="text" a:background="#FFFFFF"
                  a:textColor="#222222" a:textAlignment="center" a:gravity="center" a:selectAllOnFocus="true"/>
    </LinearLayout>

    <EditText a:id="@+id/blogviewcontent" a:textSize="11sp" a:layout_width="wrap_content" a:layout_height="wrap_content"
        a:background="#FFFFFF" a:textColor="#222222" a:layout_margin="2dp" a:inputType="textMultiLine" 
        a:scrollbars="vertical" a:lines="8" a:maxLines="200" a:gravity="top" a:selectAllOnFocus="true"/>

    <LinearLayout a:orientation="horizontal" a:layout_height="wrap_content" a:layout_width="wrap_content">
        <Button a:id="@+id/backButton" a:layout_height="wrap_content" a:layout_width="wrap_content" a:text="@string/back"/>
        <Button a:id="@+id/saveButton" a:layout_height="wrap_content" a:layout_width="wrap_content" a:text="@string/save"/>
    </LinearLayout>

</LinearLayout>

BlogViewActivity.java:
package org.kevinmrohr.android_blog.activity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import butterknife.InjectView;
import butterknife.OnClick;
import butterknife.Views;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.kevinmrohr.android_blog.R;
import org.kevinmrohr.android_blog.model.BlogPost;

public class BlogViewActivity extends Activity {
  public static final DateTimeFormatter dtf = DateTimeFormat.forPattern("MM/dd/yyyy hh:mm:ss");
  @InjectView(R.id.blogviewdate) TextView date;
  @InjectView(R.id.blogviewtitle) EditText title;
  @InjectView(R.id.blogviewcontent) EditText content;

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

    Views.inject(this);

    BlogPost blogPost = (BlogPost) getIntent().getSerializableExtra("blogPost");

    date.setText(dtf.print(blogPost.date));
    title.setText(blogPost.title);
    content.setText(blogPost.content);
  }

  @OnClick(R.id.saveButton)
  public void onSaveClick(View view) {
    Log.d("onSaveClick", "Saved blog!");
  }

  @OnClick(R.id.backButton)
  public void onBackClick(View view) {
    Intent i = new Intent(view.getContext(), ListBlogsActivity.class);
    startActivity(i);
    finish();
  }
}

This activity demonstrates a few of Butterknife's features. At compile time, dagger-compiler enhances the BlogViewActivity class by converting the @InjectView annotations into findViewById() calls. These findViewById() calls will get called when Views.inject(this) gets called in onCreate. However, Butterknife stores the results of these calls in a Map, so if you rotate your phone and onCreate() gets called again, the findViewById() calls do not need to be called again from the onCreate(). Because findViewById() is a costly method, this can represent a significant performance boost on complex UIs.

Also notice the @OnClick annotation, which attaches the annotated method to the view with the ID passed into the annotation. I personally find this a lot more aesthetically pleasing/less cluttered than setting an onItemClickHandler in java code.

The final thing to note is that a BlogPost is expected to be passed in via an Intent (Note: since the last blog post, I've updated the BlogPostRow to extend Serializable, allowing BlogPost objects to be passed on Intents).

Don't forget to add the new activity to the AndroidManifest.xml:

        <activity
            android:name=".activity.BlogViewActivity"
            android:label="@string/app_name">
        </activity>

Before we go back to the ListBlogsActivity to see how it makes passes the selected BlogPost to the BlogViewActivity via an Intent, let's take a look at the Dagger Module I created so I could use Dagger to inject my Jackson ObjectMapper dependency in the ListBlogsActivity:

package org.kevinmrohr.android_blog.module;

import dagger.Module;
import dagger.Provides;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.joda.time.DateTime;
import org.kevinmrohr.android_blog.activity.ListBlogsActivity;
import org.kevinmrohr.android_blog.serialization.CustomDateDeserializer;
import org.kevinmrohr.android_blog.serialization.CustomDateSerializer;

@Module(injects = ListBlogsActivity.class)
public class ListBlogsModule {
  @Provides ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule("JSONModule", new Version(2, 0, 0, null));
    module.addSerializer(DateTime.class, new CustomDateSerializer());
    module.addDeserializer(DateTime.class, new CustomDateDeserializer());
    mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.registerModule(module);
    return mapper;
  }
}

For those of you coming from a Spring background, this is basically analogous to an @Configuration class, except that it only works for the classes listed in the comma-separated injects list. An @Module class is required for another class to do dependency injection with Dagger, even if it doesn't define any @Provides methods (and this can be done: classes with @Injects annotated constructors can be injected into other Dagger utilizing classes).

So, all I'm really doing is hiding the boilerplate code to initialize a Jackson object mapper.

Now for the updated ListBlogsActivity:

package org.kevinmrohr.android_blog.activity;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import butterknife.InjectView;
import butterknife.Views;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.Volley;
import dagger.ObjectGraph;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.json.JSONArray;
import org.kevinmrohr.android_blog.R;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.module.ListBlogsModule;
import org.kevinmrohr.android_blog.util.BlogListUtil;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;

import static org.kevinmrohr.android_blog.util.BlogListUtil.prependHeader;

public class ListBlogsActivity extends Activity {
  @Inject ObjectMapper mapper;
  @InjectView(R.id.blogposts) ListView blogPostsListView;

  private RequestQueue mRequestQueue;
  private List<BlogPost> blogPosts;

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

    //Put Dagger and Butterknife into action
    ObjectGraph.create(new ListBlogsModule()).inject(this);  //Dagger
    Views.inject(this);                                      //Butterknife

    //Initiate the Volley request queue
    mRequestQueue = Volley.newRequestQueue(this);

    BlogListAdapter blogListAdapter = new BlogListAdapter(this, prependHeader(new ArrayList<BlogPost>()));
    blogPostsListView.setAdapter(blogListAdapter);

    getBlogPosts(blogListAdapter);

    initBlogListClickListener();
  }

  private void getBlogPosts(final BlogListAdapter blogListAdapter) {
    mRequestQueue.add(new JsonArrayRequest(getString(R.string.rest_base_url) + "/BlogPosts.json",
        new Response.Listener<JSONArray>() {
          @Override public void onResponse(JSONArray response) {
            try {
              blogPosts = mapper.readValue(response.toString(), new TypeReference<List<BlogPost>>() {});
              blogListAdapter.setData(BlogListUtil.prependHeader(blogPosts));
            } catch (Exception e) {
              throw new RuntimeException("Failed!", e);
            }
          }
        },
        new Response.ErrorListener() {
          @Override public void onErrorResponse(VolleyError error) {
            Log.e("ListBlogsActivity.onCreate()", "Volley failed to get BlogPosts! " + error.toString());
          }
        }
    ));
  }

  private void initBlogListClickListener() {
    blogPostsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        //Position 0 is the header. Don't do anything if that is clicked.
        if (position > 0) {
          Log.d("ItemClickListener", view.toString());

          //subtract 1 because the header is the first one, which is not accounted for in the blogPosts list.
          BlogPost bp = blogPosts.get(position - 1);
          Intent i = new Intent(view.getContext(), BlogViewActivity.class);
          //Add the selected blog post to the intent
          i.putExtra("blogPost", bp);

          startActivity(i);
          finish();
        }
      }
    });
  }

  @Override protected void onStop() {
    super.onStop();
    mRequestQueue.cancelAll(this);
  }
}

You can see that I'm injecting the blogposts ListView object with Butterknife, but I'm also injecting the Jackson ObjectMapper with Dagger. Just like Butterknife needs the Views.inject(this) to kick of its action, Dagger needs the ObjectGraph.create().inject(this) to create its object graph.

Note, Butterknife is not extensively featured. So, while there is an @OnClick annotation, there is no annotation for an OnItemClickListener. We're still left to do this the old manual Java way.

An OnItemClickListener is a way for you to respond to click events on a ListView. In this case, we want to transition to the BlogViewActivity. This is done by creating a new intent with the Class of the activity we want to go to:

          Intent i = new Intent(view.getContext(), BlogViewActivity.class);

We also want to pass the BlogPost object for the row that was clicked in the list of blog posts. OnItemClickedListener passes us the position of the row that was clicked. Since we have the blogPosts stored as a class level instance variable, we can easily access the selected blog post and pass it into the Intent as a Serializable. Finally, we call startActivity with the Intent we just created to signal the start of the BlogViewActivity, and we call finish() to signal the end of the ListBlogsActivity.

Monday, October 21, 2013

Blog List Example with Volley

In my previous iterations of the of the Blog List example, the BlogListService just returned a hardcoded list of BlogPost objects. In this post, we will actually retrieve the list of BlogPost objects from a restful JSON service.

Ideally, you can eliminate much of the HTTP traffic by using some sort of a client side caching layer. This is where Volley comes in.

So, in this post I'll show two things:

  1. One of the many ways to consume JSON and convert it into java objects
  2. The use of Volley as an asynchronous network thread management and caching solution (replacing the AsyncTaskLoader in the prior examples).
First, the "data service" layer I'm using for this simple example is just an nginx server serving a static JSON file. The static JSON file looks like this:

[
  {"date":"10/20/2013", "title":"Android with Maven", "content":"Android with Maven\n      Ever since making the switch from Ant to Maven 2 back in 2005, I've never looked back. So one of the first things I wanted to know was if I could use maven to build my Android apps. And of course, you can. Here's an example pom.xml file:"},
  {"date":"10/21/2013", "title":"AsyncTaskLoader", "content":"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).\n\n     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.\n\n          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."}
]

I'm going to use Jackson to map the json to java objects. I could have used gson or probably 10 other json libraries to do this, but I have had good experiences with Jackson and already had a working example in another project. I'm using maven to build my project, so here are the jackson (and joda) dependencies:
    
    <properties>
        <jackson.version>1.9.13</jackson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-core-asl</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-jaxrs</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-mapper-asl</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-mrbean</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>org.codehaus.jackson</groupid>
            <artifactid>jackson-xc</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>com.fasterxml.jackson.datatype</groupid>
            <artifactid>jackson-datatype-joda</artifactid>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupid>joda-time</groupid>
            <artifactid>joda-time</artifactid>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupid>org.joda</groupid>
            <artifactid>joda-convert</artifactid>
            <version>1.5</version>
        </dependency>
    </dependencies>

As for the Volley dependency...well I cheated for this example. To my knowledge, there is no public maven repository housing the Volley artifact at this point (which could be interpreted as Volley is not ready for primetime, or it could be interpreted as the android/volley team has little interest in maven). Anyway, I just pulled the source down with git:

git clone https://android.googlesource.com/platform/frameworks/volley

and copied it into my project. If I was going to use maven in a production application, I would have an artifact repository, and I would have built and deployed a volley jar artifact to it. But for the purposes of blogging an example, this is fine. 

For this example, I'm going to start with the Activity and work my way back to the rest request.


package org.kevinmrohr.android_blog;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.async.VolleyBlogPostRequest;
import org.kevinmrohr.android_blog.model.BlogPost;

import java.util.ArrayList;

import static org.kevinmrohr.android_blog.util.BlogListUtil.prependHeader;

public class ListBlogsActivity extends Activity {
  private RequestQueue mRequestQueue;

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

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

    mRequestQueue = Volley.newRequestQueue(this);
    mRequestQueue.add(new VolleyBlogPostRequest(this, blogListAdapter));
  }


  @Override protected void onStop() {
    super.onStop();
    mRequestQueue.cancelAll(this);
  }
}

Volley requests are placed on a Volley RequestQueue, which under the covers manages a thread pool (which defaults to having 4 threads). There are a lot of things you can configure (number of threads in the thread pool, the underlying HTTP requester, request priority, etc.) but for this example I'm just going to use the defaults.

Note that in onStop() I'm cancelling all requests in the RequestQueue, preventing wasted time and cycles dealing with responses that are going to be ignored.

My JSON describes an array of objects, so my VolleyBlogPostRequest extends JsonArrayRequest:

package org.kevinmrohr.android_blog.async;

import android.content.Context;
import android.util.Log;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import org.kevinmrohr.android_blog.R;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.service.BlogPostListener;

public class VolleyBlogPostRequest extends JsonArrayRequest {
  public VolleyBlogPostRequest(Context context, BlogListAdapter blogListAdapter) {
    super(
        context.getString(R.string.rest_base_url) + "/BlogPosts.json",
        new BlogPostListener(blogListAdapter),
        new Response.ErrorListener() {
          @Override public void onErrorResponse(VolleyError error) {
            Log.e("ListBlogsActivity.onCreate()", "Volley failed to get BlogPosts! " + error.toString());
          }
        }
    );
  }
}

Nothing too exciting here. Basically I just created this class to keep a lot of the fluff out of the Activity. The interesting things are happening in the BlogPostListener:

package org.kevinmrohr.android_blog.service;

import com.android.volley.Response;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.type.TypeReference;
import org.joda.time.DateTime;
import org.json.JSONArray;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.serialization.CustomDateDeserializer;
import org.kevinmrohr.android_blog.serialization.CustomDateSerializer;
import org.kevinmrohr.android_blog.util.BlogListUtil;

import java.util.List;

public class BlogPostListener implements Response.Listener<JSONArray> {
  private BlogListAdapter blogListAdapter;
  private ObjectMapper mapper = new ObjectMapper();

  public BlogPostListener(BlogListAdapter blogListAdapter) {
    this.blogListAdapter = blogListAdapter;
    SimpleModule module = new SimpleModule("JSONModule", new Version(2, 0, 0, null));
    module.addSerializer(DateTime.class, new CustomDateSerializer());
    module.addDeserializer(DateTime.class, new CustomDateDeserializer());
    mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.registerModule(module);
  }

  @Override public void onResponse(JSONArray response) {
    try {
      List<BlogPost> blogPosts = mapper.readValue(response.toString(), new TypeReference<List<BlogPost>>() {});
      blogListAdapter.setData(BlogListUtil.prependHeader(blogPosts));
    } catch (Exception e) {
      throw new RuntimeException("Failed!", e);
    }
  }
}

The BlogPostListener's onResponse is called when the response comes back from the endpoint we provided to the JsonArrayRequest constructor. As you can see, using Jackson to map a JSON string to java objects is pretty simple. Also, I've moved the prependHeader method out of the activity an into a static utility class (where it really belongs anyway).

Extending the BlogPost example with BindingAdapter

Today 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:

<?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:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"
        a:textStyle="bold"
        a:text="@string/date"/>

    <TextView
        a:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"
        a:textStyle="bold"
        a:text="@string/title"/>

    <TextView
        a:textSize="11sp"
        a:width="0px"
        a:layout_weight="3"
        a:layout_width="wrap_content"
        a:layout_height="wrap_content"
        a:layout_margin="2dp"
        a:textStyle="bold"
        a:text="@string/summary"/>

</LinearLayout>

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:

package org.kevinmrohr.android_blog.model;
public interface BlogPostRow {}

Then I make the BlogPost class implement BlogPostRow:

package org.kevinmrohr.android_blog.model;

import org.joda.time.DateTime;

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:
package org.kevinmrohr.android_blog;

import android.app.Activity;
import android.app.LoaderManager;
import android.content.Loader;
import android.os.Bundle;
import android.widget.ListView;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.async.BlogPostLoader;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.model.BlogPostRow;

import java.util.ArrayList;
import java.util.List;

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<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(prependHeader(data));
          }

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

  private static List<BlogPostRow> prependHeader(List<BlogPost> blogPosts) {
    List<BlogPostRow> blogPostRows = new ArrayList<BlogPostRow>();
    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:
package org.kevinmrohr.android_blog.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.kevinmrohr.android_blog.R;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.model.BlogPostRow;

import java.util.ArrayList;
import java.util.List;

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<BlogPostRow> blogPostRows = new ArrayList<BlogPostRow>();

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

  public void setData(List<BlogPostRow> data) {
    if (blogPostRows != null) {
      blogPostRows.clear();
    } else {
      blogPostRows = new ArrayList<BlogPostRow>();
    }
    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. 

Saturday, October 19, 2013

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:
package org.kevinmrohr.android_blog.model;

import org.joda.time.DateTime;

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 stupid 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:
package org.kevinmrohr.android_blog.service;

import org.kevinmrohr.android_blog.model.BlogPost;

import java.util.Arrays;
import java.util.List;

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 stupidly 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:
package org.kevinmrohr.android_blog.async;

import android.content.AsyncTaskLoader;
import android.content.Context;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.service.BlogPostService;

import java.util.List;

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:
package org.kevinmrohr.android_blog;

import android.app.Activity;
import android.app.LoaderManager;
import android.content.Loader;
import android.os.Bundle;
import android.widget.ListView;
import org.kevinmrohr.android_blog.adapter.BlogListAdapter;
import org.kevinmrohr.android_blog.async.BlogPostLoader;
import org.kevinmrohr.android_blog.model.BlogPost;
import org.kevinmrohr.android_blog.service.BlogPostService;

import java.util.ArrayList;
import java.util.List;

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:
package org.kevinmrohr.android_blog.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.kevinmrohr.android_blog.R;
import org.kevinmrohr.android_blog.model.BlogPost;

import java.util.ArrayList;
import java.util.List;

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>

Friday, October 18, 2013

Android with Maven

Ever since making the switch from Ant to Maven 2 back in 2005, I've never looked back. So one of the first things I wanted to know was if I could use maven to build my Android apps. And of course, you can. Here's an example pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.androidlearning</groupId>
    <artifactId>androidlearning</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>apk</packaging>
    <name>androidlearning</name>
    <description>Learning Android!</description>

    <dependencies>
    ...
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <sourceDirectory>src</sourceDirectory>
        <defaultGoal>install</defaultGoal>


        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                    <artifactId>android-maven-plugin</artifactId>
                    <version>3.7.0</version>
                    <extensions>true</extensions>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                <artifactId>android-maven-plugin</artifactId>
                <configuration>
                    <sdk>
                        <path>/opt/adt-bundle-linux-x86_64-20130917/sdk</path>
                        <!-- platform or api level (api level 4 = platform 1.6) -->
                        <platform>18</platform>
                    </sdk>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

There are certainly plenty of maven detractors, but the reasons why I prefer maven to ant (or any other tool that's available at the moment):

  • XML is a reasonably good format for configuration. It is a terrible format for programming. With ant, you are essentially programming a build with XML. Maven configures builds with XML.
  • Programming a build leads to developers doing...whatever they want. But builds should not be particularly complex things, and with a few conventions can be very standardized. This is what maven does. 
  • A maven expert is immediately a build expert for any product that builds with maven. Not so much with Ant (or Gradle, or any other build tool that gives the build developer freedom to program the build however they want). 
  • Maven's dependency management system is the right way to handle dependencies. There's no checking jar files into source control, just some configuration specifying what your dependencies are.

Hello (Android and Blogging) World!

So, after a year (or more?) of my good friend Bill Mote doing Android development and trying to get me involved with it, I'm making the plunge. In my past experience with learning new technologies, I typically forget at least half of what I've learned within a few months of not using the technology. Picking it back up again is always a little painful because I know I've learned all of this before...why can't I just remember?

In addition, I've been considering starting a technology blog. So, what better way than to document your learning process via a blog?

I'm currently weeks into doing Android development, so everything I've learned to this point is still pretty fresh in my mind. Over the course of the next few weeks, I'm planning making a blog post about each of the key things I've learned in doing Android development.

So...here we go!