Mastodon

Dagger Party Tricks: Deferred OkHttp Initialization

Leveraging Dagger to defer OkHttp's initialization to a background thread and buy back precious startup time.

Dagger Party Tricks: Deferred OkHttp Initialization

This is part of a blog series of a talk I gave at Droidcon NYC and Londroid earlier this year. Dagger has a steep learning curve that frequently leaves developers not wanting to explore it further once they've integrated it. This series attempts to show some neat "party tricks" and other clever Dagger patterns to inspire more advanced usage.


The Problem

Consider the following code snippet.

@Provides
fun provideApi(): MyApi {
  return Retrofit.Builder()
      .baseUrl("https://example.com")
      .build()
      .create(MyApi::class.java)
}

This is roughly the same three lines of Retrofit that most apps have somewhere in their app. Expand it out a bit to be more Dagger-y:

@Module
object ApiModule {
    @Provides
    fun provideCache(ctx: Context): Cache {
      return Cache(ctx.cacheDir, CACHE_SIZE)
    }

    @Provides
    fun provideClient(cache: Cache): OkHttpClient {
      return OkHttpClient.Builder()
          .cache(cache)
          .build()
    }

    @Provides
    fun provideRetrofit(client: OkHttpClient): Retrofit {
      return Retrofit.Builder()
          .baseUrl("https://example.com")
          .client(client)
          .build()
    }

    @Provides
    fun provideApi(retrofit: Retrofit): MyApi {
      return retrofit.create(MyApi::class.java)
    }
}

Now you've got your Retrofit, its OkHttpClient, and its Cache all being provided into your MyApi provider. Again - probably most applications using Retrofit and Dagger have some form of the above code in their app. This can then be injected into some component later:

class MainController @Inject constructor(private val api: MyApi) {
  suspend fun onLoad() {
    val result = api.fetchStuff()
  }
}
Example where the fetchStuff() endpoint is a Kotlin suspend function.

Now here's the kicker: when does this DI initialization run?

Obviously you're likely using some Retrofit call adapter that makes requests on a background thread (RxJava, Coroutines, etc), but the initialization itself in the above code will always happen on whatever thread called the Dagger injection. In the above code, that's going to be the Main thread (or whatever framework equivalent is). It's not terrible, but it's not free either. In an Android world where you're racing to keep execution within the 16ms VSYNC buffer, this suddenly becomes a source of frame drops.

  • Most applications are probably doing this on startup, whether to create a singleton instance, log app-opening analytics, or if the example MainController is the first point of entry into the app. Now this is participating in a startup path with whatever else is on that path.
  • OkHttpClient initialization can take upwards of 100ms on some Android devices in the wild due to its use of TrustManagerFactory. Initializing the Cache is also not free, as it creates an internal Executor and may incur some disk IO if you want to prepare a cache directory for it first.

All of this is done even though this thing is supposed to only ever do work on a background thread!

The Solution

Dagger has an API called Lazy. You might have heard or used this before. It's pretty simple: wrap your injected dependency in Lazy and Dagger will magically lazily initialize it. But where can we use this?

If we try in our MainController, it doesn't really work.

class MainController @Inject constructor(private val api: Lazy<MyApi>) {
  suspend fun onLoad() {
    val result = api.get().fetchStuff()
  }
}

Sure our dependency is lazy, but we're just kicking the can down the road to the caller below. We could manually wrap that get() call in something on the background, but that's going to be unwieldy to maintain if we inject this anywhere else. It could also result in us allocating a sacrificial thread for initialization at every usage. Yikes!

We want to try to move this further up the chain. Let's look at that Retrofit block again.

@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .client(client)
        ...
}

Everyone knows Retrofit speaks OkHttp. Something you may not know however is that Retrofit specifically just speaks the Call.Factory interface in its builder API. OkHttpClient is just an implementation of this interface. The above snippet is actually just a short-hand for this:

@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .callFactory(client)
        ...
}

Since Call.Factory is just a SAM interface, we can wrap and delegate in a lambda.

@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .callFactory { client.newCall(it) }
        ...
}

Boom, we can push our Lazy into here!

@Provides
fun provideRetrofit(client: Lazy<OkHttpClient>): Retrofit {
    return Retrofit.Builder()
        .callFactory { client.get().newCall(it) }
        ...
}

The best part of this is that the Call.Factory we've given it will be called on a background thread, meaning the entire OkHttpClient stack above it is initialized off the main thread. This is exactly what we wanted! As an added bonus, downstream users of this dependency don't have to know anything about this behavior or try to defend against the previous main thread initialization.

If you want to be super sure, you could even add main thread checks in your OkHttpClient and Cache providers.

@Provides
fun provideCache(ctx: Context): Cache {
  checkMainThread()
  return Cache(ctx.cacheDir, CACHE_SIZE)
}

@Provides
fun provideClient(cache: Cache): OkHttpClient {
  checkMainThread()
  return OkHttpClient.Builder()
      .cache(cache)
      .build()
}

This is a clever trick to offload expensive initialization to a background thread and hurdle that bottleneck on your startup path. It's also simple enough that you could probably drop this into your code base today. What are you waiting for?

If you want some real examples in action, I use this in my CatchUp side project:

Special thanks to the folks at Square, who shared this a few years ago.