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:
- Navigating from one activity to another
- Butterknife
- Dagger
A few notes on Butterknife and Dagger
VS Roboguice
Additional pom.xml dependencies
Just three new maven dependencies for butterknife and dagger:
com.jakewharton
butterknife
3.0.0
com.squareup.dagger
dagger
1.1.0
com.squareup.dagger
dagger-compiler
1.1.0
true
The Blog View Activity
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:
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 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()));
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>() {});
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.