Dagger 2 usage on Android

This is the second part of a write up about Dagger 2 on Android.

All usages of Dagger below refer to Dagger 2

Component lifecycle / Singleton

Dagger, and other DI-frameworks, have the concept of scopes to decide if an instance should be re-used or not. If we don't specify any scope, like in part 1, we'll get a new instance every time we ask for a dependency. That can be suitable for some cases but often we want to reuse instances between parts of our application to save resources.

In JSR 330 there is one scope annotation, @Singleton, that Dagger understands. If we annotate a dependency and the component with @Singleton that instance will be reused amongst everyone who request it.

@Module
public class MyModule {

    @Provides
    @Singleton
    Dependency1 provideDependency1() {
        return new Dependency1();
    }

    @Provides
    Dependency2 provideDependency2() {
        return new Dependency2();
    }
}

@Singleton
@Component(modules=MyModule.class)
interface MyComponent {
    Dependency1 getDependency1();
    Dependency2 getDependency2();
}

class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        MyComponent component = DaggerMyComponent.create();

        Dependency1 singleton1 = component.getDependency1();
        Dependency1 singleton2 = component.getDependency1();

        Dependency2 notSingleton1 = component.getDependency2();
        Dependency2 notSingleton2 = component.getDependency2();

        assertEquals(singleton1, singleton2);
        assertNotEquals(notSingleton1, notSingleton2);
    }
}

Dagger also supports custom scopes which we can use to give our dependencies a custom lifecycle. Typical examples on Android is instances that lives as long as an Activity does or instances that lasts as long as a user is logged in.

We're looking at custom scopes in part 3.

Module configuration / Contexts in modules

It's quite common that we want to configure our modules with different server URLs between production and development for example. Or that we need a Contex, which we can’t create our self in the module. To do that we can instantiate the modules our self and inject them when we create our component. We use the provided builder pattern instead of Component.create() that we used earlier.

@Module
public class AppModule {

    private Context context;

    public AppModule(Context context) {
        this.context = context;
    }

    @Provides
    @Singleton
    SharedPreferences provideSharedPreferences() {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }
}

@Module
public class NetModule {

    private String baseUrl;

    public NetModule(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    @Provides
    String provideBaseUrl() {
        return baseUrl;
    }
}

@Singleton
@Component(modules = {AppModule.class, NetModule.class})
interface MyComponent {
    void inject(MyActivity act);
}

public class MyApp extends Application {

    private MyComponent myComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        String serverUrl;
        if (BuildConfig.DEBUG) {
            serverUrl = "https://dev.server.tld";
        }
        else {
            serverUrl = "https://server.tld";
        }

        myComponent = DaggerMyComponent.builder()
                .appModule(new AppModule(this))
                .netModule(new NetModule(serverUrl))
                .build();
    }

    public MyComponent getComponent() {
        return myComponent;
    }
}

Named dependencies / multiple dependencies of the same type

There are cases where we want to provide more than one object of the same type. For example, exposing a server URL and a API-key which both are strings. Or we need two different SharedPreference's, one for normal settings and one for secrets.

To do that we need to use qualifiers. JSR 330 provides us with the Qualifier @Named which is used together with a string to identify the dependency.

@Module
public class NetModule {

    ...

    @Provides
    @Named("baseUrl")
    String getBaseUrl() {
        return baseUrl;
    }

    @Provides
    @Named("apiKey")
    String provideApiKey() {
        return apiKey;
    }
}

And then we can use the same qualifier everywhere we need a dependency. It works everywhere @Inject or @Provides works.

public class MyActivity extends Activity {

    @Inject
    @Named("baseUrl")
    String baseUrl;

    @Inject
    @Named("apiKey")
    String apiKey;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        ((MyApp) getApplication()).getComponent().inject(this);
    }
}

We can also setup our own custom qualifiers in Dagger. They work in the same way but allows us to use our IDE's a little better by providing static links.

Instead of providing a unique string to @Named we create unique @interface's.

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface DefaultPrefs {
}

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface SecretPrefs {
}

And just like in @Named we can use those qualifiers on @Inject and @Provides.


@Module
public class AppModule {

    ...

    @Provides
    @Singleton
    @DefaultPrefs
    SharedPreferences provideDefaultSharedPreferences(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    @Provides
    @Singleton
    @SecretPrefs
    SharedPreferences provideSecretSharedPreferences(Context context) {
        return context.getSharedPreferences("secrets", Context.MODE_PRIVATE);
    }
}

public class Authenticator {

    @Inject
    public Authenticator(@SecretPrefs SharedPreferences secretPreferences) {
        ...
    }
}

Lazy injections

Dagger allows us to request dependencies wrapped in Lazy which won't be created until we call get() on the object. That can be useful if we have some object that is expensive to instantiate and only used on certain user interactions.

@Inject
Lazy<ExpensiveObject> expensiveObject;

void onRareEvent() {
    expensiveObject.get().doSomething();
}

Injections in base classes

Due to the way Dagger works we must do the injection in the super class directly, we can't hide it in a base class for example. But a common solution is to fetch the Component in a base class at least. And either provide it as a field or use a abstract function.

abstract class BaseActivity extends Activity {

    abstract void onInject(MyComponent component);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        onInject(((MyApplication)this).getComponent());
    }
}

public class MyActivity extends Activity {

    @Inject
    Dependency BaseActivity;

    void onInject(MyComponent component) {
        component.inject(this);
    }
}