Do you know why Android developers learn JS? Because they need something to do while waiting for the app to build.
If you are an 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. A big part of that build time consumption is used by dependency injection.
Since Dagger is the most popular dependency injector and is used in most 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 the 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 that 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 a completely 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 an 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 the 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 the 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 the 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 the 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. The 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 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 the implementation of all components listed in the AppComponent
interface. TheDoubleCheck.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.