All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.slack.circuit.runtime.presenter.Presenter.kt Maven / Gradle / Ivy

There is a newer version: 0.25.0
Show newest version
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.runtime.presenter

import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposableTarget
import androidx.compose.runtime.Stable
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.screen.Screen

/**
 * Presents a given [UiState].
 *
 * Events (if any) should be a part of the [UiState] itself as an `eventSink: (Event) -> Unit`
 * property.
 *
 * If a given [Presenter] only ever emits the same state, you can define a single value-less
 * `object` type for the state.
 *
 * @see present for more thorough documentation.
 */
@Stable
public interface Presenter {
  /**
   * 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.
   *
   * ```kotlin
   * data class State(
   *   val favorites: List,
   *   eventSink: (Event) -> Unit
   * ) : CircuitUiState
   *
   * class FavoritesPresenter(...) : Presenter {
   *   @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][Presenter.Factory].
   *
   * ```kotlin
   * class FavoritesPresenter @AssistedInject constructor(
   *   @Assisted private val screen: FavoritesScreen,
   *   @Assisted private val navigator: Navigator,
   *   private val favoritesRepository: FavoritesRepository
   * ) : Presenter {
   *   @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")][ComposableTarget]. 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.
   *
   * ```kotlin
   * // In build.gradle.kts
   * kotlin.compilerOptions.allWarningsAsErrors.set(true)
   * ```
   */
  @Composable
  // Prevent compose UI from running in presenters, these only produce state
  // The name here is a little funny, but intended to help make the warning printed a little easier
  // to understand.
  // "Calling a presenter composable function where a UI Composable composable was expected"
  @ComposableTarget("presenter")
  public fun present(): UiState

  /**
   * A factory that produces [presenters][Presenter] for a given [Screen]. `Circuit` instances use
   * the created presenter and connects it to a given `Ui` for the same [Screen].
   *
   * Factories should be simple aggregate multiple presenters for a canonical "whole screen". That
   * is to say, they should be hand-written and aggregate all the presenters responsible for the UI
   * visible within the surface this presents on.
   *
   * ## Example
   *
   * Consider this example of a Profile UI.
   *
   * ```
   *                           ┌────────────────────┐
   *                      ┌─── │                    │
   *                      │    ├────────────────────┤◄──┐
   *                      │    │ X                  │   │
   *                      │    │                    │ ProfileHeaderPresenter
   *                      │    │ Fred Rogers        │   │
   *                      │    ├────────────────────┤◄──┘
   *                      │    │ ┌───────┐  ┌────┐  │
   * ProfilePresenterFactory   │ │Message│  │Call│◄─┼─── ProfileActionsPresenter
   *                      │    │ └───────┘  └────┘  │
   *                      │    │                    │
   *                      │    │  - - - - - - - - ◄─┼────┐
   *                      │    │  - - - - - - - -   │    │
   *                      │    │  - - - - - - - -   │  ProfileDetailsPresenter
   *                      │    │  - - - - - - - - ◄─┼────┘
   *                      └─── │                    │
   *                           └────────────────────┘
   * ```
   *
   * This would be represented by the following factory implementation:
   * ```kotlin
   * class ProfilePresenter.Factory @Inject constructor(
   *   val headerPresenter: ProfilerHeaderPresenter.Factory,
   *   val actionsPresenter: ProfilerActionsPresenter.Factory,
   *   val detailsPresenter: ProfilerDetailsPresenter.Factory,
   *   val callScreenRouter: CallScreenRouter.Factory
   * ) : Presenter.Factory {
   *   override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*, *>? {
   *     return when (screen) {
   *       is ProfileHeader -> headerPresenter.create(screen)
   *       is ProfileActions -> actionsPresenter.create(screen, callScreenRouter.create(navigator))
   *       is ProfileDetails -> detailsPresenter.create(screen)
   *       else -> null
   *     }
   *   }
   * }
   * ```
   */
  // Diagram generated from asciiflow: https://shorturl.at/fgjtA
  @Stable
  public fun interface Factory {
    /**
     * Creates a [Presenter] for the given [screen] if it can handle it, or returns null if it
     * cannot handle the given [screen].
     */
    public fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>?
  }
}

/**
 * Due to this bug in Studio, we can't write lambda impls of [Presenter] directly. This works around
 * it by offering a shim function of the same name. Once it's fixed, we can remove this and make
 * [Presenter] a fun interface instead.
 *
 * Bug: https://issuetracker.google.com/issues/240292828
 *
 * @see [Presenter] for main docs.
 */
public inline fun  presenterOf(
  crossinline body: @Composable () -> UiState
): Presenter {
  return object : Presenter {
    @Composable
    override fun present(): UiState {
      return body()
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy