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)