Dependency Injection
While Firebase Components provides basic Dependency Injection capabilities for interop between Firebase SDKs, it’s not ideal as a general purpose DI framework for a few reasons, to name some:
- It’s verbose, i.e. requires manually specifying dependencies and constructing instances of components in Component definitions.
- It has a runtime cost, i.e. initialization time is linear in the number of Components present in the graph
As a result using Firebase Components is appropriate only for inter-SDK injection and scoping instances per FirebaseApp
.
On the other hand, manually instantiating SDKs is often tedious, errorprone, and leads to code smells that make code less testable and couples it to the implementation rather than the interface. For more context see Dependency Injection and Motivation.
It’s recommended to use Dagger for internal dependency injection within the SDKs and Components to inject inter-sdk dependencies that are available only at runtime into the Dagger Graph via builder setters or factory arguments.
See: Dagger docs See: Dagger tutorial
While Hilt is the recommended way to use dagger in Android applications, it’s not suitable for SDK/library development.
How to get started
Since Dagger does not strictly follow semver and requires the dagger-compiler version to match the dagger library version, it’s not safe to depend on it via a pom level dependency, see This comment for context. For this reason in Firebase SDKs we “vendor/repackage” Dagger into the SDK itself under com.google.firebase.{sdkname}.dagger
. While it incurs in a size increase, it’s usually on the order of a couple of KB and is considered negligible.
To use Dagger in your SDK use the following in your Gradle build file:
plugins {
id("firebase-vendor")
}
dependencies {
implementation(libs.javax.inject)
vendor(libs.dagger.dagger) {
exclude group: "javax.inject", module: "javax.inject"
}
annotationProcessor(libs.dagger.compiler)
}
General Dagger setup
As mentioned in Firebase Components, all components are scoped per FirebaseApp
meaning there is a single instance of the component within a given FirebaseApp
.
This makes it a natural fit to get all inter-sdk dependencies and instatiate the Dagger component inside the ComponentRegistrar
.
class MyRegistrar : ComponentRegistrar {
override fun getComponents() = listOf(
Component.builder(MySdk::class.java)
.add(Dependency.required(FirebaseOptions::class.java))
.add(Dependency.optionalProvider(SomeInteropDep::class.java))
.factory(c -> DaggerMySdkComponent.builder()
.setFirebaseApp(c.get(FirebaseApp::class.java))
.setSomeInterop(c.getProvider(SomeInteropDep::class.java))
.build()
.getMySdk())
.build()
}
Here’s a simple way to define the dagger component:
@Component(modules = MySdkComponent.MainModule::class)
@Singleton
interface MySdkComponent {
// Informs dagger that this is one of the types we want to be able to create
// In this example we only care about MySdk
fun getMySdk() : MySdk
// Tells Dagger that some types are not available statically and in order to create the component
// it needs FirebaseApp and Provider<SomeInteropDep>
@Component.Builder
interface Builder {
@BindsInstance fun setFirebaseApp(app: FirebaseApp)
@BindsInstance fun setSomeInterop(interop: com.google.firebase.inject.Provider<SomeInteropDep>)
fun build() : MySdkComponent
}
@Module
interface MainModule {
// define module @Provides and @Binds here
}
}
The only thing left to do is to properly annotate MySdk
:
@Singleton
class MySdk @Inject constructor(app: FirebaseApp, interopAdapter: MySdkAdapter) {
fun someMethod() {
interopAdapter.doInterop()
}
}
@Singleton
class MySdkInteropAdapter @Inject constructor(private val interop: com.google.firebase.inject.Provider<SomeInteropDep>) {
fun doInterop() {
interop.get().doStuff()
}
}
Scope
Unlike Component, Dagger does not use singleton scope by default and instead injects a new instance of a type at each injection point, in the example above we want MySdk
and MySdkInteropAdapter
to be singletons so they are are annotated with @Singleton
.
See Scoped bindings for more details.
Support multiple instances of the SDK per FirebaseApp
(multi-resource)
As mentioned in Firebase Components, some SDKs support multi-resource mode, which effectively means that there are 2 scopes at play:
@Singleton
scope that the mainMultiResourceComponent
has.- Each instance of the sdk will have its own scope.
flowchart LR
subgraph FirebaseApp
direction TB
subgraph FirebaseComponents
direction BT
subgraph GlobalComponents[Outside of SDK]
direction LR
FirebaseOptions
SomeInterop
Executor["@Background Executor"]
end
subgraph DatabaseComponent["@Singleton DatabaseMultiDb"]
direction TB
subgraph Singleton["@Singleton"]
SomeImpl -.-> SomeInterop
SomeImpl -.-> Executor
end
subgraph Default["@DbScope SDK(default)"]
MainClassDefault[FirebaseDatabase] --> SomeImpl
SomeOtherImplDefault[SomeOtherImpl] -.-> FirebaseOptions
MainClassDefault --> SomeOtherImplDefault
end
subgraph MyDbName["@DbScope SDK(myDbName)"]
MainClassMyDbName[FirebaseDatabase] --> SomeImpl
SomeOtherImplMyDbName[SomeOtherImpl] -.-> FirebaseOptions
MainClassMyDbName --> SomeOtherImplMyDbName
end
end
end
end
classDef green fill:#4db6ac
classDef blue fill:#1a73e8
class GlobalComponents green
class DatabaseComponent green
class Default blue
class MyDbName blue
As you can see above, DatabaseMultiDb
and SomeImpl
are singletons, while FirebaseDatabase
and SomeOtherImpl
are scoped per database name
.
It can be easily achieved with the help of Dagger’s subcomponents.
For example:
@Scope
annotation class DbScope
@Component(modules = DatabaseComponent.MainModule::class)
interface DatabaseComponent {
fun getMultiDb() : DatabaseMultiDb
@Component.Builder
interface Builder {
// usual setters for Firebase component dependencies
// ...
fun build() : DatabaseComponent
}
@Module(subcomponents = DbInstanceComponent::class)
interface MainModule {}
@Subcomponent(modules = DbInstanceComponent.InstanceModule::class)
@DbScope
interface DbInstanceComponent {
fun factory() : Factory
@Subcomponent.Factory
interface Factory {
fun create(@BindsInstance @Named("dbName") dbName: String)
}
}
@Module
interface InstanceModule {
// ...
}
}
Annotating FirebaseDatabase
:
@DbScope
class FirebaseDatabase @Inject constructor(options: FirebaseOptions, @Named dbName: String) {
// ...
}
Implementing DatabaseMultiDb
:
@Singleton
class DatabaseMultiDb @Inject constructor(private val factory: DbInstanceComponent.Factory) {
private val instances = mutableMapOf<String, FirebaseDatabase>()
@Synchronized
fun get(dbName: String) : FirebaseDatabase {
if (!instances.containsKey(dbName)) {
mInstances.put(dbName, factory.create(dbName))
}
return mInstances.get(dbName);
}
}