By Tom Reznik, on Sep 26, 2014

Android Discovery App

Preview Your Content on Android, Before Writing Any Code

Today, we are rolling out a new app to the Google Play store, the Discovery app for Android.

The app makes use of our recently announced Java SDK in order to get content delivered from Contentful to your device.

The Discovery app aims to provide an easy way to navigate through any type of content stored at Contentful, while offering several ways to visualize it on a real Android device.

This blog post will go into detail about some of the tools we are using with the Discovery app, and also present solutions to a few issues we have dealt with during the development phase.

The app is available on Google Play and is released as open source under the MIT license. Code is on GitHub.

Dealing with callbacks

In the Discovery app, we made use of the CallbackSet class. It is a simple thread-safe container for CDACallback instances.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class CallbackSet {
    private final HashSet<CDACallback> data;
    private final HashMap<String, HashSet<CDACallback>> tags;
    private final Object LOCK = new Object();

    public CallbackSet() {
        this.data = new HashSet<CDACallback>();
        this.tags = new HashMap<String, HashSet<CDACallback>>();
    }

    public <T extends CDACallback> T add(T callback) {
        synchronized (LOCK) {
            data.add(callback);
        }

        return callback;
    }

    public <T extends CDACallback> T add(String tag, T callback) {
        synchronized (LOCK) {
            HashSet<CDACallback> set = tags.get(tag);

            if (set == null) {
                set = new HashSet<CDACallback>();
                tags.put(tag, set);
            }

            set.add(callback);
            data.add(callback);
        }

        return callback;
    }

    public void cancelAndClear() {
        synchronized (LOCK) {
            for (CDACallback cb : data) {
                cb.cancel();
            }

            data.clear();
            tags.clear();
        }
    }

    public void cancel(String tag) {
        synchronized (LOCK) {
            HashSet<CDACallback> set = tags.get(tag);

            if (set != null) {
                for (CDACallback callback : set) {
                    callback.cancel();
                }

                data.removeAll(set);
                set.clear();
            }

            tags.remove(tag);
        }
    }
}

By calling the add(T callback) method, a callback reference is attached to the set, and the return type is inferred for convenience by using Java generics. All callbacks in a set can be cancelled with a single call to the cancelAndClear() method.

If you would like to cancel specific callbacks, consider using the add(String tag, T callback) method which makes it possible to tag callbacks, and later cancel them using the cancel(String tag) method.

This is particularly useful when invoking requests from within the context of an Activity or a Fragment, where callbacks should be cancelled according to specific lifecycle events.

Loaders

The Discovery app makes heavy use of Loaders, mainly to fetch remote data via network calls, but also to perform other asynchronous operations. A Loader can be used to load data asynchronously from the context of either an Activity or a Fragment.

In many cases Loaders can serve the same purpose of an AsyncTask, but far more efficiently. The biggest advantage with Loaders is the fact they can cache results between configuration changes, and return these cached results when asked for.

Consider the following scenario:

  • Activity onCreate() is called
  • Loader is initialized by calling initLoader()
  • Loader does some work in loadInBackground() method
  • Loader delivers the result via onLoadFinished() callback
  • User rotates the screen
  • Activity onDestroy() is called, followed by onCreate()
  • Loader is initialized again by calling initLoader()

In this case, the Loader will not go through the trouble of loadInBackground() again, and should deliver the previously cached result. Unless of course the contents of it's data source have changed, and depending on your implementation of the Loader class.

So what happens when you call initLoader() ? to quote the official documentation for this method:

"Call to initialize a particular ID with a Loader. If this ID already has a Loader associated with it, it is left unchanged and any previous callbacks replaced with the newly provided ones. If there is not currently a Loader for the ID, a new one is created and started.

This function should generally be used when a component is initializing, to ensure that a Loader it relies on is created. This allows it to re-use an existing Loader's data if there already is one, so that for example when an Activity is re-created after a configuration change it does not need to re-create its loaders."

What this means is that when you call initLoader() you provide an ID, which should be unique between controllers (Activity or Fragment components). In the same call you also provide a set of callbacks. Then, there are two possible scenarios:

1
1. The specified ID does not exist and is used for the first time

In this case a new Loader instance will be created and then started. The results will be delivered to the onLoadFinished() callback once the Loader has completed it's work.

1
2. The specified ID already exists

In this case the same Loader instance will be used, and only the callback set will be replaced. This also tells LoaderManager to deliver the most recent data to onLoadFinished() immediately in case such data exists.

Butter Knife

Butter Knife is a view injection library which uses annotation processing to inject code at compile-time. It eliminates the use of findViewById() calls, along with the need to cast the result to whatever view type you are referring. It also allows you to attach listeners to your views by annotating methods and without the use of anonymous inner-classes.

Creating fields that represent your views in your layout hierarchy is a trivial task now, all you have to do is annotate these fields with an @InjectView() annotation, specifying the resource ID you are after. You can also annotate methods to turn them into view listeners, i.e. @OnClick(), @OnItemSelected() and other listeners which the library supports.

The cool thing about Butter Knife is the way it works under the hood. It registers an annotation processor that will be invoked at compile-time, this processor can investigate the code and examine spots where specific custom annotations are used. Once such annotations are located, it injects code dynamically which will be available at runtime, essentially hardcoding the findViewById(), setOnClickListener(), and other calls into an encapsulated Java file that will be bundled up with your classes. Once you call ButterKnife.inject() at runtime, it simply executes that code.

