Skip to content

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 20 commits into from
Dec 13, 2022
Merged
5 changes: 5 additions & 0 deletions contributor-docs/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
nav_order: 1
permalink: /
---

# Contributor documentation

This site is a collection of docs and best practices for contributors to Firebase Android SDKs.
Expand Down
1 change: 1 addition & 0 deletions contributor-docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mermaid:

# Enable or disable heading anchors
heading_anchors: true
permalink: pretty

callouts_level: quiet
callouts:
Expand Down
2 changes: 2 additions & 0 deletions contributor-docs/best_practices/best_practices.md
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 contributor-docs/best_practices/dependency_injection.md
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);
}
}
```
51 changes: 50 additions & 1 deletion contributor-docs/components/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
has_children: true
permalink: /components
permalink: /components/
nav_order: 4
---

# Firebase Components
Expand Down Expand Up @@ -145,6 +146,8 @@ The initialization phase of the FirebaseApp will consist of the following steps:
3. Store a map of {iface -> ComponentFactory} so that components can be instantiated on demand(Note that component instantiation does not yet happen)
4. Initialize EAGER components or schedule them to initialize on device unlock, if in direct boot mode.

### Initialization example

Below is an example illustration of the state of the component graph after initialization:

```mermaid
Expand Down Expand Up @@ -192,3 +195,49 @@ eager components depends on it(see Prefer Lazy dependencies to avoid this as mus
component initializes them by using a Lazy dependency.*
For example, if the application calls `FirebaseDatabase.getInstance()`, the container will initialize `Auth` and `Database`
and will return `Database` to the user.

### Support multiple instances of the SDK per `FirebaseApp`(multi-resource)

Some SDKs support multi-resource mode of operation, where it's possible to create more than one instance per `FirebaseApp`.

Examples:

* RTDB allows more than one database in a single Firebase project, so it's possible to instantiate one instance of the sdk per datbase

```kotlin
val rtdbOne = Firebase.database(app) // uses default database
val rtdbTwo = Firebase.database(app, "dbName")
```

* Firestore, functions, and others support the same usage pattern

To allow for that, such SDKs register a singleton "MultiResource" [Firebase component]({{ site.baseurl }}{% link components/components.md %}),
which creates instances per resource(e.g. db name).

Example

```kotlin
class DatabaseComponent(private val app: FirebaseApp, private val tokenProvider: InternalTokenProvider) {
private val instances: MutableMap<String, FirebaseDatabase> = new HashMap<>();

@Synchronized
fun get(dbName: String) : FirebaseDatabase {
if (!instances.containsKey(dbName)) {
instances.put(dbName, FirebaseDatabase(app, tokenProvider, dbName))
}
return instances.get(dbName);
}
}

class FirebaseDatabase(
app: FirebaseApp,
tokenProvider: InternalTokenProvider,
private val String dbName)

companion object {
fun getInstance(app : FirebaseApp) = getInstance("default")
fun getInstance(app : FirebaseApp, dbName: String) =
app.get(DatabaseComponent::class.java).get("default")
}

```
35 changes: 35 additions & 0 deletions contributor-docs/components/executors.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,38 @@ Qualified<Executor> bgExecutor = qualified(Background.class, Executor.class);
// ...
Executor sequentialExecutor = FirebaseExecutors.newSequentialExecutor(c.get(bgExecutor));
```

## Testing

`@Lightweight` and `@Background` executors have StrictMode enabled and throw exceptions on violations.
For example trying to do Network IO on either of them will throw.
With that in mind, when it comes to writing tests, prefer to use the common executors as opposed to creating
your own thread pools. This will ensure that your code uses the appropriate executor and does not slow down
all of Firebase by using the wrong one.

To do that, you should prefer relying on Components to inject the right executor even in tests. This will ensure
your tests are always using the executor that is actually used in your SDK build.
If your SDK uses Dagger, see [Dependency Injection]({{ site.baseurl }}{% link best_practices/dependency_injection.md %})
and [Dagger's testing guide](https://dagger.dev/dev-guide/testing).

When the above is not an option, you can use `TestOnlyExecutors`, but make sure you're testing your code with
the same executor that is used in production code:

```kotlin
dependencies {
// ...
testImplementation(project(":integ-testing"))
// or
androidTestImplementation(project(":integ-testing"))
}

```

This gives access to

```java
TestOnlyExecutors.ui();
TestOnlyExecutors.background();
TestOnlyExecutors.blocking();
TestOnlyExecutors.lite();
```
6 changes: 5 additions & 1 deletion contributor-docs/how_firebase_works.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
nav_order: 3
---

# How Firebase Works

## Background
Expand Down Expand Up @@ -38,7 +42,7 @@ During initialization, `FirebaseApp` discovers all Firebase SDKs present in the
In addition to `FirebaseOptions`, `FirebaseApp` registers additional components that product SDKs can request via dependency injection. To name a few:

* `android.content.Context`(Application context)
* [Common Executors](https://github.com/firebase/firebase-android-sdk/blob/master/docs/executors.md)
* [Common Executors]({{ site.baseurl }}{% link components/executors.md %})
* `FirebaseOptions`
* Various internal components

Expand Down
Loading