GraphExtension

@Target(allowedTargets = [AnnotationTarget.CLASS])
annotation class GraphExtension(val scope: KClass<*> = Nothing::class, val additionalScopes: Array<KClass<*>> = [], val excludes: Array<KClass<*>> = [], val bindingContainers: Array<KClass<*>> = [])(source)

Declares the annotated type to be a dependency graph extension. As the name implies, graph extensions extend a parent graph they are declared for and contain a superset of bindings that includes both the parent graph(s) as well as their own. These are similar in functionality to Dagger’s Subcomponent

See DependencyGraph before reading this section!

Graph extensions must be either an interface or an abstract class.

Metro's compiler plugin will build, validate, and implement this graph at compile-time when the parent graph is generated.

Graph extensions can be chained and implicitly inherit their parents’ scopes.

Creating Graphs

You cannot create a graph extension independent of its parent graph, you may only access it via accessor on the parent graph. You can declare this in multiple ways.

  • Declare an accessor on the parent graph directly.

@GraphExtension
interface LoggedInGraph

@DependencyGraph
interface AppGraph {
val loggedInGraph: LoggedInGraph
}
  • (If the extension has a creator) declare the creator on the parent graph directly.

@GraphExtension
interface LoggedInGraph {
@GraphExtension.Factory
interface Factory {
fun createLoggedInGraph(): LoggedInGraph
}
}

@DependencyGraph
interface AppGraph {
val loggedInGraphFactory: LoggedInGraph.Factory
}
  • (If the extension has a creator) make the parent graph implement the creator.

@GraphExtension
interface LoggedInGraph {
@GraphExtension.Factory
interface Factory {
fun createLoggedInGraph(): LoggedInGraph
}
}

@DependencyGraph
interface AppGraph : LoggedInGraph.Factory
  • Contribute the factory to the parent graph via ContributesTo with a scope.

@GraphExtension
interface LoggedInGraph {
@ContributesTo(AppScope::class)
@GraphExtension.Factory
interface Factory {
fun createLoggedInGraph(): LoggedInGraph
}
}

@DependencyGraph(AppScope::class)
interface AppGraph

Scoping

See Scope and DependencyGraph before reading this section!

Like DependencyGraph, graph extensions 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 example:

@GraphExtension(AppScope::class)
interface AppGraph

Is functionally equivalent to writing the below.

@SingleIn(AppScope::class)
@GraphExtension(AppScope::class)
interface AppGraph

Providers

Like DependencyGraph, graph extensions may declare providers via Provides and Binds to provide dependencies into the graph.

Creators can provide instance dependencies and other graphs as dependencies.

@GraphExtension
interface AppGraph {
val httpClient: HttpClient

@Provides fun provideHttpClient: HttpClient = HttpClient()
}

Creators

See DependencyGraph's section on creators.

Aggregation

See DependencyGraph's section on aggregation.

Contributing Graph Extensions

Graph extensions may be contributed to a parent graph and its contribution merging will be deferred until the parent graph is generated.

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 @GraphExtension(LoggedInScope::class) + contribute its factory via @ContributesTo(AppScope::class) to say: "I want to contribute a new graph extension to the app graph." The extension will be generated in :app, which already depends on both :login and :user-data. Now UserRepository can be injected in LoggedInGraph.

@GraphExtension(LoggedInScope::class)
interface LoggedInGraph {

val userRepository: UserRepository

@ContributesTo(AppScope::class)
@GraphExtension.Factory
interface Factory {
fun createLoggedInGraph(): LoggedInGraph
}
}

In the :app module:

@DependencyGraph(AppScope::class)
interface AppGraph

The generated code will modify AppGraph to implement LoggedInGraph.Factory and implement createLoggedInGraph() using a generated final LoggedInGraphImpl class that includes all contributed bindings, including UserRepository from :user-data.

// modifications generated during compile-time
interface AppGraph : LoggedInGraph.Factory {
override fun createLoggedInGraph(): LoggedInGraph {
return LoggedInGraphImpl(this)
}

// Generated in IR
class $$MetroGraph : AppGraph {
class LoggedInGraphImpl(appGraph: $$MetroGraph) : 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 contributed factory:

@ContributesTo(AppScope::class)
@GraphExtension.Factory
interface Factory {
fun create(@Provides userId: String): LoggedInGraph
}

This maps to:

// Generated in IR
@DependencyGraph(LoggedInScope::class)
class LoggedInGraphImpl(
parent: AppGraph,
@Provides userId: String
): LoggedInGraph {
// ...
}

In AppGraph, the generated factory method looks like:

// Generated in IR
override fun create(userId: String): LoggedInGraph {
return LoggedInGraphImpl(this, userId)
}

Note: Abstract factory classes cannot be used as graph contributions.

Like regular graph extensions, contributed graph extensions may also be chained.

Types

Link copied to clipboard
@Target(allowedTargets = [AnnotationTarget.CLASS])
annotation class Factory

A factory for the graph extension.

Properties

Link copied to clipboard

Additional scopes this graph extension aggregates. scope must be defined if this is defined, as this property is purely for convenience.

Link copied to clipboard

Optional list of included binding containers. See the doc on BindingContainer for more details.

Link copied to clipboard

Optional list of excluded contributing classes (requires a scope to be defined).

Link copied to clipboard
val scope: KClass<*>

The scope this graph extension aggregates.