Skip to content

Add Component documentation #4429

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 4 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contributor-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ It describes how Firebase works on Android and provides guidance on how to build

- [Development Environment Setup]({{ site.baseurl }}{% link onboarding/env_setup.md %})
- [Creating a new SDK]({{ site.baseurl }}{% link onboarding/new_sdk.md %})
- [How Firebase works]({{ site.baseurl }}{% link how_firebase_works.md %})
- [How Firebase works]({{ site.baseurl }}{% link how_firebase_works.md %})
2 changes: 1 addition & 1 deletion contributor-docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ remote_theme: just-the-docs/[email protected]
plugins:
- jekyll-remote-theme

color_scheme: dark
color_scheme: light

# Aux links for the upper right navigation
aux_links:
Expand Down
189 changes: 189 additions & 0 deletions contributor-docs/components/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
has_children: true
---

# Firebase Components

Firebase is known for being easy to use and requiring no/minimal configuration at runtime.
Just adding SDKs to the app makes them discover each other to provide additional functionality,
e.g. `Firestore` automatically integrates with `Auth` if present in the app.

* Firebase SDKs have required and optional dependencies on other Firebase SDKs
* SDKs have different initialization requirements, e.g. `Analytics` and `Crashlytics` must be
initialized upon application startup, while some are initialized on demand only.

To accommodate these requirements Firebase uses a component model that discovers SDKs present in the app,
determines their dependencies and provides them to dependent SDKs via a `Dependency Injection` mechanism.

This page describes the aforementioned Component Model, how it works and why it's needed.

## Design Considerations

### Transparent/invisible to 3p Developers

To provide good developer experience, we don't want developers to think about how SDKs work and interoperate internally.
Instead we want our SDKs to have a simple API surface that hides all of the internal details.
Most products have an API surface that allows developers to get aninstance of a given SDK via `FirebaseFoo.getInstance()`
and start using it right away.

### Simple to use and integrate with for component developers

* The component model is lightweight in terms of integration effort. It is not opinionated on how components are structured.
* The component model should require as little cooperation from components runtime as possible.
* It provides component developers with an API that is easy to use correctly, and hard to use incorrectly.
* Does not sacrifice testability of individual components in isolation

### Performant at startup and initialization

The runtime does as little work as possible during initialization.

## What is a Component?

A Firebase Component is an entity that:

* Implements one or more interfaces
* Has a list of dependencies(required or optional). See [Dependencies]({{ site.baseurl }}{% link components/dependencies.md %})
* Has initialization requirements(e.g. eager in default app)
* Defines a factory creates an instance of the component’s interface given it's dependencies.
(In other words describes how to create the given component.)

Example:

```java
// Defines a component that is registered as both `FirebaseAuth` and `InternalAuthProvider`.
Component<FirebaseAuth> auth = Component.builder(FirebaseAuth.class, InternalAuthProvider.class)
// Declares dependencies
.add(Dependency.required(FirebaseOptions.class))
// Defines a factory
.factory(container -> new FirebaseAuth(container.get(FirebaseOptions.class)))
.eagerInDefaultApp() // alwaysEager() or lazy(), lazy is the default.
.build()
```

All components are singletons within a Component Container(e.g. one instance per FirebaseApp).
There are however SDKs that need the ability to expose multiple objects per FirebaseApp,
for example RTBD(as well as Storage and Firestore) has multidb support which allows developers
to access one or more databases within one FirebaseApp. To address this requirement,
SDKs have to register their components in the following form(or similar):

```java
// This is the singleton holder of different instances of FirebaseDatabase.
interface RtdbComponent {
FirebaseDatabase getDefault();
FirebaseDatabase get(String databaseName);
}
```

As you can see in the previous section, components are just values and don't have any behavior per se,
essentially they are just blueprints of how to create them and what dependencies they need.

So there needs to be some ComponentRuntime that can discover and wire them together into a dependency graph,
in order to do that, there needs to be an agreed upon location where SDKs can register the components they provide.

The next 2 sections describe how it's done.

## Component Registration

In order to define the `Components` an SDK provides, it needs to define a class that implements `ComponentRegistrar`,
this class contains all component definitions the SDK wants to register with the runtime:

