How to Optimize Dagger Module

  —  
 read

Do you know why Android developers learn JS? Because they need something to do while waiting for the app to build.

If you are Android developer, you are most probably familiar with experiencing long build times. Long build times slow down your development process and nobody likes it.

Especially in situations when you are debugging some nasty bug, you don't want to wait until your focus on a solution is gone.

An alternative solution to learning JS

A lot of build time is consumed by annotation processor tools of some kind. Big part of that build time consumption is used by dependency injection.

Since Dagger is the most popular dependency injector, and used in most of the modern projects, it made sense to take a deeper look and see if we can somehow optimize it.

It turned out that making a simple change can significantly improve build time.

Use @Binds instead of @Provides

To understand why this is an improvement we need to take a look at code that is being generated when using @Provides method.

For this purpose I have created a simple example, HelpersModule which provides StringManager. StringManager is simply a helper object used to provide you strings in presenters or view models without directly exposing Context.

But back to the Dagger, take a look at our HelpersModule.

@Module
class HelpersModule {

    @Provides
    @Singleton fun stringProvider(context: Context): StringManager = StringManagerImpl(context.resources)
}


We have annotated a method which gives us a concrete implementation of a wanted object using @Provides annotation, sounds good right?

Actually, it is a bit more than this. Under the hood Dagger needs to generate complete new factory class for this.

Take a look:

public final class HelpersModule_StringProviderFactory implements Factory<StringManager> {

    private final HelpersModule module;
    private final Provider<Context> contextProvider;

    public HelpersModule_StringProviderFactory(HelpersModule module,Provider<Context> contextProvider) {
        this.module = module;
        this.contextProvider = contextProvider;
    }

    @Override
    public StringManager get() {
        return stringProvider(module, contextProvider.get());
    }

    public static HelpersModule_StringProviderFactory create(HelpersModule module, Provider<Context> contextProvider) {
        return new HelpersModule_StringProviderFactory(module, contextProvider);
    }

    public static StringManager stringProvider(HelpersModule instance, Context context) {
        return Preconditions.checkNotNull(instance.stringProvider(context), "Cannot return null from a non-@Nullable @Provides method");
    }
}


If you read this factory class line by line you can figure out more or less what is going on.

In simple words, it is nothing more than a Java representation of what we have taken for granted while writing HelpersModule. The main point is that provides annotation will generate a class.

Let's try to write this identical thing using @Binds annotation:

@Module
interface HelpersModule {

    @Binds
    @Singleton 
    fun stringProvider(stringManagerImpl: StringManagerImpl): StringManager
}


If you try to make a build now, you'll see expected error:

error: [Dagger/MissingBinding] android.content.res.Resources cannot be provided without an @Inject constructor or an @Provides-annotated method.


We forgot to provide Resources object to StringManager. Let's be naive and create a new @Binds annotated method which will provide resources.

@Module
interface HelpersModule {

    @Binds
    @Singleton 
    fun resourcesProvider(context: Context): Resources = context.resources

    @Binds
    @Singleton 
    fun stringProvider(stringManagerImpl: StringManagerImpl): StringManager
}


Here's what happens:

error: @Binds methods' parameter type must be assignable to the return type


And that is exactly the difference between @Provides and @Binds annotations. @Binds annotation must be assignable to the return type. What does it mean?

In order to enable injection of some object that needs something itself, like StringManager needed Resources, we need to have all of these atomic objects provided somewhere.

In this example we needed to provide resources using the @Provides annotated method.

@Module
abstract class HelpersModule {

    @Provides
    @Singleton 
    fun resourcesProvider(@AppContext context: Context): Resources = context.resources

    @Binds
    @Singleton 
    abstract fun stringProvider(stringManagerImpl: StringManagerImpl): StringManager
}


Notice how I had to change our interface into abstract class in order to keep method that has implementation.

This should be fine now. Let's make a build:

error: A @Module may not contain both non-static and abstract binding methods


Ok, that is pretty much straightforward.

We can make the resourcesProvider method static or move it somewhere else. To keep the code clean, we need to separate different semantic entities.

