diff --git a/contributor-docs/README.md b/contributor-docs/README.md index 576d6ebdd54..c7b37f2b6f3 100644 --- a/contributor-docs/README.md +++ b/contributor-docs/README.md @@ -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. diff --git a/contributor-docs/_config.yml b/contributor-docs/_config.yml index 30b0263fa05..bb995ce78b3 100644 --- a/contributor-docs/_config.yml +++ b/contributor-docs/_config.yml @@ -48,6 +48,7 @@ mermaid: # Enable or disable heading anchors heading_anchors: true +permalink: pretty callouts_level: quiet callouts: diff --git a/contributor-docs/best_practices/best_practices.md b/contributor-docs/best_practices/best_practices.md index 053ccbbce66..1fc465d3ed7 100644 --- a/contributor-docs/best_practices/best_practices.md +++ b/contributor-docs/best_practices/best_practices.md @@ -1,5 +1,7 @@ --- has_children: true +permalink: /best_practices/ +nav_order: 5 --- # Best Practices diff --git a/contributor-docs/best_practices/dependency_injection.md b/contributor-docs/best_practices/dependency_injection.md new file mode 100644 index 00000000000..9ae5a66b60e --- /dev/null +++ b/contributor-docs/best_practices/dependency_injection.md @@ -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 + @Component.Builder + interface Builder { + @BindsInstance fun setFirebaseApp(app: FirebaseApp) + @BindsInstance fun setSomeInterop(interop: com.google.firebase.inject.Provider) + 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) { + 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() + + @Synchronized + fun get(dbName: String) : FirebaseDatabase { + if (!instances.containsKey(dbName)) { + mInstances.put(dbName, factory.create(dbName)) + } + return mInstances.get(dbName); + } +} +``` diff --git a/contributor-docs/components/components.md b/contributor-docs/components/components.md index c42874d55ed..de624274aab 100644 --- a/contributor-docs/components/components.md +++ b/contributor-docs/components/components.md @@ -1,6 +1,7 @@ --- has_children: true -permalink: /components +permalink: /components/ +nav_order: 4 --- # Firebase Components @@ -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 @@ -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 = 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") + } + +``` diff --git a/contributor-docs/components/executors.md b/contributor-docs/components/executors.md index c03fb52b518..f5dde773a86 100644 --- a/contributor-docs/components/executors.md +++ b/contributor-docs/components/executors.md @@ -176,3 +176,38 @@ Qualified 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(); +``` diff --git a/contributor-docs/how_firebase_works.md b/contributor-docs/how_firebase_works.md index 216d8d20c6c..2424bda6b3c 100644 --- a/contributor-docs/how_firebase_works.md +++ b/contributor-docs/how_firebase_works.md @@ -1,3 +1,7 @@ +--- +nav_order: 3 +--- + # How Firebase Works ## Background @@ -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 diff --git a/contributor-docs/onboarding/new_sdk.md b/contributor-docs/onboarding/new_sdk.md index f94427c3c5b..a922741fb3b 100644 --- a/contributor-docs/onboarding/new_sdk.md +++ b/contributor-docs/onboarding/new_sdk.md @@ -188,6 +188,10 @@ For Kotlin src/main/kotlin/com/google/firebase/foo/FirebaseFooRegistrar.kt +{: .warning } +You should strongly consider using [Dependency Injection]({{ site.baseurl }}{% link best_practices/dependency_injection.md %}) +to instantiate your sdk instead of manually constructing its instance in the `factory()` below. + ```kotlin class FirebaseFooRegistrar : ComponentRegistrar { override fun getComponents() = diff --git a/contributor-docs/onboarding/onboarding.md b/contributor-docs/onboarding/onboarding.md index 8aa439fc392..b7291dde2bc 100644 --- a/contributor-docs/onboarding/onboarding.md +++ b/contributor-docs/onboarding/onboarding.md @@ -1,6 +1,7 @@ --- has_children: true -permalink: /onboarding +permalink: /onboarding/ +nav_order: 2 --- # Onboarding diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 589b9ad670e..26ca9b7095c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] coroutines = "1.6.4" +dagger = "2.43.2" grpc = "1.50.2" javalite = "3.17.3" kotlin = "1.7.10" @@ -9,6 +10,9 @@ truth = "1.1.2" [libraries] androidx-annotation = { module = "androidx.annotation:annotation", version = "1.5.0" } +dagger = { module = "com.google.dagger:dagger", version.ref = "dagger"} +dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } +javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" }