Skip to content

Screen

Screens are keys for Presenter and UI pairings.

The core Screen interface is this:

interface Screen : Parcelable

These types are Parcelable on Android for saveability in our backstack and easy deeplinking. A Screen can be a simple marker data object or a data class with information to pass on.

@Parcelize
data object HomeScreen : Screen

@Parcelize
data class AddFavoritesScreen(val externalId: UUID) : Screen

These are used by Navigators (when called from presenters) or CircuitContent (when called from UIs) to start a new sub-circuit or nested circuit.

// In a presenter class
fun showAddFavorites() {
  navigator.goTo(
    AddFavoritesScreen(
      externalId = uuidGenerator.generate()
    )
  )
}

The information passed into a screen can also be used to interact with the data layer. In the example here, we are getting the externalId from the screen in order to get information back from our repository.

// In a presenter class
class AddFavoritesPresenter
@AssistedInject
constructor(
  @Assisted private val screen: AddFavoritesScreen,
  private val favoritesRepository: FavoritesRepository,
) : Presenter<AddFavoritesScreen.State> {
  @Composable
  override fun present() : AddFavoritesScreen.State {
      val favorite = favoritesRepository.getFavorite(screen.externalId)
      // ...
  }
}

Screens are also used to look up those corresponding components in Circuit.

val presenter: Presenter<*>? = circuit.presenter(addFavoritesScreen, navigator)
val ui: Ui<*>? = circuit.ui(addFavoritesScreen)

Nomenclature

Semantically, in this example we would call all of these components together the “AddFavorites Screen”.

Saving and restoring

Circuit’s saveable back stacks (rememberSaveableBackStack and rememberSaveableNavStack) persist navigation state across configuration changes and process death. How screens are converted to a saveable form is pluggable via CircuitSaver. Both Screen and PopResult extend the CircuitSaveable marker, and a CircuitSaver converts those values to and from representations that Compose’s SaveableStateRegistry can store.

abstract class CircuitSaver protected constructor() {
  abstract fun save(value: CircuitSaveable): Any?

  protected abstract fun restore(saved: Any): CircuitSaveable?
}

inline fun <reified T : Screen> CircuitSaver.restoreScreen(
  saved: Any,
  onAbsent: () -> Unit = {},
  onTypeMismatch: (CircuitSaveable) -> Unit = {
    error("Expected ${T::class}, but CircuitSaver restored ${it::class}.")
  },
): T?

inline fun <reified T : PopResult> CircuitSaver.restorePopResult(
  saved: Any,
  onAbsent: () -> Unit = {},
  onTypeMismatch: (CircuitSaveable) -> Unit = {
    error("Expected ${T::class}, but CircuitSaver restored ${it::class}.")
  },
): T?

restore is a protected implementation hook for CircuitSaver authors. Application code uses the reified helpers instead.

The reified type parameter is the concrete expected type: restoreScreen<HomeScreen>(saved) rejects another Screen subtype. When the saver returns null, the helper invokes onAbsent and returns null. When the restored value is not the requested type, the helper passes it to onTypeMismatch. That callback throws by default; if a custom callback completes normally, the helper returns null. restorePopResult has the same behavior for PopResult subtypes.

The default (DefaultCircuitSaver) passes values through unchanged. On Android that means screens persist via their Parcelable implementations, matching Circuit’s historical behavior. Other platforms hold saved state in memory only.

Choosing a strategy

Parcelable is the Android default and needs no setup. Annotate screens with @Parcelize and the default saver persists them. For common-code screens, implement ParcelableScreen, which adds Parcelable on Android and is just a Screen elsewhere.

To persist SavedState encoded with kotlinx-serialization, use the circuit-serialization artifact. In 0.35, Android screens and pop results still need to be Parcelable in addition to being @Serializable, even though the saver stores SavedState rather than the Parcelable value:

@Parcelize
@Serializable
data object HomeScreen : Screen

val saver = SerializableCircuitSaver(
  SavedStateConfiguration {
    serializersModule = SerializersModule {
      polymorphic(CircuitSaveable::class) {
        subclass(HomeScreen::class)
        // Register every screen and pop result here
      }
    }
  }
)

On JVM and Android, ReflectiveSerializableCircuitSaver() from the circuit-serialization-reflect artifact skips the registration requirement by resolving serializers reflectively from the saved class name. The artifact embeds the R8/ProGuard rules it needs, so minified apps work without additional configuration.

Both serializing savers can restore navigation state saved by Circuit 0.34’s default saver, so adopting serialization in 0.35 does not by itself reset existing navigation state.

When a saved value can no longer be restored:

  • SaveableBackStack drops the affected record. If none survive, it starts from its initial value.
  • SaveableNavStack discards incomplete forward history. If the active screen or its back history is missing, it starts from its initial value.
  • Stored back-stack snapshots are discarded if any record is missing.
  • An unrestorable pending pop result clears its expectation, so awaitResult returns null.

See the circuit-serialization README for the full setup.

To disable persistence entirely, use CircuitSaver.NoOp. Stacks saved with it restore to their initial state. This changes persistence behavior only: Android Screen and PopResult implementations still need to be Parcelable in 0.35.

Wiring

Pass a saver directly to back stack creation, or provide it once at the app root:

// Explicit
val backStack = rememberSaveableBackStack(root = HomeScreen, circuitSaver = saver)

// Or at the root, reaches all back stacks below it
ProvideCircuitSaver(saver) {
  // App content
}

Circuit.Builder.setCircuitSaver(saver) also provides it via CircuitCompositionLocals, reaching any back stack created inside it.

Back stacks created outside CircuitCompositionLocals

Composition locals only reach content below their provider. If a back stack is created above CircuitCompositionLocals, a saver set on Circuit.Builder won’t apply to it. Pass the saver explicitly or use ProvideCircuitSaver above the back stack creation.

Roadmap

Screen and PopResult still extend Parcelable on Android in 0.35. A future release removes those supertypes. To prepare, implement ParcelableScreen or ParcelablePopResult on values that should keep using Parcelable, or adopt a serializing CircuitSaver. The circuit-serialization README has the full roadmap.