diff --git a/contributor-docs/README.md b/contributor-docs/README.md index a22f0abc440..576d6ebdd54 100644 --- a/contributor-docs/README.md +++ b/contributor-docs/README.md @@ -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 %}) \ No newline at end of file +- [How Firebase works]({{ site.baseurl }}{% link how_firebase_works.md %}) diff --git a/contributor-docs/_config.yml b/contributor-docs/_config.yml index 4e59cc7201f..14f8efa4e92 100644 --- a/contributor-docs/_config.yml +++ b/contributor-docs/_config.yml @@ -6,7 +6,7 @@ remote_theme: just-the-docs/just-the-docs@v0.4.0.rc3 plugins: - jekyll-remote-theme -color_scheme: dark +color_scheme: light # Aux links for the upper right navigation aux_links: diff --git a/contributor-docs/components/components.md b/contributor-docs/components/components.md new file mode 100644 index 00000000000..2c3ad2e8def --- /dev/null +++ b/contributor-docs/components/components.md @@ -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 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> 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 + + + + + + + +``` + +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. diff --git a/contributor-docs/components/dependencies.md b/contributor-docs/components/dependencies.md new file mode 100644 index 00000000000..819a098b78d --- /dev/null +++ b/contributor-docs/components/dependencies.md @@ -0,0 +1,7 @@ +--- +parent: Firebase Components +--- + +# Dependencies + +TODO diff --git a/contributor-docs/components/multibindings.md b/contributor-docs/components/multibindings.md new file mode 100644 index 00000000000..ed061657061 --- /dev/null +++ b/contributor-docs/components/multibindings.md @@ -0,0 +1,7 @@ +--- +parent: Firebase Components +--- + +# Multibindings + +TODO diff --git a/contributor-docs/how_firebase_works.md b/contributor-docs/how_firebase_works.md index cdfef8c5db2..909e6f7e1f3 100644 --- a/contributor-docs/how_firebase_works.md +++ b/contributor-docs/how_firebase_works.md @@ -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 %})). diff --git a/contributor-docs/in_depth/components.md b/contributor-docs/in_depth/components.md deleted file mode 100644 index bd2de29ebb0..00000000000 --- a/contributor-docs/in_depth/components.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -parent: In Depth ---- - -# Firebase Components - -TODO diff --git a/contributor-docs/in_depth/in_depth.md b/contributor-docs/in_depth/in_depth.md deleted file mode 100644 index f40132a00bf..00000000000 --- a/contributor-docs/in_depth/in_depth.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -has_children: true ---- - -# In Depth