-
Notifications
You must be signed in to change notification settings - Fork 617
Add dependency injection docs #4447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
4f94a9a
update
vkryachko 9e77ea7
fix index
vkryachko 6daaf6c
update
vkryachko 31844c5
update
vkryachko 98fd638
fix links
vkryachko cfd0b86
fix dagger
vkryachko eef3adf
add multi resource docs
vkryachko 2043a93
pretty urls
vkryachko 3c6bcf9
undo
vkryachko 3d86fb3
redo
vkryachko 241f179
undo
vkryachko 893c7ff
pretty urls
vkryachko 028b8c9
fix
vkryachko 4840d46
undo workflow changes
vkryachko 990c0f3
undo text-transform
vkryachko 2035ca2
update
vkryachko 74f3a34
deploy
vkryachko 72ff8cd
undo
vkryachko 29725c7
Apply suggestions from code review
vkryachko ba98830
fix
vkryachko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
--- | ||
has_children: true | ||
permalink: /best_practices/ | ||
nav_order: 5 | ||
--- | ||
|
||
# Best Practices |
247 changes: 247 additions & 0 deletions
247
contributor-docs/best_practices/dependency_injection.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
--- | ||
parent: Best Practices | ||
--- | ||
|
||
# Dependency Injection | ||
|
||
While [Firebase Components]({{ site.baseurl }}{% link components/components.md %}) 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]({{ site.baseurl }}{% link components/components.md %}) 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](https://en.wikipedia.org/wiki/Dependency_injection) and [Motivation](https://github.com/google/guice/wiki/Motivation). | ||
|
||
{: .important } | ||
It's recommended to use [Dagger](https://dagger.dev) for internal dependency injection within the SDKs and | ||
[Components]({{ site.baseurl }}{% link components/components.md %}) to inject inter-sdk dependencies that are available only at | ||
runtime into the [Dagger Graph](https://dagger.dev/dev-guide/#building-the-graph) via | ||
[builder setters](https://dagger.dev/dev-guide/#binding-instances) or [factory arguments](https://dagger.dev/api/latest/dagger/Component.Factory.html). | ||
|
||
See: [Dagger docs](https://dagger.dev) | ||
See: [Dagger tutorial](https://dagger.dev/tutorial/) | ||
|
||
{: .highlight } | ||
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](https://dagger.dev) 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](https://github.com/firebase/firebase-android-sdk/issues/1677#issuecomment-645669608) 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: | ||
|
||
```groovy | ||
plugins { | ||
id("firebase-vendor") | ||
} | ||
|
||
dependencies { | ||
implementation(libs.javax.inject) | ||
vendor(libs.dagger) { | ||
exclude group: "javax.inject", module: "javax.inject" | ||
} | ||
annotationProcessor(libs.dagger.compiler) | ||
} | ||
``` | ||
|
||
## General Dagger setup | ||
|
||
As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), 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`. | ||
|
||
```kotlin | ||
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: | ||
|
||
```kotlin | ||
@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`: | ||
|
||
```kotlin | ||
@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](https://dagger.dev/dev-guide/#singletons-and-scoped-bindings) for more details. | ||
|
||
### Support multiple instances of the SDK per `FirebaseApp`(multi-resource) | ||
|
||
As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), some SDKs support multi-resource mode, | ||
which effectively means that there are 2 scopes at play: | ||
|
||
1. `@Singleton` scope that the main `MultiResourceComponent` has. | ||
2. Each instance of the sdk will have its own scope. | ||
|
||
```mermaid | ||
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](https://dagger.dev/dev-guide/subcomponents). | ||
|
||
For example: | ||
|
||
```kotlin | ||
@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`: | ||
|
||
```kotlin | ||
@DbScope | ||
class FirebaseDatabase @Inject constructor(options: FirebaseOptions, @Named dbName: String) { | ||
// ... | ||
} | ||
``` | ||
|
||
Implementing `DatabaseMultiDb`: | ||
|
||
```kotlin | ||
@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); | ||
} | ||
} | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.