In a typical Android application Dagger is initialized in app startup during the Application.onCreate.
Setup: Initializing Dagger
A typical application may initialize Dagger and request a given dependency. When using Hilt the auto application code may look like the following:
@HiltAndroidApp | |
class MyApplication : Application() { | |
@Inject | |
lateinit var appInit: ApplicationInitializer | |
override fun onCreate() { | |
super.onCreate() | |
appInit.initTracking() | |
} | |
} |
Initializing app using Dagger Hilt
The Hilt compiler plugin will generate code that is similar to manually initializing the component manually.
class MyApplication : Application() { | |
lateinit var componentManager: ApplicationComponentManager | |
@Inject | |
lateinit var appInit: ApplicationInitializer | |
override fun onCreate() { | |
super.onCreate() | |
val component = DaggerApplicationComponentManager | |
.applicationModule(ApplicationModule(this)) | |
.build() | |
component.inject(myRepo) | |
appInit.initTracking() | |
} | |
} |
Initializing app using vanilla Dagger
On Application startup we need to connect to a remote server and send some diagnostic data to a remote server and initialize tracking. As soon as initTrackingis called it will start a new thread and execute the work in the non-blocking lambda and return execution context to Application.onCreate.
class ApplicationInitializer @Inject constructor( | |
private val okHttpClient: OkHttpClient, | |
private val gson: Gson | |
) { | |
fun initTracking() { | |
thread { | |
// init async | |
} | |
} | |
} |
ApplicationInitializer blocking Dagger inject
@InstallIn(SingletonComponent::class) | |
@Module | |
object NetworkModule { | |
@Singleton | |
fun provideOkHttp(): OkHttpClient { | |
return OkHttpClient.Builder().build() | |
} | |
@Singleton | |
fun provideGson(): Gson { | |
return GsonBuilder().create() | |
} | |
} |
Dagger Module providing dependencies
The Problem
When the app starts up it will execute Application.onCreate, create the component and inject the dependencies. In the Dagger module, providers marked with @Singleton will be lazily initialized once on the first usage. Certain dependencies such as OkHttp and Gson can take several hundred ms to initialize. In the above example the both provideOkHttp and provideGson will block in the Application.onCreate->component.inject() while the dependencies are initialized the first time. The usage of the dependencies is already non-blocking so ideally the initial creation would also be performed in a background thread.
blocking dagger startup
Deferring Dagger Init using Provider
Fortunately Dagger provides a construct enabling deferred initialization called Provider. Provider is in interface with a single method get. The first time get() is called Dagger will create an instance of the dependency using the method annotated with @Provides. Subsequent calls will utilize a cached version. The advantage of using the Provider interface is that it enables additional control over when and where the dependency will be created.
package javax.inject; | |
public interface Provider<T> { | |
T get(); | |
} |
The above example could be refactored to the following:
class ApplicationInitializer @Inject constructor( | |
private val provideOkHttp: Provider<OkHttpClient>, | |
private val provideGson: Provider<Gson> | |
) { | |
fun initTracking() { | |
thread { | |
val okhttp = provideOkHttp.get() | |
val gson = provideGson.get() | |
// init async | |
} | |
} | |
} |
Job Offers
Non-blocking startup
Enforcing Thread Safety
The above code will correctly initialize the dependencies using the Provider interface but there is nothing preventing another dependency from requesting the dependency in a blocking manner. To enforce this we can add an additional thread check.
@InstallIn(SingletonComponent::class) | |
@Module | |
object NetworkModule { | |
@Singleton | |
fun provideOkHttp(): OkHttpClient { | |
// verify not main thread | |
assert(Looper.getMainLooper() != Looper.myLooper()) | |
return OkHttpClient.Builder().build() | |
} | |
@Singleton | |
fun provideGson(): Gson { | |
// verify not main thread | |
assert(Looper.getMainLooper() != Looper.myLooper()) | |
return GsonBuilder().create() | |
} | |
} |
Async Chained Dependencies
It is common for dependencies to require other dependencies. For example a repository may require OkHttp and Gson to be initialized async. The module can be be updated as follows:
@InstallIn(SingletonComponent::class) | |
@Module | |
object NetworkModule { | |
@Singleton | |
fun provideOkHttp(): OkHttpClient { | |
// verify not main thread | |
assert(Looper.getMainLooper() != Looper.myLooper()) | |
return OkHttpClient.Builder().build() | |
} | |
@Singleton | |
fun provideGson(): Gson { | |
// verify not main thread | |
assert(Looper.getMainLooper() != Looper.myLooper()) | |
return GsonBuilder().create() | |
} | |
fun provideRepo( | |
provideOkHttp: Provider<OkHttpClient>, | |
provideGson: Provider<Gson> | |
): MainRepo { | |
return MainRepo(provideOkHttp::get, provideGson::get) | |
} | |
} |
The repository can be updated to receive lambdas to enable async injection without adding Dagger dependencies.
class MainRepo( | |
val provideOkHttp: () -> OkHttpClient, | |
val provideGson: () -> Gson | |
) { | |
fun fetchSomething() { | |
thread { | |
val okHttp = provideOkHttp() | |
val gson = provideGson() | |
} | |
} | |
} |
That is it. Please clap and follow me if you found this useful.