present

abstract fun present(): UiState(source)

The primary Composable entry point to present a UiState. In production, a Navigator is used to automatically connect this with a corresponding Ui to render the state returned by this function.

When handling events, embed a eventSink: (Event) -> Unit property in the state as needed.

data class State(
val favorites: List<Favorite>,
eventSink: (Event) -> Unit
) : CircuitUiState

class FavoritesPresenter(...) : Presenter<State, Event> {
@Composable override fun present(): State {
// ...
return State(...) { event ->
// Handle UI events here
}
}
}

Dependency Injection

Presenters should use dependency injection, usually assisted injection to accept Navigator or Screen instances as inputs. Their corresponding assisted factories should then be used by hand-written presenter factories.

class FavoritesPresenter @AssistedInject constructor(
@Assisted private val screen: FavoritesScreen,
@Assisted private val navigator: Navigator,
private val favoritesRepository: FavoritesRepository
) : Presenter<State> {
@Composable override fun present(): State {
// ...
}

@AssistedFactory
fun interface Factory {
fun create(screen: FavoritesScreen, navigator: Navigator): FavoritesPresenter
}
}

Testing

When testing, simply drive UI events with a MutableSharedFlow use Molecule+Turbine to drive this function.

@Test
fun `emit initial state and refresh`() = runTest {
val favorites = listOf("Moose", "Reeses", "Lola")
val repository = FakeFavoritesRepository(favorites)
val presenter = FavoritesPresenter(repository)

moleculeFlow(Immediate) { presenter.present() }
.test {
assertThat(awaitItem()).isEqualTo(State.Loading)
val successState = awaitItem()
assertThat(successState).isEqualTo(State.Success(favorites))
successState.eventSink(Event.Refresh)
assertThat(awaitItem()).isEqualTo(State.Success(favorites))
}
}

Note that Circuit's test artifact has a Presenter.test() helper extension function for the above case.

@Test
fun `emit initial state and refresh`() = runTest {
val favorites = listOf("Moose", "Reeses", "Lola")
val repository = FakeFavoritesRepository(favorites)
val presenter = FavoritesPresenter(repository)

presenter.test {
assertThat(awaitItem()).isEqualTo(State.Loading)
val successState = awaitItem()
// ...
}
}

No Compose UI

Presenter logic should not emit any Compose UI. They are purely for presentation business logic. To help enforce this, present is annotated with @ComposableTarget("presenter"). This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do.

This warning does not appear in the IDE, so it's recommended to use allWarningsAsErrors in your build configuration to fail the build on this event.

// In build.gradle.kts
kotlin.compilerOptions.allWarningsAsErrors.set(true)