```java
public class MyRegistrar implements ComponentRegistrar {
/// Returns a one or more Components that will be registered in
/// FirebaseApp and participate in dependency resolution and injection.
@Override
public Collection<FirebaseComponent<?>> getComponents() {
Arrays.asList(Component.builder(MyType.class)
/* ... */
.build());
}
}
```

## Component Discovery

In addition to creating the `ComponentRegistrar` class, SDKs also need to add them to their `AndroidManifest.xml` under `ComponentDiscoveryService`:

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service android:name="com.google.firebase.components.ComponentDiscoveryService"
android:exported="false">
<meta-data
android:name="com.google.firebase.components:com.google.firebase.foo.FirebaseFooRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>
</application>
</manifest>
```

When the final app is built, manifest registrar entries will all end up inside the above `service` as metadata key- value pairs.
At this point `FirebaseApp` will instantiate them and use the `ComponentRuntime` to construct the component graph.

## Dependency resolution and initialization

### Definitions and constraints

* **Component A depends on Component B** if `B` depends on an `interface` that `A` implements.
* **For any Interface I, only one component is allowed to implement I**(with the exception of
[Multibindings]({{ site.baseurl }}{% link components/multibindings.md %})). If this invariant is violated, the container will
fail to start at runtime.
* **There must not be any dependency cycles** among components. See Dependency Cycle Resolution on how this limitation can
be mitigated
* **Components are initialized lazily by default**(unless a component is declared eager) and are initialized when requested
by an application either directly or transitively.

The initialization phase of the FirebaseApp will consist of the following steps:

1. Get a list of available FirebaseComponents that were discovered by the Discovery mechanism
2. Topologically sort components based on their declared dependencies - failing if a dependency cycle is detected or multiple implementations are registered for any interface.
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.

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

```mermaid
flowchart TD
Analytics --> Installations
Auth --> Context
Auth --> FirebaseOptions
Context[android.os.Context]
Crashlytics --> Installations
Crashlytics --> FirebaseApp
Crashlytics --> FirebaseOptions
Crashlytics -.-> Analytics
Crashlytics --> Context
Database -.-> Auth
Database --> Context
Database --> FirebaseApp
Database --> FirebaseOptions
Firestore -.-> Auth
Messaging --> Installations
Messaging --> FirebaseOptions
Messaging --> Context
RemoteConfig --> FirebaseApp
RemoteConfig --> Context
RemoteConfig --> Installations


classDef eager fill:#4db66e,stroke:#4db6ac,color:#000;
classDef transitive fill:#4db6ac,stroke:#4db6ac,color:#000;
classDef always fill:#1a73e8,stroke:#7baaf7,color:#fff;

class Analytics eager
class Crashlytics eager
class Context always
class FirebaseOptions always
class FirebaseApp always
class Installations transitive
```

There are **2 explicitly eager** components in this example: `Crashlytics` and `Analytics`.
These components are initialized when `FirebaseApp` is initialized. `Installations` is initialized eagerly because
eager components depends on it(see Prefer Lazy dependencies to avoid this as mush as possible).
`FirebaseApp`, `FirebaseOptions` and `Android Context` are always present in the Component Container and are considered initialized as well.

*The rest of the components are left uninitialized and will remain so until the client application requests them or an eager
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.
7 changes: 7 additions & 0 deletions contributor-docs/components/dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
parent: Firebase Components
---

# Dependencies

TODO
7 changes: 7 additions & 0 deletions contributor-docs/components/multibindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
parent: Firebase Components
---

# Multibindings

TODO
2 changes: 1 addition & 1 deletion contributor-docs/how_firebase_works.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ public class MyRegistrar implements ComponentRegistrar {

This registrar is then registered in `AndroidManifest.xml` of the SDK and is used by `FirebaseApp` to discover all components and construct the dependency graph.

More details in [Firebase Components]({{ site.baseurl }}{% link in_depth/components.md %})).
More details in [Firebase Components]({{ site.baseurl }}{% link components/components.md %})).
7 changes: 0 additions & 7 deletions contributor-docs/in_depth/components.md

This file was deleted.

5 changes: 0 additions & 5 deletions contributor-docs/in_depth/in_depth.md

This file was deleted.