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:

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:

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

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&lt;JSONArray&gt;() {
          @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.