Skip to content

Code Generation

If using Dagger and Anvil or Hilt, Circuit offers a KSP-based code gen solution to ease boilerplate around generating factories.

Installation

plugins {
  id("com.google.devtools.ksp")
}

dependencies {
  api("com.slack.circuit:circuit-codegen-annotations:<version>")
  ksp("com.slack.circuit:circuit-codegen:<version>")
}

Note that Anvil is enabled by default. If you are using Hilt, you must specify the mode as a KSP arg.

ksp {
  arg("circuit.codegen.mode", "hilt")
}

If using Kotlin multiplatform with typealias annotations for Dagger annotations (i.e. expect annotations in common with actual typealias declarations in JVM source sets), you can match on just annotation short names alone to support this case via circuit.codegen.lenient mode.

ksp {
  arg("circuit.codegen.lenient", "true")
}

Usage

The primary entry point is the CircuitInject annotation.

This annotation is used to mark a UI or presenter class or function for code generation. When annotated, the type’s corresponding factory will be generated and keyed with the defined screen.

The generated factories are then contributed to Anvil via ContributesMultibinding and scoped with the provided scope key.

Classes

Presenter and Ui classes can be annotated and have their corresponding Presenter.Factory or Ui.Factory classes generated for them.

Presenter

@CircuitInject(HomeScreen::class, AppScope::class)
class HomePresenter @Inject constructor(...) : Presenter<HomeState> { ... }

// Generates
@ContributesMultibinding(AppScope::class)
class HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }

UI

@CircuitInject(HomeScreen::class, AppScope::class)
class HomeUi @Inject constructor(...) : Ui<HomeState> { ... }

// Generates
@ContributesMultibinding(AppScope::class)
class HomeUiFactory @Inject constructor() : Ui.Factory { ... }

Functions

Simple functions can be annotated and have a corresponding Presenter.Factory generated. This is primarily useful for simple cases where a class is just technical tedium.

Requirements - Presenter function names must end in Presenter, otherwise they will be treated as UI functions. - Presenter functions must return a CircuitUiState type. - UI functions can optionally accept a CircuitUiState type as a parameter, but it is not required. - UI functions must return Unit. - Both presenter and UI functions must be Composable.

Presenter

@CircuitInject(HomeScreen::class, AppScope::class)
@Composable
fun HomePresenter(): HomeState { ... }

// Generates
@ContributesMultibinding(AppScope::class)
class HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }

UI

@CircuitInject(HomeScreen::class, AppScope::class)
@Composable
fun Home(state: HomeState) { ... }
*
// Generates
@ContributesMultibinding(AppScope::class)
class HomeUiFactory @Inject constructor() : Ui.Factory { ... }

Assisted injection

Any type that is offered in Presenter.Factory and Ui.Factory can be offered as an assisted injection to types using Dagger AssistedInject. For these cases, the AssistedFactory -annotated interface should be annotated with CircuitInject instead of the enclosing class.

Types available for assisted injection are:

  • Screen – the screen key used to create the Presenter or Ui.
  • Navigator – (presenters only)
  • Circuit

Each should only be defined at-most once.

Examples

// Function example
@CircuitInject(HomeScreen::class, AppScope::class)
@Composable
fun HomePresenter(screen: Screen, navigator: Navigator): HomeState { ... }

// Class example
class HomePresenter @AssistedInject constructor(
  @Assisted screen: Screen,
  @Assisted navigator: Navigator,
  ...
) : Presenter<HomeState> {
  // ...
  @CircuitInject(HomeScreen::class, AppScope::class)
  @AssistedFactory
  fun interface Factory {
    fun create(screen: Screen, navigator: Navigator, context: CircuitContext): HomePresenter
  }
}