Skip to content

Recipe: Ask for confirmation with a dialog

Problem: before a destructive action (delete, discard), you need a yes/no confirmation and the answer back in the presenter.

Use an overlay with OverlayEffect. The effect suspends on the result and sends it back as an event. Requires the circuit-overlay artifact and a ContentWithOverlays above this UI.

One-time setup: ContentWithOverlays

Wrap your navigable content in ContentWithOverlays once at the composition root. Every screen within it can show overlays. If you use shared-element transitions, put SharedElementTransitionLayout just outside ContentWithOverlays; otherwise omit it.

setContent {
  CircuitCompositionLocals(circuit) {
    SharedElementTransitionLayout {            // Optional: only if you use shared elements.
      ContentWithOverlays {
        NavigableCircuitContent(navigator = navigator, navStack = navStack)
      }
    }
  }
}

Show the dialog from nullable state

Use a nullable state property for the pending confirmation. null means hidden; a non-null value carries what to confirm. OverlayEffect shows the overlay when that value is present, and alertDialogOverlay returns a DialogResult (Confirm, Cancel, or Dismiss).

// State carries what to confirm, or null.
data class ItemState(
  val pendingDelete: ItemId?,
  val eventSink: (ItemEvent) -> Unit,
) : CircuitUiState
@Composable
fun Item(state: ItemState, modifier: Modifier = Modifier) {
  if (state.pendingDelete != null) {
    OverlayEffect(state.pendingDelete) {
      val result = show(
        alertDialogOverlay(
          title = { Text("Delete this item?") },
          confirmButton = { onClick -> Button(onClick = onClick) { Text("Delete") } },
          dismissButton = { onClick -> TextButton(onClick = onClick) { Text("Cancel") } },
        )
      )
      val confirmed = result == DialogResult.Confirm
      state.eventSink(ItemEvent.DeleteAnswered(state.pendingDelete, confirmed))
    }
  }
  // … the rest of the item …
}

A button in the UI requests the dialog by sending an event; the presenter sets pendingDelete, which makes OverlayEffect fire:

IconButton(onClick = { state.eventSink(ItemEvent.DeleteClicked(item.id)) }) {
  Icon(Icons.Default.Delete, contentDescription = "Delete")
}

Keep the pending value in state. The presenter sets pendingDelete, the UI shows the dialog, and the answer returns through DeleteAnswered.

See also: Overlays · CircuitX overlays · Pick a value from a bottom sheet