I recently gave a presentation about how Dagger works under the hood, and I was once again struck by the elegance of the javax.inject.Provider interface. The interface is so simple it almost seems useless, but it’s also incredibly flexible, and forms the basis of much of the code generated by Dagger.

Like many dependency injection frameworks for JVM languages, Dagger uses and builds on the standard set of annotations for injectable classes defined in JSR-330 and provided in the javax.inject package.

Here’s what the interface looks like.

public interface Provider<T> {
  T get();
}

Dagger generates providers for all of the dependencies required in your object graph. This means any classes with an @Inject annotated constructor, types returned from @Provides annotated factory functions in a module, and a few other cases will all have individual Providers generated by Dagger.

You write:

class WebApi @Inject constructor(
  httpClient: OkHttpClient
)

And Dagger generates (roughly):

public final class WebApi_Factory implements Provider<WebApi> {
  private final Provider<OkHttpClient> httpClientProvider;

  public WebApi_Factory(
    Provider<OkHttpClient> httpClientProvider
  ) {
    this.httpClientProvider = httpClientProvider;
  }

  @Override
  public WebApi get() {
    return new WebApi(httpClientProvider.get());
  }
}

So far that all seems pretty straight-forward. But let’s take a look at the power that this simple interface offers.

Shared instances

The simplest implementation of a Provider is shown above, and this is roughly what Dagger generates. It simply creates a new instance of an object by calling it’s injected constructor each time the get() function is called.

This is handy, but in a lot of cases we want to share a single instance of a class. The Provider interface makes this trivial. We can write a Provider that simply wraps another Provider, keeping a reference to the first generated value.

// We need a marker value to support nullable Ts
private const val UNINITIALIZED = Any()

/**
 * A non-threadsafe [Provider] that keeps and returns a single instance.
 */
class SharedInstanceProvider<T>(
  val delegate: Provider<T>
) : Provider<T> {
  private var instance: Any = UNINITIALIZED

  override fun get(): T {
    if (instance == UNINITIALIZED) {
      instance = delegate.get()
    }
    return instance as T
  }
}

With this simple Provider we can now wrap any other Provider, allowing us to provide a shared instance for all consumers.

Dagger has something similar to the above, called Lazy, which you can inject instead of a raw dependency to get the same functionality. Dagger also uses this functionality to scope dependencies to a Component or Subcomponent.

Eager vs. Lazy instantiation

The Provider interface allows the lifecycle of an object to be considered an implementation detail of the system. In both cases above the object creation is lazy: nothing gets instantiated until the get() function is called. But that fact is simply an implementation detail.

The simplicity of the Provider interface means that when the instance is created doesn’t matter to the caller, and we can make an instance that eagerly instantiates objects if we choose.

/**
 * A [Provider] that eagarly creates a shared instance.
 */
class EagerProvider<T>(
  delegate: Provider<T>
): Provider<T> {
  private val instance = delegate.get()

  override fun get(): T = instance
}

The above allows us to make any Provider eagerly instanted without requiring changes to any consuming code.

What, not how

Because Dagger’s generated factories are passing around Provider implementations, it’s incredibly easy to provide dependencies in a multitude of different ways without drastically complicating the code.

In Dagger, dependencies can be provided as static instances to your component factory, by factory functions in a module, or via constructor injection. They can be bound as some supertype, collected into sets and maps, or optionally provided.

The Provider interface hides the details of how an object is instantiated from consumers, allowing them to focus on what is being provided.

For example, a Provider that returns things from factory functions in a Module might look like this:

@Module
class NetworkModule {
  @Provides fun provideAnalytics(backend: Backend): Analytics
}

class AnalyticsProvider(
  private val module: NetworkModule,
  private val backendProvider: Provider<Backend>
): Provider<Analytics> {
  override fun get(): Analytics {
    return module.provideAnalytics(backendProvider.get())
  }
}

A Provider that returns static values passed into your component factory would look like this:

class ApiKeyProvider(
  private val apiKey: String,
): Provider<String> {
  override fun get(): String = apiKey
}

With all of these different implementations, consumers never need to know about the internals, and never need to change when those internal change.

Aim for simple APIs

When I’m writing code, even in proprietary internal modules, I like to keep the API surface of whatever I’m working on in mind. I like to see how I can make the API as simple as possible for consumers (even if the only consumer is slightly older me), and I’m often surprised by the flexibility that these simple abstractions can provide.