Consider the following example:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ButterKnifeActivity extends Activity {
    @InjectView(R.id.textview) TextView textView;

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

        // Inject views
        ButterKnife.inject(this);
        
        // Views should be available at this point
    }

    @OnClick(R.id.button)
    void onClickButton() {
        // Handle click event
    }
}

This actually results with the following Java code being generated to a file named ButterKnifeActivity$$ViewInjector.java:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Generated code from Butter Knife. Do not modify!
package com.contentful.butterknifetest;

import android.view.View;
import butterknife.ButterKnife.Finder;

public class ButterKnifeActivity$$ViewInjector {
  public static void inject(Finder finder, 
  final com.contentful.butterknifetest.ButterKnifeActivity target, 
  Object source) {

    View view;
    
    view = finder.findRequiredView(source, 
    2131230721, "field 'textView'");
    
    target.textView = (android.widget.TextView) view;
    
    view = finder.findRequiredView(source, 
    2131230720, "method 'onClickButton'");
    
    view.setOnClickListener(
      new android.view.View.OnClickListener() {
        @Override public void onClick(
          android.view.View p0
        ) {
          target.onClickButton();
        }
      });
  }

  public static void reset(com.contentful.butterknifetest.ButterKnifeActivity target) {
    target.textView = null;
  }
}

So without going into too much detail, when we call ButterKnife.inject(this) from our Activity, it actually tracks down the proper ViewInjector class and invokes it's static inject() method. Eventually resulting in the desired view references being injected directly to our class. It also sets any view listeners you might have registered.

You can also inject multiple listeners at once:

java
1
2
3
4
@OnClick({ R.id.view_a, R.id.view_b, R.id.view_c })
public void onClickSomething(View v) {
  // Handle click event
}

If you still have to call findViewById() manually, consider using one of the ButterKnife.findById() methods, which actually infers the target type and by that eliminates the need to cast the result.

To sum it up, Butter Knife is a really powerful tool. By using it you get to write less code for repeated tasks known to any Android developer, thus resulting in a shorter development time and much more maintainable code. And while it is possible to implement this type of feature with a library that actually inspects your code at runtime and looks for these annotations, eventually achieving the same result, it would be far less efficient and have a serious impact on performance. Since as you probably know, reflection is expensive. Butter Knife does all of this heavy lifting at compile-time, allowing it to actually make your fields reference the desired views with minimum effort.

Picasso

Picasso is an image downloading and caching library for Android. It allows you to fetch, transform, cache and display images from various sources. Whether those are remote images served via HTTP, from a Content Provider, from your resources folder, assets folder or even from somewhere in the Android filesystem, Picasso can take care of the job.

While there are a few alternatives to Picasso out there (Volley, Universal Image Loader, and others) - Picasso has a really powerful API. It has multiple cache levels (disk + memory), and uses LRU by default. Once you make a request for an image, it first checks the memory cache (on the main thread!) and if that image is available it immediately sets it on your ImageView. If the image is not available, it delegates work to one of it's "hunters" to fetch these images from the context of a background thread. An important thing to know about Picasso is that it doesn't take care of disk caching by itself, it relies on the HTTP client to do that. The default client that ships with ICS+ has built in disk caching support, but if you are supporting lower API levels, consider using a replacement such as OkHttp.

You can easily resize images, either to specific dimensions or to actually fit the target view's dimensions (and if that view has not been measured yet, Picasso will wait for that to happen).

A notable feature of Picasso is the fact it automatically detects re-using of views by an Adapter, and cancels any pending downloads in case the view is being recycled.

Another powerful feature of it's API is the ability to apply custom transformations to Bitmap objects. If a custom transformation is requested, and once all built-in transformations are applied (i.e. resize, rotate, ...), this transformation will be invoked from the context of a worker thread, allowing you to create and return a new transformed Bitmap. A good example of this is presented in the Coffee guide app which we have released a while ago. We wanted to transform remote images into circular ones and display them.

Coffee Guide List Item

We create a custom transformation with the CircleTransform class:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CircleTransform implements Transformation {
    @Override
    public Bitmap transform(Bitmap source) {
        int size = Math.min(source.getWidth(), source.getHeight());

        int x = (source.getWidth() - size) / 2;
        int y = (source.getHeight() - size) / 2;

        Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
        if (squaredBitmap != source) {
            source.recycle();
        }

        Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());

        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
        paint.setShader(shader);
        paint.setAntiAlias(true);

        float r = size/2f;
        canvas.drawCircle(r, r, r, paint);

        squaredBitmap.recycle();
        return bitmap;
    }

    @Override
    public String key() {
        return getClass().getName();
    }
}

Then, in order to apply this transformation to an image, all you have to do is specify it with the RequestCreator object you are invoking with Picasso, for example:

java
1
2
3
4
Picasso.with(context)
        .load(/* ... */)
        .transform(new CircleTransform())
        .into(imageView);

Picasso offers a lot of other useful features which are not covered in this blog post, so if you are not familiar with them already, be sure to check those out.

Conclusion

There are many great open-source libraries out there, libraries that can simplify challenging tasks in every developer's workflow. Hopefully this blog post reveals something new to you, and if you have any feedback, we would love to hear it. Stay tuned to our blog, more coming soon!

Happy coding q:)

Tom Reznik

Former android developer at Contentful. You can follow Tom on Twitter.