FirebaseCore has a dependency injection system (referred to as "Interop") used to depend on functionalities provided by other Firebase products (specifically, the frameworks that offer those products). This gives the ability to depend on a typesafe interface-only API to consume without depending on the entire product and simulates optional dependencies - depending on the definition but not the product itself and only functioning when the product implementing that definition is included.
- Overview
- Protocol Only Frameworks
- Types and Core API
- Registering with Core
- Singletons and Instance Management
- Single Instance per
FIRApp
- Framework does not provide functionality (example: Functions)
- Framework provides functionality to other Frameworks (example: Auth)
- Multiple Instances per FIRApp
- Single Instance per
- Depending on Functionality from Another Framework
- Singletons and Instance Management
- Advanced Use Cases
When a Firebase framework wants to provide functionality to another Firebase framework, it must be
done through the Interop system. Both frameworks depend on a shared protocol in the Interop folder
that describes the functionality provided by one framework 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 frameworks to depend on each other in a typesafe way and allows us to explicitly declare version dependencies on the interfaces required instead of the product's version.
In order to share protocols between two frameworks, we introduced headers that declare the desired protocol(s).
Both the implementing and dependent framework will import the
<ProductName>Interop
headers: the implementing framework must conform to the protocols defined
and register it with Core, while the dependent framework will use the protocol definition to use
methods defined by it.
An Interop folder can have multiple protocols, but all should be implemented by the product 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.
For the rest of the documentation, it's important to be familiar with the various classes and API
provided by Core. Since the frameworks 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:]
- A dependency on a specific protocol's functionality. Created with the factory method
@class FIRComponent
- A component to register with Core to be consumed by other frameworks. 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.
#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.
@protocol FIRLibrary
- Describes functionality for frameworks registering components in the
FIRComponentContainer
as well as other Core configuration functionality. It allows Core to fetch components lazily from the implementing framework.
- Describes functionality for frameworks registering components in the
Each Firebase framework should register with Core in the +load
method of the class conforming to
FIRLibrary
. 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/Extension/FirebaseCoreInternal.h"
@interface FIRFoo <FIRLibrary>
@end
@implementation FIRFoo
+ (void)load {
// Register with Core as a library. The version should be fetched from a constant defined
// elsewhere, but that's not covered or relevant for this example.
[FIRApp registerInternalLibrary:self
withName:@"fire-foo"
withVersion:@"1.0.0"];
}
// TODO: Conform to `FIRLibrary`. See later sections for more information.
@end
All Firebase frameworks provide singleton access for convenience that map to a specific FIRApp
:
[FIRAuth auth]
, [FIRFunctions functionsForApp:]
, etc. Some frameworks 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 product's offerings.
The registration for a single instance per FIRApp
changes if the framwork provides functionality
to other frameworks or not.
In this case, the framework is a "leaf node" since no other frameworks depend on functionality from it. It has a private, empty protocol that it uses to register with the container. Using Functions as an example:
// FIRFunctions.m
/// Empty protocol to register Functions as a component with Core.
@protocol FIRFunctionsInstanceProvider
@end
/// Privately conform to the protocol for component registration.
@interface FIRFunctions () <FIRFunctionsInstanceProvider, FIRLibrary>
@end
@implementation FIRFunctions
+ (void)load {
NSString *version = @"<# Fetch the version here #>";
[FIRApp registerInternalLibrary:self withName:@"fire-fun" withVersion:version];
}
/// The array of components to register with Core. Since Functions is a leaf node and
/// doesn't provide any functionality to other frameworks, 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 ];
}
// The public entry point for the SDK.
+ (FIRFunctions *)functionsForApp:(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
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 frameworks:
// 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, FIRLibrary>
@end
+ (void)load {
// Remember to register in +load!
NSString *version = @"<# Fetch the version here #>";
[FIRApp registerInternalLibrary:self withName:@"fire-auth" withVersion:version];
}
/// 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];
}
// The public entry point for the SDK.
+ (FIRAuth *)authForApp:(FIRApp *)app {
// Use the instance from the provided app's container.
id<FIRAuthInteroperable> auth = FIR_COMPONENT(FIRAuthInteroperable, app.container);
return (FIRAuth *)auth;
}
Instead of directly providing an instance from the container, Firestore and similar products 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, FIRLibrary>
/// 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 {
// Remember to register in +load!
NSString *version = @"<# Fetch the version here #>";
[FIRApp registerInternalLibrary:self withName:@"fire-fst" withVersion:version];
}
- (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...
}
// `FIRLibrary` 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];
}
If you haven't already read 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 FirebaseAuthInterop
headers to your
product's podspec source_files
attribute for CocoaPods and something
comparable for any other package manager supported. Note, for Swift Package Manager,
nothing special is needed as long as all the pods and headers are in the same repo.
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!
}
Consider a situation where a framework 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 private 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 ];
}