For this purpose I have used my ApplicationModule which should contain those atomic providers. I have put resourcesProvider there. So this is final setup:

@Module
class ApplicationModule {

    @Provides
    @Singleton
    @AppContext
    fun provideApplicationContext(application: PlaygroundApp): Context {
        return application.applicationContext
    }

    @Provides
    @Singleton
    fun resourcesProvider(@AppContext context: Context): Resources = context.resources
}

@Module
interface HelpersModule {

    @Binds
    @Singleton 
    fun stringProvider(stringManagerImpl: StringManagerImpl): StringManager
}


After we have written this, let's make a build.

It works!

Now, we want to see generated class:








Correct. It does not exist.

Why?

Because there is no need for it.

In compilation time Dagger uses @Binds annotated methods to check if it can provide wanted object and to find appropriate Provider.

Like always, let's take a look under the hood. So, I have injected StringManager into DashboardViewModel.

class DashboardViewModel @Inject constructor(
    stringManager: StringManager
) : BaseViewModel<Nothing, DashboardEvent>() {

...
}


Dagger needs a factory for this view model. Generated class looks like this:

public final class DashboardViewModel_Factory implements Factory<DashboardViewModel> {
    private final Provider<StringManager> stringManagerProvider;

    public DashboardViewModel_Factory(Provider<StringManager> stringManagerProvider) {
        this.stringManagerProvider = stringManagerProvider;
    }

    @Override
    public DashboardViewModel get() {
        return new DashboardViewModel(stringManagerProvider.get());
    }

    public static DashboardViewModel_Factory create(Provider<StringManager> stringManagerProvider) {
        return new DashboardViewModel_Factory(stringManagerProvider);
    }

    public static DashboardViewModel newInstance(StringManager stringManager) {
        return new DashboardViewModel(stringManager);
    }
}


As you can see, Provider<StringManager> is all that was needed.

Maybe you think, "Ok, what did we accomplish here?".

Very soon this view model will need many other objects. We will still have one generated factory class. Inside of that factory, each injected object will have its Provider<T>. And none of the objects annotated with @Binds annotation will need to have their own generated factory class.

In large scale projects this makes a difference. Generating files is what really is consuming build time.

If you are still wondering where to find Provider<StringManager> and how it is linked to this factory, let's take a look at the generated DaggerAppComponent class.

public final class DaggerAppComponent implements AppComponent {  
    ...
    private Provider<Resources> resourcesProvider;  
    private Provider<StringManagerImpl> stringManagerImplProvider;
    ...
    private void initialize(final ApplicationModule applicationModuleParam,  
        final ApiModule apiModuleParam, final PlaygroundApp applicationParam) {
        ...
        this.resourcesProvider = DoubleCheck.provider(ApplicationModule_ResourcesProviderFactory.create(applicationModuleParam, provideApplicationContextProvider));  
        this.stringManagerImplProvider = StringManagerImpl_Factory.create(resourcesProvider);  
        this.stringProvider = DoubleCheck.provider((Provider) stringManagerImplProvider);
        ...
    }
    ...
}


DaggerAppComponent provides implementation of all components listed in the AppComponent interface. DoubleCheck.provider method simply returns a Provider that caches the value from the given delegate provider.

Use @Binds annotation wherever possible

We talked about Dagger module optimization through code examples. I have tested this approach on one of our projects at Infinum.

I made the changes using three rules:

  1. I have converted @Binds to @Provides annotation wherever possible.

  2. Whenever I could, I changed class or abstract class into interface. Interface is convenient because it forces you to use @Binds annotation.

  3. If there were some methods that required implementation, I moved them into another module.

After applying those changes I managed to reduce build time by 20%.

I have also counted generated files for both commit builds, and as expected, for each @Provides -> @Binds conversion there was one generated file less.

One other benefit is that DaggerAppComponent generated less code.

All of this didn’t take more than 10 minutes.

TL;DR Use @Binds instead of @Provides annotation wherever possible.

Turn lenghty build time into a cute ghost, like the ones designer Ana Valjak drew in the cover art.