Migrating off Roboguice 3 — Part 2

Let the migration begin …

Unlike Toothpick which only provides dependency injection for java objects, Roboguice 3 also provides view and extra injection for Android objects and also comes with its own logger. It was pretty clear early on that migrating off Roboguice 3 on our main app would require implementing multiple frameworks. The easiest approach was to replace each of Roboguice’s functionality one by one and test that everything worked before implementing the next phase.

Our plan for the migration to get off Roboguice 3 looked like this;

View Injection — Replace with Butterknife

Roboguice Ln (logger) — Replace with Timber

Extra Injection — Replace with Dart

Java Dependency Injection — Replace with Toothpick

View Injection

Replacing the Roboguice view injection was fairly simple. I did a global search and replace of the package name as well as the annotation.

Next I removed all the @ContentView annotations and replaced it with the setContentView() in the onCreate() method.

Roboguice

import com.google.inject.Inject;
@ContentView(R.layout.pay_bill)
public class PayBillActivity extends BaseActivity {
   @InjectView(R.id.buttonContinue)
Button continueButton;

Butterknife

import butterknife.BindView;
public class PayBillActivity extends BaseActivity {
   @BindView(R.id.buttonContinue)
Button continueButton;
   public void onCreate(Bundle saveInstanceState) {
setContentView(R.layout.pay_bill);

The final step involved setting the Butterknife.bind() method in both our BaseActivity and BaseFragment for injecting the views.

BaseActivity

@Override
public void setContentView(int layoutResID) {
super.setContentView(layoutResID);
ButterKnife.bind(this);
}

BaseFragment

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
}

Phew 1 framework down 4 to go.

Logger

The Roboguice 3 logger was used extensively in the app probably due to the convenience of not having to create a tag when logging. After doing a quick investigation of the logging frameworks available I was pleasantly surprised to find that the Timber framework had an almost identical interface to the Roboguice 3 logger.

Roboguice

package roboguice.util.Ln
public static int d(Throwable throwable, Object s1, Object... args)

Timber

package timber.log.Timber
public static void d(Throwable t, @NonNls String message, Object... args)

I performed another search and replace of the package and the logger and the migration was nearly complete. I wanted to ensure we leveraged any new features that came with these new frameworks. I noticed that the Timber logger had a nice feature for planting trees, which basically meant that now we could log our errors to fabric.io crashlytics and get useful metrics on exceptions that were previously swallowed. This was just another benefit of migrating off Roboguice 3.

2 frameworks down and 2 to go — half way

public class CrashlyticsTree extends Timber.Tree {

private static final String KEY_PRIORITY = "priority";
private static final String KEY_TAG = "tag";
private static final String KEY_MESSAGE = "message";

@Override
protected void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable t) {

if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
return;
}

Crashlytics.setInt(KEY_PRIORITY, priority);
Crashlytics.setString(KEY_TAG, tag);
Crashlytics.setString(KEY_MESSAGE, message);

if (t == null) {
Crashlytics.logException(new Exception(message));
} else {
Crashlytics.logException(t);
}

}

}

Dart

We also used Roboguice 3’s extra injection extensively throughout the app. Fortunately, while I was at the AndevCon in San Francisco, I decided to go to another presentation by Groupon about a framework called Dart which provides exactly the same functionality.

Again I did a global search and replace of the package name however as the format was slightly different between the annotations I had to manually make the changes to implement the code which did not take that long. I also added the injector Dart.inject(this) to the onCreate method to our BaseActivity so our developers did not have to bother adding this to each activity.

Roboguice

import roboguice.inject.InjectExtra;
public class PayBillActivity extends BaseActivity {
@InjectExtra(value = DISPLAY_MESSAGE, optional = true)                                                                 private String message;

Dart

import com.f2prateek.dart.InjectExtra;
public class PayBillActivity extends BaseActivity {
  @Nullable
@InjectExtra(DISPLAY_MESSAGE)
String message;

I also noticed that Dart provided a nice intent builder called Henson which generates convenience methods for any activity with injected extras. I decided to take advantage of this feature and converted the app to use Henson. I must say that the builder pattern is one of my favourite design patterns mainly due to its elegance and simplicity. Making this additional change simplified the code especially when the activities had lots of extras.

Before

Intent i = new Intent(this, PayBillActivity.class);
i.putExtra("MESSAGE", "hello");

After

Henson.with(context).gotoPayBillActivity().message("hello").build();

3 frameworks down and 1 to go… kind of anyway. While reading the dart README.md file I noticed the framework provided built in support for the Parcler framework. Our project had quite a few parseable object and having used Parcler before i decided to bite the bullet and increase the scope of the migration to include yet another framework. It was another piece of the boilerplate code i really wanted to replace so another developer on my team went through the app and converted all the classes to use the Parceler framework.

Toothpick

The final step involved implementing the Toothpick framework. This step took a bit longer than the other frameworks but overall went quite smoothly as I had previously road tested the framework on a sample project. The main concept in Toothpick revolves around scopes and creating a scope tree. I borrowed this definition from Stephanen Nicolas’s slides Migrate to TP from RG 4.

Scopes are:

  • a place to define bindings (via modules)
  • a place to recycle injected instances

Scopes can be used to:

  • inject dependencies into instances
  • getInstance of classes

Just like Roboguice 3, Toothpick also has modules that can be used to define bindings such as a particular class should be a singleton. These modules are then installed in a scope and used during the injection process.

A scope lower in the dependency tree can also be used to override a binding defined higher up in the scope tree. We used this feature in our Espresso tests to override the real objects with mocks.

Looking at our app we really only needed 2 scopes;

  • Application scope for defining global singletons and objects that access the ApplicationContext
  • Activity scope for defining objects that rely on the Activity Context.

The scope tree looked like this:

                     RootScope 
|
AppScope
|
ActivityScope

Your probably thinking why there are 3 scopes when I only mentioned 2. We found that when we updated our Espresso tests we needed the extra root scope to help with overriding the bindings with mocks as mentioned previously.

The application object was updated to install the module in the root scope and to create a scope tree as follows:

public class App extends Application {

public static final String ROOTSCOPE = "rootscope";
public static final String APPSCOPE = "appscope";
@Override
public void onCreate() {

super.onCreate();
context = this.getApplicationContext();

Scope rootScope = Toothpick.openScopes(ROOTSCOPE);
rootScope.installModules(new ApModule(this));

Toothpick.openScopes(ROOTSCOPE, APPSCOPE);

if (BuildConfig.DEBUG) {
Toothpick.setConfiguration(Configuration.forDevelopment());
}

}

Toothpick also has a really nice development feature which allows it to detect cyclic dependencies. When our app was failing to load, this feature proved useful in helping resolve this issue. Toothpick detected a cyclic dependency on startup and even printed out a nice graph showing where the issue was.

The migration involved replacing the Roboguice 3 injector with equivalent Toothpick injector throughout the app and also adding the Toothpick injector to all fragments and activities that were using injection. This update required removing the RoboContext from our services and updating our BaseActivity to extend from AppCompatActivity and our BaseFragment to extend from the support Fragment.

Roboguice

RoboGuice.getInjector(this).injectMembers(this);

Toothpick

Toothpick.inject(this, Toothpick.openScope(App.APPSCOPE));

Our application also extensively used the @ContextSingleton annotation that was popular with Roboguice 3. We noticed that we could replace the Context with the ApplicationContext in all our ContextSingleton classes except for one as these classes were either reading strings or accessing share preferences and did not require the Activity context.

For the one class that required an Activity context we injected this class in a new activity scope in the onCreate() method where it was being used:

@Override
protected void onCreate(Bundle bundle) {
    activityScope = Toothpick.openScopes(Application.ROOTSCOPE, Application.APPSCOPE, this);
activityScope.installModules(new ActivityModule(this));

super.onCreate(bundle);
}

When the activity is destroyed the scope also needs to be closed.

@Override
protected void onDestroy() {
Toothpick.closeScope(this);
super.onDestroy();
}

Conclusion

The migration from Roboguice 3 to the 5 new frameworks roughly took about 7–10 man days on our mid-size app. The app codebase is about 55K lines of code with another 20K lines of test code. Planning the migration and testing each framework meant the migration went relatively smoothly without encountering too many problems.

The migration was successful and during the migration we even managed to deliver other business features. There are many benefits to upgrading including improved performance, faster development (less boilerplate), happier developers and better maintainability.

Acknowledgments

Deepam Palaniswami — Converting the classes to use the Parcler framework

Wenchao Kong — Fixing the Unit and Espresso tests

Sara Sagi & Jenny Ho — Regression testing the app and giving us the thumps up to do this work in the first place

Stephanen Nicolas & Daniel Lemures — Promptly answering all my questions about the Toothpick framework

Jane Lee — Editing and proof reading