Dependency Graphs¶
The primary entry points in Metro are dependency graphs. These are interfaces annotated with @DependencyGraph
and created with @DependencyGraph.Factory
interfaces. Graphs expose types from the object graph via accessor properties or functions.
Tip
These are synonymous with components and @Component
/@Component.Factory
in Dagger and kotlin-inject.
Tip
“Accessors” in Metro are synonymous with Dagger’s provision methods.
Accessors and member injections act as roots, from which the dependency graph is resolved. Dependencies can be provided via conventional @Provides
functions in graphs or their supertypes, constructor-injected classes, or accessed from graph dependencies.
@DependencyGraph
interface AppGraph {
val message: String
@Provides
fun provideMessage(): String = "Hello, world!"
}
Note the @Provides
function must define an explicit return type.
Simple graphs like this can be created via the createGraph()
intrinsic.
val graph = createGraph<AppGraph>()
Graphs are relatively cheap and should be used freely.
Inputs¶
Runtime inputs can be provided via a @DependencyGraph.Factory
interface that returns the target graph. These parameters must be annotated with exactly one of @Provides
, @Includes
, or @Extends
.
Provides¶
The simplest input is an instance parameter annotated with @Provides
. This provides this instance as an available binding on the graph.
@DependencyGraph
interface AppGraph {
val message: String
@DependencyGraph.Factory
fun interface Factory {
fun create(@Provides message: String): AppGraph
}
}
Provided parameters may be any type.
Tip
This is analogous to Dagger’s @BindsInstance
.
Includes¶
@Includes
-annotated parameters are treated as containers of available bindings. Metro will treat accessors of these types as usable dependencies.
They are commonly other graph types whose’ dependencies you want to consume via explicit API.
@DependencyGraph
interface AppGraph {
val message: String
@DependencyGraph.Factory
fun interface Factory {
fun create(@Includes messageGraph: MessageGraph): AppGraph
}
@DependencyGraph interface MessageGraph {
val message: String
@Provides fun provideMessage(): String = "Hello, world!"
}
}
@Includes
instance dependencies do not need to be other graphs though! They can be any regular class type. They cannot be enums or annotation classes.
@DependencyGraph
interface AppGraph {
val message: String
@DependencyGraph.Factory
fun interface Factory {
fun create(@Includes messageProvider: MessageProvider): AppGraph
}
interface MessageProvider {
val message: String
}
}
Warning
Includes parameters cannot be injected from the graph.
Extends¶
@Extends
-annotated parameters are for extending parent graphs. See Graph Extensions at the bottom of this doc for more information.
Creating factories¶
Graph factories can be created with the createGraphFactory()
intrinsic.
val messageGraph =
createGraphFactory<AppGraph.Factory>()
.create("Hello, world!")
Scoping¶
See Scopes for more details on scopes!
Graphs may declare a scope
(and optionally additionalScopes
if there are more). Each of these declared scopes act as an implicit @SingleIn
representation of that scope for aggregation.
For example:
@DependencyGraph(AppScope::class)
interface AppGraph
Is functionally equivalent to writing the below.
@SingleIn(AppScope::class)
@DependencyGraph(AppScope::class)
interface AppGraph
Graph Extensions¶
Dependency graphs can be marked as extendable to allow child graphs to extend them. These are similar in functionality to Dagger’s Subcomponents
but are detached in nature like in kotlin-inject.
A graph must opt itself into extension in via @DependencyGraph(..., isExtendable = true)
, which will make the Metro compiler generate extra metadata for downstream child graphs.
Then, a child graph can add an @Extends
-annotated parameter to its creator to extend that graph.
@DependencyGraph(isExtendable = true)
interface AppGraph {
@Provides fun provideHttpClient(): HttpClient { ... }
}
@DependencyGraph
interface UserGraph {
@DependencyGraph.Factory
fun interface Factory {
fun create(@Extends appGraph: AppGraph): UserGraph
}
}
Child graphs then contain a superset of bindings they can inject, including both their bindings and their parents’. Graph extensions can be chained as well.
Child graphs also implicitly inherit their parents’ scopes.
Contributed Graph Extensions¶
@ContributesGraphExtension
is a specialized type of graph that is contributed to some parent scope. Its generation is deferred until the parent graph interface is merged.
The Problem¶
Imagine this module dependency tree:
:app
/ \
v v
:login :user-data
:app
defines the main dependency graph with @DependencyGraph
. The :login
module defines a graph extension for authenticated user flows, and :user-data
provides some core functionality like UserRepository
.
If :login
defines its own graph directly with @DependencyGraph
, it won’t see contributions from :user-data
unless :login
depends on it directly.
The Solution¶
Instead, :login
can use @ContributesGraphExtension(LoggedInScope::class)
+ an associated @ContributesGraphExtension.Factory(AppScope::class)
to say: “I want to contribute a new graph extension to a future AppScope
parent graph.”
The graph extension will then be generated in :app
, which already depends on both :login
and :user-data
. Now UserRepository
can be injected in LoggedInGraph
.
@ContributesGraphExtension(LoggedInScope::class)
interface LoggedInGraph {
val userRepository: UserRepository
@ContributesGraphExtension.Factory(AppScope::class)
interface Factory {
fun createLoggedInGraph(): LoggedInGraph
}
}
In the :app
module:
@DependencyGraph(AppScope::class, isExtendable = true)
interface AppGraph
The generated code will modify AppGraph
to implement LoggedInGraph.Factory
and implement createLoggedInGraph()
using a generated final $$ContributedLoggedInGraph
class that includes all contributed bindings, including UserRepository
from :user-data
.
interface AppGraph
// modifications generated during compile-time
: LoggedInGraph.Factory {
override fun createLoggedInGraph(): LoggedInGraph {
return $$ContributedLoggedInGraph(this)
}
// Generated in IR
@DependencyGraph(LoggedInScope::class)
class LoggedInGraph$$MetroGraph(appGraph: AppGraph) : LoggedInGraph {
// ...
}
}
Finally, you can obtain a LoggedInGraph
instance from AppGraph
since it now implements LoggedInGraph.Factory
:
// Using the asContribution() intrinsic
val loggedInGraph = appGraph.asContribution<LoggedInGraph.Factory>().createLoggedInGraph()
// Or if you have IDE support enabled
val loggedInGraph = appGraph.createLoggedInGraph()
Graph arguments¶
You can pass arguments to the graph via the factory:
@ContributesGraphExtension.Factory(AppScope::class)
interface Factory {
fun create(@Provides userId: String): LoggedInGraph
}
This maps to:
// Generated in IR
@DependencyGraph(LoggedInScope::class)
class $$ContributedLoggedInGraph(
@Extends parent: AppGraph,
@Provides userId: String
): LoggedInGraph {
// ...
}
In AppGraph
, the generated factory method looks like:
// Generated in IR
override fun create(userId: String): LoggedInGraph {
return LoggedInGraph$$MetroGraph(this, userId)
}
Warning
Abstract factory classes cannot be used as graph contributions.
Contributed graphs may also be chained, but note that @ContributesGraphExtension.isExtendable
must be true to do so!
Implementation Notes¶
Dependency graph code gen is designed to largely match how Dagger components are generated.
- Internal graph validation uses Tarjan’s algorithm + topological sort implementation.
- This runs in O(V+E) time
- The returned ordered list of bindings can be used to determine provider field generation order.
- Any binding whose order depends on one later in the returned order implicitly requires use of
DelegateFactory
. - Dependencies are traversed from public accessors and
inject()
functions. - Metro generates Provider Factory classes for each provider. These should be generated at the same time that the provider is compiled so that their factory classes. This is for two primary purposes:
- They can be reused to avoid code duplication
- Metro can copy default values for provider values over to the generated factory to support optional bindings. Since default values may refer to private references, we must generate these factories as nested classes.
- Metro generates a graph impl class that holds all aggregated bindings and manages scoping.
- Scoped bindings are stored in provider fields backed by
DoubleCheck
. - Reused unscoped providers instances are stored in reusable fields.
@Provides
factory parameters are stored in a field backed byInstanceFactory
.- Multibindings create new collection instances every time.
- Multibinding providers are not accessible as standalone bindings.
- Graph extensions are implemented via a combination of things
- Custom
MetroMetadata
is generated and serialized into Kotlin’sMetadata
annotations. - Extendable parent graphs opt-in to generating this metadata. They write information about their available provider and instance fields, binds callable IDs, parent graphs, and provides callable IDs.
- Extendable parent graphs generate
_metroAccessor
-suffixedinternal
functions that expose instance fields and provider fields. - Child graphs read this metadata and look up the relevant callable symbols, then incorporating these when building its own binding graph.
- Custom