Skip to content

Presenter

The core Presenter interface is this:

interface Presenter<UiState : CircuitUiState> {
  @Composable fun present(): UiState
}

Presenters are solely intended to be business logic for your UI and a translation layer in front of your data layers. They are generally Dagger-injected types as the data layers they interpret are usually coming from the DI graph. In simple cases, they can be typed as a simple @Composable presenter function allowing Circuit code gen to generate the corresponding interface and factory for you.

A very simple presenter can look like this:

class FavoritesPresenter(...) : Presenter<State> {
  @Composable override fun present(): State {
    var favorites by remember { mutableStateOf(<initial>) }

    return State(favorites) { event -> ... }
  }
}

In this example, the present() function simply computes a state and returns it. If it has UI events to handle, an eventSink: (Event) -> Unit property should be exposed in the State type it returns.

With DI, the above example becomes something more like this:

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, context: CircuitContext): FavoritesPresenter
  }
}

Assisted injection allows passing on the screen and navigator from the relevant Presenter.Factory to this presenter for further reference.

When dealing with nested presenters, a presenter could bypass implementing a class entirely by simply being written as a function that other presenters can use.

// From cashapp/molecule's README examples
@Composable
fun ProfilePresenter(
  userFlow: Flow<User>,
  balanceFlow: Flow<Long>,
): ProfileModel {
  val user by userFlow.collectAsState(null)
  val balance by balanceFlow.collectAsState(0L)

  return if (user == null) {
    Loading
  } else {
    Data(user.name, balance)
  }
}

Presenters can present other presenters by injecting their assisted factories/providers, but note that this makes them a composite presenter that is now assuming responsibility for managing state of multiple nested presenters.

Retention

There are three types of composable retention functions used in Circuit.

  1. remember – from Compose, remembers a value across recompositions. Can be any type.
  2. rememberRetained – custom, remembers a value across recompositions and configuration changes. Can be any type, but should not retain leak-able things like Navigator instances or Context instances. Backed by a hidden ViewModel on Android. Note that this is not necessary in most cases if handling configuration changes yourself via android:configChanges.
  3. rememberSaveable – from Compose, remembers a value across recompositions, configuration changes, and process death. Must be Parcelable or implement a custom Saver, should not retain leakable things like Navigator instances or Context instances. Backed by the framework saved instance state system.

Developers should use the right tool accordingly depending on their use case. Consider these three examples.

The first one will preserve the count value across recompositions, but not configuration changes or process death.

@Composable
fun CounterPresenter(): CounterState {
  var count by remember { mutableStateOf(0) }

  return CounterState(count) { event ->
    when (event) {
      is CounterEvent.Increment -> count++
      is CounterEvent.Decrement -> count--
    }
  }
}

The second one will preserve the state across recompositions and configuration changes, but not process death.

@Composable
fun CounterPresenter(): CounterState {
  var count by rememberRetained { mutableStateOf(0) }

  return CounterState(count) { event ->
    when (event) {
      is CounterEvent.Increment -> count++
      is CounterEvent.Decrement -> count--
    }
  }
}

The third case will preserve the count state across recompositions, configuration changes, and process death. However, it only works with primitives or Parcelable state types.

@Composable
fun CounterPresenter(): CounterState {
  var count by rememberSaveable { mutableStateOf(0) }

  return CounterState(count) { event ->
    when (event) {
      is CounterEvent.Increment -> count++
      is CounterEvent.Decrement -> count--
    }
  }
}