Skip to content

Commit 856ade9

Browse files
authored
Merge ba98830 into 66c7398
2 parents 66c7398 + ba98830 commit 856ade9

File tree

10 files changed

+355
-3
lines changed

10 files changed

+355
-3
lines changed

contributor-docs/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
nav_order: 1
3+
permalink: /
4+
---
5+
16
# Contributor documentation
27

38
This site is a collection of docs and best practices for contributors to Firebase Android SDKs.

contributor-docs/_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ mermaid:
4848

4949
# Enable or disable heading anchors
5050
heading_anchors: true
51+
permalink: pretty
5152

5253
callouts_level: quiet
5354
callouts:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
---
22
has_children: true
3+
permalink: /best_practices/
4+
nav_order: 5
35
---
46

57
# Best Practices
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
parent: Best Practices
3+
---
4+
5+
# Dependency Injection
6+
7+
While [Firebase Components]({{ site.baseurl }}{% link components/components.md %}) provides basic
8+
Dependency Injection capabilities for interop between Firebase SDKs, it's not ideal as a general purpose
9+
DI framework for a few reasons, to name some:
10+
11+
* It's verbose, i.e. requires manually specifying dependencies and constructing instances of components in Component
12+
definitions.
13+
* It has a runtime cost, i.e. initialization time is linear in the number of Components present in the graph
14+
15+
As a result using [Firebase Components]({{ site.baseurl }}{% link components/components.md %}) is appropriate only
16+
for inter-SDK injection and scoping instances per `FirebaseApp`.
17+
18+
On the other hand, manually instantiating SDKs is often tedious, errorprone, and leads to code smells
19+
that make code less testable and couples it to the implementation rather than the interface. For more context see
20+
[Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) and [Motivation](https://github.com/google/guice/wiki/Motivation).
21+
22+
{: .important }
23+
It's recommended to use [Dagger](https://dagger.dev) for internal dependency injection within the SDKs and
24+
[Components]({{ site.baseurl }}{% link components/components.md %}) to inject inter-sdk dependencies that are available only at
25+
runtime into the [Dagger Graph](https://dagger.dev/dev-guide/#building-the-graph) via
26+
[builder setters](https://dagger.dev/dev-guide/#binding-instances) or [factory arguments](https://dagger.dev/api/latest/dagger/Component.Factory.html).
27+
28+
See: [Dagger docs](https://dagger.dev)
29+
See: [Dagger tutorial](https://dagger.dev/tutorial/)
30+
31+
{: .highlight }
32+
While Hilt is the recommended way to use dagger in Android applications, it's not suitable for SDK/library development.
33+
34+
## How to get started
35+
36+
Since [Dagger](https://dagger.dev) does not strictly follow semver and requires the dagger-compiler version to match the dagger library version,
37+
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
38+
`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
39+
negligible.
40+
41+
To use Dagger in your SDK use the following in your Gradle build file:
42+
43+
```groovy
44+
plugins {
45+
id("firebase-vendor")
46+
}
47+
48+
dependencies {
49+
implementation(libs.javax.inject)
50+
vendor(libs.dagger) {
51+
exclude group: "javax.inject", module: "javax.inject"
52+
}
53+
annotationProcessor(libs.dagger.compiler)
54+
}
55+
```
56+
57+
## General Dagger setup
58+
59+
As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), all components are scoped per `FirebaseApp`
60+
meaning there is a single instance of the component within a given `FirebaseApp`.
61+
62+
This makes it a natural fit to get all inter-sdk dependencies and instatiate the Dagger component inside the `ComponentRegistrar`.
63+
64+
```kotlin
65+
class MyRegistrar : ComponentRegistrar {
66+
override fun getComponents() = listOf(
67+
Component.builder(MySdk::class.java)
68+
.add(Dependency.required(FirebaseOptions::class.java))
69+
.add(Dependency.optionalProvider(SomeInteropDep::class.java))
70+
.factory(c -> DaggerMySdkComponent.builder()
71+
.setFirebaseApp(c.get(FirebaseApp::class.java))
72+
.setSomeInterop(c.getProvider(SomeInteropDep::class.java))
73+
.build()
74+
.getMySdk())
75+
.build()
76+
}
77+
```
78+
79+
Here's a simple way to define the dagger component:
80+
81+
```kotlin
82+
@Component(modules = MySdkComponent.MainModule::class)
83+
@Singleton
84+
interface MySdkComponent {
85+
// Informs dagger that this is one of the types we want to be able to create
86+
// In this example we only care about MySdk
87+
fun getMySdk() : MySdk
88+
89+
// Tells Dagger that some types are not available statically and in order to create the component
90+
// it needs FirebaseApp and Provider<SomeInteropDep>
91+
@Component.Builder
92+
interface Builder {
93+
@BindsInstance fun setFirebaseApp(app: FirebaseApp)
94+
@BindsInstance fun setSomeInterop(interop: com.google.firebase.inject.Provider<SomeInteropDep>)
95+
fun build() : MySdkComponent
96+
}
97+
98+
@Module
99+
interface MainModule {
100+
// define module @Provides and @Binds here
101+
}
102+
}
103+
```
104+
105+
The only thing left to do is to properly annotate `MySdk`:
106+
107+
```kotlin
108+
@Singleton
109+
class MySdk @Inject constructor(app: FirebaseApp, interopAdapter: MySdkAdapter) {
110+
fun someMethod() {
111+
interopAdapter.doInterop()
112+
}
113+
}
114+
115+
@Singleton
116+
class MySdkInteropAdapter @Inject constructor(private val interop: com.google.firebase.inject.Provider<SomeInteropDep>) {
117+
fun doInterop() {
118+
interop.get().doStuff()
119+
}
120+
}
121+
```
122+
123+
## Scope
124+
125+
Unlike Component, Dagger does not use singleton scope by default and instead injects a new instance of a type at each injection point,
126+
in the example above we want `MySdk` and `MySdkInteropAdapter` to be singletons so they are are annotated with `@Singleton`.
127+
128+
See [Scoped bindings](https://dagger.dev/dev-guide/#singletons-and-scoped-bindings) for more details.
129+
130+
### Support multiple instances of the SDK per `FirebaseApp`(multi-resource)
131+
132+
As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), some SDKs support multi-resource mode,
133+
which effectively means that there are 2 scopes at play:
134+
135+
1. `@Singleton` scope that the main `MultiResourceComponent` has.
136+
2. Each instance of the sdk will have its own scope.
137+
138+
```mermaid
139+
flowchart LR
140+
subgraph FirebaseApp
141+
direction TB
142+
subgraph FirebaseComponents
143+
direction BT
144+
subgraph GlobalComponents[Outside of SDK]
145+
direction LR
146+
147+
FirebaseOptions
148+
SomeInterop
149+
Executor["@Background Executor"]
150+
end
151+
152+
subgraph DatabaseComponent["@Singleton DatabaseMultiDb"]
153+
direction TB
154+
subgraph Singleton["@Singleton"]
155+
SomeImpl -.-> SomeInterop
156+
SomeImpl -.-> Executor
157+
end
158+
159+
subgraph Default["@DbScope SDK(default)"]
160+
MainClassDefault[FirebaseDatabase] --> SomeImpl
161+
SomeOtherImplDefault[SomeOtherImpl] -.-> FirebaseOptions
162+
MainClassDefault --> SomeOtherImplDefault
163+
end
164+
subgraph MyDbName["@DbScope SDK(myDbName)"]
165+
MainClassMyDbName[FirebaseDatabase] --> SomeImpl
166+
SomeOtherImplMyDbName[SomeOtherImpl] -.-> FirebaseOptions
167+
MainClassMyDbName --> SomeOtherImplMyDbName
168+
end
169+
end
170+
end
171+
end
172+
173+
classDef green fill:#4db6ac
174+
classDef blue fill:#1a73e8
175+
class GlobalComponents green
176+
class DatabaseComponent green
177+
class Default blue
178+
class MyDbName blue
179+
```
180+
181+
As you can see above, `DatabaseMultiDb` and `SomeImpl` are singletons, while `FirebaseDatabase` and `SomeOtherImpl` are scoped per `database name`.
182+
183+
It can be easily achieved with the help of [Dagger's subcomponents](https://dagger.dev/dev-guide/subcomponents).
184+
185+
For example:
186+
187+
```kotlin
188+
@Scope
189+
annotation class DbScope
190+
191+
@Component(modules = DatabaseComponent.MainModule::class)
192+
interface DatabaseComponent {
193+
fun getMultiDb() : DatabaseMultiDb
194+
195+
@Component.Builder
196+
interface Builder {
197+
// usual setters for Firebase component dependencies
198+
// ...
199+
fun build() : DatabaseComponent
200+
}
201+
202+
@Module(subcomponents = DbInstanceComponent::class)
203+
interface MainModule {}
204+
205+
@Subcomponent(modules = DbInstanceComponent.InstanceModule::class)
206+
@DbScope
207+
interface DbInstanceComponent {
208+
fun factory() : Factory
209+
210+
@Subcomponent.Factory
211+
interface Factory {
212+
fun create(@BindsInstance @Named("dbName") dbName: String)
213+
}
214+
}
215+
216+
@Module
217+
interface InstanceModule {
218+
// ...
219+
}
220+
}
221+
```
222+
223+
Annotating `FirebaseDatabase`:
224+
225+
```kotlin
226+
@DbScope
227+
class FirebaseDatabase @Inject constructor(options: FirebaseOptions, @Named dbName: String) {
228+
// ...
229+
}
230+
```
231+
232+
Implementing `DatabaseMultiDb`:
233+
234+
```kotlin
235+
@Singleton
236+
class DatabaseMultiDb @Inject constructor(private val factory: DbInstanceComponent.Factory) {
237+
private val instances = mutableMapOf<String, FirebaseDatabase>()
238+
239+
@Synchronized
240+
fun get(dbName: String) : FirebaseDatabase {
241+
if (!instances.containsKey(dbName)) {
242+
mInstances.put(dbName, factory.create(dbName))
243+
}
244+
return mInstances.get(dbName);
245+
}
246+
}
247+
```

contributor-docs/components/components.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
has_children: true
3-
permalink: /components
3+
permalink: /components/
4+
nav_order: 4
45
---
56

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

149+
### Initialization example
150+
148151
Below is an example illustration of the state of the component graph after initialization:
149152

150153
```mermaid
@@ -192,3 +195,49 @@ eager components depends on it(see Prefer Lazy dependencies to avoid this as mus
192195
component initializes them by using a Lazy dependency.*
193196
For example, if the application calls `FirebaseDatabase.getInstance()`, the container will initialize `Auth` and `Database`
194197
and will return `Database` to the user.
198+
199+
### Support multiple instances of the SDK per `FirebaseApp`(multi-resource)
200+
201+
Some SDKs support multi-resource mode of operation, where it's possible to create more than one instance per `FirebaseApp`.
202+
203+
Examples:
204+
205+
* RTDB allows more than one database in a single Firebase project, so it's possible to instantiate one instance of the sdk per datbase
206+
207+
```kotlin
208+
val rtdbOne = Firebase.database(app) // uses default database
209+
val rtdbTwo = Firebase.database(app, "dbName")
210+
```
211+
212+
* Firestore, functions, and others support the same usage pattern
213+
214+
To allow for that, such SDKs register a singleton "MultiResource" [Firebase component]({{ site.baseurl }}{% link components/components.md %}),
215+
which creates instances per resource(e.g. db name).
216+
217+
Example
218+
219+
```kotlin
220+
class DatabaseComponent(private val app: FirebaseApp, private val tokenProvider: InternalTokenProvider) {
221+
private val instances: MutableMap<String, FirebaseDatabase> = new HashMap<>();
222+
223+
@Synchronized
224+
fun get(dbName: String) : FirebaseDatabase {
225+
if (!instances.containsKey(dbName)) {
226+
instances.put(dbName, FirebaseDatabase(app, tokenProvider, dbName))
227+
}
228+
return instances.get(dbName);
229+
}
230+
}
231+
232+
class FirebaseDatabase(
233+
app: FirebaseApp,
234+
tokenProvider: InternalTokenProvider,
235+
private val String dbName)
236+
237+
companion object {
238+
fun getInstance(app : FirebaseApp) = getInstance("default")
239+
fun getInstance(app : FirebaseApp, dbName: String) =
240+
app.get(DatabaseComponent::class.java).get("default")
241+
}
242+
243+
```

contributor-docs/components/executors.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,38 @@ Qualified<Executor> bgExecutor = qualified(Background.class, Executor.class);
176176
// ...
177177
Executor sequentialExecutor = FirebaseExecutors.newSequentialExecutor(c.get(bgExecutor));
178178
```
179+
180+
## Testing
181+
182+
`@Lightweight` and `@Background` executors have StrictMode enabled and throw exceptions on violations.
183+
For example trying to do Network IO on either of them will throw.
184+
With that in mind, when it comes to writing tests, prefer to use the common executors as opposed to creating
185+
your own thread pools. This will ensure that your code uses the appropriate executor and does not slow down
186+
all of Firebase by using the wrong one.
187+
188+
To do that, you should prefer relying on Components to inject the right executor even in tests. This will ensure
189+
your tests are always using the executor that is actually used in your SDK build.
190+
If your SDK uses Dagger, see [Dependency Injection]({{ site.baseurl }}{% link best_practices/dependency_injection.md %})
191+
and [Dagger's testing guide](https://dagger.dev/dev-guide/testing).
192+
193+
When the above is not an option, you can use `TestOnlyExecutors`, but make sure you're testing your code with
194+
the same executor that is used in production code:
195+
196+
```kotlin
197+
dependencies {
198+
// ...
199+
testImplementation(project(":integ-testing"))
200+
// or
201+
androidTestImplementation(project(":integ-testing"))
202+
}
203+
204+
```
205+
206+
This gives access to
207+
208+
```java
209+
TestOnlyExecutors.ui();
210+
TestOnlyExecutors.background();
211+
TestOnlyExecutors.blocking();
212+
TestOnlyExecutors.lite();
213+
```

contributor-docs/how_firebase_works.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
nav_order: 3
3+
---
4+
15
# How Firebase Works
26

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

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

0 commit comments

Comments
 (0)