mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
450 lines
18 KiB
Markdown
450 lines
18 KiB
Markdown
# Using Core's Component System
|
|
|
|
FirebaseCore has a dependency injection system (referred to as "Interop") used to depend on
|
|
functionalities provided by other Firebase SDKs. This gives the ability to depend on a typesafe
|
|
interface-only API to consume without depending on the entire SDK and simulates optional
|
|
dependencies - depending on the definition but not the implementing SDK and only functioning when
|
|
the implementing SDK is included.
|
|
|
|
//TODO(ryanwilson): Table of Contents
|
|
|
|
## Overview
|
|
|
|
When a Firebase SDK wants to provide functionality to another Firebase SDK, it must be done through
|
|
the Interop system. Both SDKs depend on a shared protocol that describes the functionality provided
|
|
by one SDK and required by the other. Let's use `A` and `B`, where `B` depends on functionality
|
|
provided by `A` and the functionality is described by protocol `AInterop`.
|
|
|
|
During configuration, `A` tells Core that it provides functionality for `AInterop` and `B` tells
|
|
Core it would like functionality `AInterop` (and specifies whether it is required or optional) as
|
|
well as how to instantiate an instance of `B`. When a developer requests `B`, FirebaseCore
|
|
instantiates `B` and passes a container that contains the instance of `A` that provides `AInterop`.
|
|
|
|
`B` has no idea what class `A` is, and it doesn't need to. All `B` needs to know is that it has an
|
|
instance of an object that conforms to `AInterop` and provides the functionality it needs.
|
|
|
|
This system allows Firebase SDKs to depend on each other in a typesafe way and allows us to
|
|
explicitly declare version dependencies on the interfaces required instead of the SDK version.
|
|
|
|
## Protocol Only Frameworks
|
|
|
|
In order to share protocols between two SDKs, we introduced header only frameworks that declare the
|
|
desired protocol(s).
|
|
|
|
The naming convention for the framework should be `<SDKName>Interop` but the protocols don't have a
|
|
strict naming guideline other than having a `FIR` prefix in Objective-C.
|
|
|
|
Both the implementing and dependent SDK will have a required dependency on this `<SDKName>Interop`
|
|
framework: the implementing SDK must conform to the protocols defined and register it with Core,
|
|
while the dependent SDK will use the protocol definition to use methods defined by it.
|
|
|
|
An Interop framework can have multiple protocols, but all should be implemented by the SDK it is
|
|
named after.
|
|
|
|
Protocols *can not* declare class methods. This is an intentional decision to ensure all interfaces
|
|
interact properly based on the `FIRApp` that's used.
|
|
|
|
## Types and Core API
|
|
|
|
For the rest of the documentation, it's important to be familiar with the various classes and API
|
|
provided by Core. Since the SDKs are written in Objective-C, we'll use the Objective-C names. The
|
|
Swift names are identical but dropping the `FIR` prefix.
|
|
|
|
- `@class FIRDependency`
|
|
- A dependency on a specific protocol's functionality. Created with the factory method
|
|
`[FIRDependency dependencyWithProtocol:isRequired:]`
|
|
- `@class FIRComponent`
|
|
- A component to register with Core to be consumed by other SDKs. It declares the protocol
|
|
offered, dependencies, and a block for Core to instantiate it.
|
|
- `@class FIRComponentContainer`
|
|
- A container that holds different components that are registered with Core.
|
|
- `@protocol FIRComponentRegistrant`
|
|
- Describes functionality for SDKs registering components in the `FIRComponentContainer`. It
|
|
allows Core to fetch components lazily from the SDK.
|
|
- `#define FIR_COMPONENT(protocol, container)` (macro)
|
|
- The macro to request an instance conforming to a given protocol from a container. Due to
|
|
Objective-C's lightweight generic system, the safest and most readable API is provided by a
|
|
macro that uses internal types to give compiler warnings if a developer tries to assign the
|
|
result to a variable with the incorrect type.
|
|
|
|
## Registering with Core
|
|
|
|
Each Firebase SDK should register with Core in the `+load` method of the class conforming to
|
|
`FIRComponentRegistrant`. This needs to happen at `+load` time because Core needs to resolve any
|
|
dependencies before a class has a chance to be called by a developer (if called at all).
|
|
|
|
```
|
|
#import <FirebaseCore/FIRComponentContainer.h>
|
|
#import <FirebaseCore/FIRComponentRegistrant.h>
|
|
|
|
@interface FIRFoo <FIRComponentRegistrant>
|
|
@end
|
|
|
|
@implementation FIRFoo
|
|
|
|
+ (void)load {
|
|
// Register with Core's container.
|
|
[FIRComponentContainer registerAsComponentRegistrant:self];
|
|
}
|
|
|
|
// TODO: Conform to `FIRComponentRegistrant`. See later sections for more information.
|
|
|
|
@end
|
|
```
|
|
|
|
### Singletons and Instance Management
|
|
|
|
All Firebase SDKs provide singleton access for convenience that map to a specific `FIRApp`:
|
|
`[FIRAuth auth]`, `[FIRFunctions functionsForApp:]`, etc. Some SDKs can also have multiple instances
|
|
per `FIRApp` such as Storage: `[FIRStorage storageForApp:URL:]`.
|
|
|
|
These instances must be created and managed by Core through the component system. This allows the
|
|
`FIRApp` lifecycle to control the lifecycle of instances associated with itself. There are different
|
|
ways to do so depending on the SDK's situation.
|
|
|
|
#### Single Instance per `FIRApp`
|
|
|
|
The registration for a single instance per `FIRApp` changes if the SDK provides functionality to
|
|
other SDKs or not.
|
|
|
|
##### SDK does not provide functionality (example: Functions)
|
|
|
|
In this case, the SDK is a "leaf node" since no SDKs depend on it. It has a private, empty protocol
|
|
that it uses to register with the container. Using Functions as an example:
|
|
|
|
```
|
|
// FIRFunctions.h
|
|
|
|
/// Empty protocol to register Functions as a component with Core.
|
|
@protocol FIRFunctionsInstanceProvider
|
|
@end
|
|
|
|
/// Privately conform to the protocol for component registration.
|
|
@interface FIRFunctions () <FIRFunctionsInstanceProvider, FIRComponentRegistrant>
|
|
@end
|
|
|
|
@implementation FIRFunctions
|
|
|
|
+ (void)load {
|
|
[FIRComponentContainer registerAsComponentRegistrant:self];
|
|
}
|
|
|
|
/// The array of components to register with Core. Since Functions is a leaf node and
|
|
/// doesn't provide any functionality to other SDKs, it should use Core for instance
|
|
/// management only.
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
// Each component needs a block for Core to call in order to instantiate instances of the
|
|
// desired class.
|
|
FIRComponentCreationBlock creationBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
// We want the same instance to be returned when requested from the container, enable
|
|
// `isCacheable`.
|
|
*isCacheable = YES;
|
|
|
|
// Use an appropriate initializer and inject anything required from the container.
|
|
return [[FIRFunctions alloc] initWithApp:container.app];
|
|
};
|
|
|
|
// Create the component that can create instances of `FIRFunctions`.
|
|
FIRComponent *internalProvider =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
|
|
creationBlock:creationBlock];
|
|
|
|
// Return the array of components, in this case only the internal instance provider.
|
|
return @[ internalProvider ];
|
|
}
|
|
|
|
+ (FIRFunctions *)functionsWithApp:(FIRApp *)app {
|
|
// Get the instance from the `FIRApp`'s container. This will create a new instance the
|
|
// first time it is called, and since `isCacheable` is set in the component creation
|
|
// block, it will return the existing instance on subsequent calls.
|
|
id<FIRFunctionsInstanceProvider> instance =
|
|
FIR_COMPONENT(FIRFunctionsInstanceProvider, app.container);
|
|
|
|
// In the component creation block, we return an instance of `FIRFunctions`. Cast it and
|
|
// return it.
|
|
return (FIRFunctions *)instance;
|
|
}
|
|
|
|
// ... Other `FIRFunctions` methods.
|
|
|
|
@end
|
|
```
|
|
|
|
##### SDK provides functionality to other SDKs (example: Auth)
|
|
|
|
This example will be very similar to the one above, but let's define a simple protocol that Auth
|
|
could conform to and provide to other SDKs:
|
|
|
|
```
|
|
// FIRAuthInterop.h in the FirebaseAuthInterop framework.
|
|
|
|
@protocol FIRAuthInterop
|
|
/// Get the current Auth user's UID. Returns nil if there is no user signed in.
|
|
- (nullable NSString *)getUserID;
|
|
@end
|
|
```
|
|
|
|
```
|
|
// FIRAuth.m in the FirebaseAuth framework.
|
|
|
|
/// Privately conform to the protocol for interop and component registration.
|
|
@interface FIRAuth () <FIRAuthInteroperable, FIRComponentRegistrant>
|
|
@end
|
|
|
|
/// The components to register with Core.
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
// Provide a component that will return an instance of `FIRAuth`.
|
|
FIRComponentCreationBlock authCreationBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
// Cache so the same `FIRAuth` instance is returned each time.
|
|
*isCacheable = YES;
|
|
return [[FIRAuth alloc] initWithApp:container.app];
|
|
};
|
|
FIRComponent *authInterop =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRAuthInteroperable)
|
|
creationBlock:authCreationBlock];
|
|
return @[authInterop];
|
|
}
|
|
|
|
+ (FIRAuth *)authWithApp:(FIRApp *)app {
|
|
// Use the instance from the provided app's container.
|
|
id<FIRAuthInteroperable> auth = FIR_COMPONENT(FIRAuthInteroperable, app.container);
|
|
return (FIRAuth *)auth;
|
|
}
|
|
```
|
|
|
|
#### Multiple Instances per `FIRApp`
|
|
|
|
Instead of directly providing an instance from the container, Firestore and similar SDKs should
|
|
create a "provider" that stores and creates instances with the required parameters. This means a
|
|
single provider per `FIRApp`, but multiple instances are possible per provider.
|
|
|
|
```
|
|
/// Provider protocol to register with Core.
|
|
@protocol FSTFirestoreMultiDBProvider
|
|
|
|
/// Cached instances of Firestore objects.
|
|
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRFirestore *> *instances;
|
|
|
|
/// Firestore can be initialized with an app as well as a database. The instance provider is already
|
|
/// associated with a `FIRApp` so pass in any other required parameters (in this case, just the
|
|
/// database string).
|
|
- (FIRFirestore *)firestoreForDatabase:(NSString *)database;
|
|
|
|
@end
|
|
```
|
|
|
|
Instead of the Firestore class conforming to `FSTInstanceProvider`, the work can be done in a
|
|
separate class to keep `Firestore.m` cleaner.
|
|
|
|
```
|
|
/// A concrete implementation for FSTFirestoreMultiDBProvider to create Firestore instances.
|
|
@interface FSTFirestoreComponent : NSObject <FSTFirestoreMultiDBProvider,
|
|
FIRComponentRegistrant>
|
|
|
|
/// The `FIRApp` that instances will be set up with.
|
|
@property(nonatomic, weak, readonly) FIRApp *app;
|
|
|
|
/// Cached instances of Firestore objects.
|
|
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRFirestore *> *instances;
|
|
|
|
/// Default method for retrieving a Firestore instance, or creating one if it doesn't exist.
|
|
- (FIRFirestore *)firestoreForDatabase:(NSString *)database;
|
|
|
|
/// Default initializer.
|
|
- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER;
|
|
- (instancetype)init NS_UNAVAILABLE;
|
|
@end
|
|
|
|
@implementation FSTFirestoreInstanceProvider
|
|
|
|
// Explicitly @synthesize because instances is part of the FSTInstanceProvider protocol.
|
|
@synthesize instances = _instances;
|
|
|
|
+ (void)load {
|
|
// Don't forget to register!
|
|
[FIRComponentContainer registerAsComponentRegistrant:self];
|
|
}
|
|
|
|
- (instancetype)initWithApp:(FIRApp *)app {
|
|
self = [super init];
|
|
if (self) {
|
|
_instances = [[NSMutableDictionary alloc] init];
|
|
_app = app;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
/// `FSTFirestoreMultiDBProvider` conformance.
|
|
- (FIRFirestore *)firestoreForDatabase:(NSString *)database {
|
|
// Regular initialization code to create Firestore instances with required parameters...
|
|
}
|
|
|
|
// `FIRComponentRegistrant` conformance.
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
// Ignore any dependencies for simplicity in this example.
|
|
FIRComponentCreationBlock creationBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
*isCacheable = YES;
|
|
|
|
// NOTE: Instead of returning an instance of Firestore, return an instance of the
|
|
// instance provider.
|
|
return [[FIRFirestoreComponent alloc] initWithApp:container.app];
|
|
};
|
|
FIRComponent *firestoreProvider =
|
|
[FIRComponent componentWithProtocol:@protocol(FSTFirestoreMultiDBProvider)
|
|
creationBlock:creationBlock];
|
|
return @[ firestoreProvider ];
|
|
}
|
|
|
|
@end
|
|
```
|
|
|
|
All `Firestore.m` needs to do now is call the component container from the singleton calls:
|
|
|
|
```
|
|
+ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database {
|
|
id<FSTFirestoreMultiDBProvider> provider =
|
|
FIR_COMPONENT(FSTFirestoreMultiDBProvider, app.container);
|
|
return [provider firestoreForDatabase:database];
|
|
}
|
|
```
|
|
|
|
### Depending on Functionality from Another SDK
|
|
|
|
*If you haven't already read [Registering with Core](#registering-with-core), please do so until you
|
|
get back to this spot as it lays the groundwork necessary to understand this section.*
|
|
|
|
Adding dependencies is easy once components are registered with Core. Let's take the example from
|
|
Functions above and add a dependency to `FIRAuthInterop` defined above.
|
|
|
|
**Important**: You will also need to add a dependency on the `FirebaseAuthInterop` pod to your SDK's
|
|
podspec and any package manager supported.
|
|
|
|
Before adding the dependency on `FIRAuthInterop`.
|
|
|
|
```
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
FIRComponentCreationBlock creationBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
*isCacheable = YES;
|
|
return [[FIRFunctions alloc] initWithApp:container.app];
|
|
};
|
|
|
|
FIRComponent *internalProvider =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
|
|
creationBlock:creationBlock];
|
|
|
|
return @[ internalProvider ];
|
|
}
|
|
```
|
|
|
|
After adding the dependency on `FIRAuthInterop`. See comments with "ADDED:".
|
|
|
|
```
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
FIRComponentCreationBlock creationBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
*isCacheable = YES;
|
|
|
|
// ADDED: Retrieve an instance that conforms to `FIRAuthInterop` from the container.
|
|
id<FIRAuthInterop> auth = FIR_COMPONENT(FIRAuthInterop, container);
|
|
|
|
// ADDED: Note the constructor has a new parameter: auth. It's good practice to inject
|
|
// the instance needed in the constructor instead of pulling it from the app
|
|
// passed in. This allows for better unit testing with fakes since any object
|
|
// can conform to `FIRAuthInterop` and be verified easily.
|
|
return [[FIRFunctions alloc] initWithApp:container.app auth:auth];
|
|
};
|
|
|
|
// ADDED: Define a dependency on the `FIRAuthInteroperable` protocol. Declare if the
|
|
// dependency is required or not.
|
|
FIRDependency *auth =
|
|
[FIRDependency dependencyWithProtocol:@protocol(FIRAuthInteroperable)
|
|
isRequired:NO];
|
|
|
|
// ADDED: A longer constructor is used to instantiate the `FIRComponent`; this time
|
|
// it includes instantiation timing and an array of dependencies. The timing
|
|
// allows components to be initialized upon configure time or lazily, when
|
|
// it is requested from the container. Pass in the `auth` dependency created
|
|
// above.
|
|
FIRComponent *internalProvider =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
|
|
instantiationTiming:FIRInstantiationTimingLazy
|
|
dependencies:@[ auth ]
|
|
creationBlock:creationBlock];
|
|
|
|
return @[ internalProvider ];
|
|
}
|
|
```
|
|
|
|
Based on the new constructor, Functions can now use the `auth` instance as defined by the
|
|
protocol:
|
|
|
|
```
|
|
NSString *userID = [auth getUserID];
|
|
if (userID) {
|
|
// Auth is available and a user is signed in!
|
|
}
|
|
```
|
|
|
|
## Advanced Use Cases
|
|
|
|
### Providing Multiple Components and Sharing Instances
|
|
|
|
Consider a situation where an SDK wants to offer functionality defined in multiple protocols
|
|
with the same instance. For example, Auth could provide `FIRAuthUserInterop` and
|
|
`FIRAuthSignInInterop`. If a single Auth instance should be shared between those two protocols, the
|
|
system currently doesn't work.
|
|
|
|
In order to alleviate this, Auth could create a third (internal only) protocol
|
|
(`FIRAuthCombinedInterop`) that conforms to both `FIRAuthUserInterop` and `FIRAuthSignInInterop` and
|
|
becomes a dependency for each of those two components and returned in the component creation block.
|
|
An abbreviated code sample:
|
|
|
|
```
|
|
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
// Standard creation block to get an instance of Auth.
|
|
FIRComponentCreationBlock authBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
*isCacheable = YES;
|
|
return [[FIRAuth alloc] initWithApp:container.app];
|
|
};
|
|
|
|
FIRComponentCreationBlock combinedBlock =
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
// No need to cache, let it use the cached value from the combined component.
|
|
return FIR_COMPONENT(FIRAuthCombinedInterop, container);
|
|
};
|
|
|
|
// Declare a self dependency on the combined interop component.
|
|
FIRDependency *auth =
|
|
[FIRDependency dependencyWithProtocol:@protocol(FIRAuthCombinedInterop)
|
|
isRequired:YES];
|
|
|
|
// Declare the three components provided.
|
|
FIRComponent *authComponent =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRAuthCombinedInterop)
|
|
creationBlock:authBlock];
|
|
|
|
// Both the user and sign in components depend on the previous component as
|
|
// declared in the dependency above.
|
|
|
|
FIRComponent *userComponent =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRAuthUserInterop)
|
|
instantiationTiming:FIRInstantiationTimingLazy
|
|
dependencies:@[ auth ]
|
|
creationBlock:combinedBlock];
|
|
|
|
FIRComponent *signInComponent =
|
|
[FIRComponent componentWithProtocol:@protocol(FIRAuthSignInInterop)
|
|
instantiationTiming:FIRInstantiationTimingLazy
|
|
dependencies:@[ auth ]
|
|
creationBlock:combinedBlock];
|
|
|
|
return @[ authComponent, userComponent, signInComponent ];
|
|
}
|
|
```
|