present
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)