Circuit Integration¶
Metro includes built-in support for Circuit, a Compose-first architecture for building kotlin apps. This integration generates Presenter.Factory and Ui.Factory implementations from @CircuitInject-annotated classes and functions, similar to Circuit’s existing KSP code generator but running entirely within Metro’s compiler plugin. These factories then contribute into Set<Presenter.Factory> and Set<Presenter.Factory multibindings.
Setup¶
Enable Circuit codegen in your Gradle build:
metro {
enableCircuitCodegen.set(true)
}
This requires the Circuit runtime libraries on your classpath. The circuit-runtime-presenter and circuit-runtime-ui artifacts are optional — you can use presenter-only or UI-only modules. This will also add the circuit-codegen-annotations artifact to your implementation classpath.
This is only compatible with Kotlin 2.3.20+ as it requires support for generating top-level declarations in FIR.
This will likely eventually move to a separate artifact.
Usage¶
Class-based Presenters and UIs¶
Annotate your Presenter or Ui implementation with @CircuitInject:
@CircuitInject(HomeScreen::class, AppScope::class)
@Inject
class HomePresenter(
private val repository: UserRepository,
) : Presenter<HomeState> {
@Composable
override fun present(): HomeState {
// ...
}
}
Metro generates a Presenter.Factory (or Ui.Factory) that:
- Is annotated with
@Injectand@ContributesIntoSet(scope) - Has a constructor that accepts a
Provider<HomePresenter> - Implements
create()with screen matching and delegation to the provider
Function-based Presenters and UIs¶
Annotate a top-level @Composable function:
@CircuitInject(HomeScreen::class, AppScope::class)
@Inject
@Composable
fun HomePresenter(
screen: HomeScreen, // Circuit-provided
navigator: Navigator, // Circuit-provided
repository: UserRepository, // Injected as Provider<UserRepository>
): HomeState {
// ...
}
Metro generates a factory class whose constructor accepts Provider-wrapped parameters for injected dependencies. At create() time, providers are invoked once (outside the composition) and passed to the function body along with any Circuit-provided parameters.
UI functions return Unit and must have a Modifier parameter:
@CircuitInject(HomeScreen::class, AppScope::class)
@Inject
@Composable
fun HomeUi(
state: HomeState, // Circuit-provided
modifier: Modifier, // Circuit-provided
analytics: Analytics, // Injected
) {
// ...
}
Assisted Injection¶
For presenters/UIs that need assisted injection (e.g., receiving a Navigator as an assisted parameter):
@AssistedInject
class FavoritesPresenter(
@Assisted private val navigator: Navigator,
private val repository: FavoritesRepository,
) : Presenter<FavoritesState> {
@CircuitInject(FavoritesScreen::class, AppScope::class)
@AssistedFactory
fun interface Factory {
fun create(@Assisted navigator: Navigator): FavoritesPresenter
}
@Composable
override fun present(): FavoritesState { /* ... */ }
}
The generated Circuit factory automatically bridges Circuit’s Presenter.Factory.create(screen, navigator, context) to your @AssistedFactory’s create() method by matching parameters by name. In the example above, navigator from Circuit’s create() is passed through to Factory.create(navigator) automatically.
Important:
- The
@CircuitInjectannotation goes on the@AssistedFactoryinterface, not the class itself. - The
@AssistedFactorymust be nested inside the targetPresenter/Uiclass. - The assisted parameters on your factory’s
create()method must be circuit-provided parameters (e.g.,Navigator,Screen). Custom assisted parameters that aren’t circuit-provided types are not supported — the generated factory has no way to obtain them at runtime since only Circuit’screate()parameters are available.
Circuit-Provided Parameters¶
Some parameter types are provided by Circuit at runtime and should not be injected:
| Parameter Type | Available To |
|---|---|
Screen (and subtypes) |
Presenter, UI |
Navigator |
Presenter only |
CircuitUiState (and subtypes) |
UI only |
Modifier |
UI only |
All other parameter types are treated as injected dependencies and wrapped in Provider<T> on the generated factory’s constructor.
Parameters already wrapped in Provider<T>, Lazy<T>, or function types are passed through as-is without additional wrapping.
CircuitContext is intentionally excluded from the circuit-provided set. It is a factory-level concept and should not be accepted by presenters or UIs.
Validation¶
The compiler plugin validates @CircuitInject usage for common usage errors.
Notes¶
- Top-level
@AssistedFactorywith@CircuitInjectis not supported — the factory must be nested inside the targetPresenter/Uiclass. This is enforced by the compiler. expectdeclarations with@CircuitInjectare skipped. Onlyactualdeclarations are processed. You must annotate theactualdeclaration (too). kotlinc requires this symmetry as well.