Starting in Navigation 3 1.2.0, you can return
results from destinations using the ResultEventBus
API.
ResultEventBus provides two communication models:
- Event-based results: For transient, one-time events (such as showing a
confirmation snackbar or triggering a side effect) using
ResultEffect. - State-based results: For observing the latest result as Compose
StateusingconflateAsState.
Set up the result event bus
To make ResultEventBus available to your composable destinations, add the
rememberResultEventBusNavEntryDecorator to the list
of decorators passed to your NavDisplay. This provides each
destination's content with a LocalResultEventBus
composition local.
NavDisplay( /* ... */ entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberResultEventBusNavEntryDecorator() ) )
Result keys
ResultEventBus identifies and routes each result using a key. Senders and
receivers match results by using the same key.
You can specify result keys in two ways:
- Explicit keys: You can specify an explicit key (such as
resultKey = "pickup_address"). Use explicit keys when returning common types (such asString,Boolean, or primitives), or when multiple destinations return different instances of the same data type. - Type-derived keys: When you don't specify an explicit key,
ResultEventBusautomatically generates a key using thetoStringrepresentation of the result type'sKClass(such asContact::class.toString()). Use type-derived keys for distinct, domain-specific data types.
Return results from a destination
To keep screen composables reusable and testable, don't access
LocalResultEventBus directly inside your screen UI. Instead, expose callback
lambdas from your screen. In your entryProvider, handle the callback by
sending the result using
LocalResultEventBus.current and navigating back.
You can send results using an explicit result key:
import androidx.compose.runtime.Composable import androidx.navigation3.runtime.result.LocalResultEventBus entry<AddressPickerRoute> { val resultBus = LocalResultEventBus.current AddressPickerScreen( onAddressSelected = { selectedAddress: Address -> resultBus.sendResult( resultKey = "pickup_address", result = selectedAddress ) navigator.goBack() } ) }
You can also send results using a type-derived key:
import androidx.compose.runtime.Composable import androidx.navigation3.runtime.result.LocalResultEventBus entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { selectedContact: Contact -> resultBus.sendResult(result = selectedContact) navigator.goBack() } ) }
Receive results
Destinations can consume results using either event-based effects or state-based observables.
| API | Behavior | Recommended use cases |
|---|---|---|
ResultEffect |
Queued: Processes all results emitted for the key in order. | One-time events and side effects (such as displaying a snackbar or forwarding to a ViewModel). |
conflateAsState |
Conflated: Drops intermediate results and retains only the latest result as Compose State. |
Lightweight UI state modifiers (such as active filter tags or selection overrides). |
Handle one-time events with ResultEffect
Use ResultEffect when handling one-time events such as
triggering analytics, displaying a snackbar, or forwarding a result to a
ViewModel.
ResultEffect maintains a queue for incoming results. If multiple results are
sent for a given key, ResultEffect processes each result in the order it was
sent. Additionally, ResultEffect runs in a coroutine scope, which lets you
call suspending functions directly inside the effect body.
You can listen for results associated with an explicit result key:
import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.result.ResultEffect @Composable fun RideSummaryScreen( onOpenAddressPicker: (key: String) -> Unit, viewModel: RideSummaryViewModel = viewModel() ) { ResultEffect<Address>(resultKey = "pickup_address") { address -> viewModel.onPickupAddressSelected(address) } ResultEffect<Address>(resultKey = "destination_address") { address -> viewModel.onDestinationAddressSelected(address) } RideSummaryContent( pickupAddress = viewModel.pickupAddress, destinationAddress = viewModel.destinationAddress, onPickPickup = { onOpenAddressPicker("pickup_address") }, onPickDestination = { onOpenAddressPicker("destination_address") } ) }
You can also listen for results using a type-derived key:
import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.result.ResultEffect @Composable fun ComposeMessageScreen( onPickContact: () -> Unit, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, viewModel: ComposeMessageViewModel = viewModel() ) { ResultEffect<Contact> { contact -> // Suspending calls are supported directly in the effect body snackbarHostState.showSnackbar("Selected ${contact.name}") viewModel.onRecipientSelected(contact) } ComposeMessageContent( recipient = viewModel.recipient, onPickContact = onPickContact ) }
When navigating between destinations, ResultEffect executes through the
following lifecycle sequence:
- Sender emits: The sender destination sends a result using
resultBus.sendResult(resultKey = "pickup_address", address)and pops the back stack. - Receiver enters composition: The receiver destination becomes the
active screen, and
ResultEffectstarts listening for results. - Receiver processes results:
ResultEffectreceives and executes its effect body for each result sent for that key, processing all emissions in the order they were sent. - Receiver leaves composition: When the receiver destination is popped
from the back stack,
ResultEffectleaves composition and stops listening for results.
Observe latest results as state with conflateAsState
When you only need the latest result value to directly modify or filter local UI
state and want Compose to automatically recompose whenever the result updates,
call conflateAsState on ResultEventBus.
You can observe results associated with an explicit result key:
import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.navigation3.runtime.result.LocalResultEventBus @Composable fun ThemePreviewScreen( onOpenColorPicker: (key: String) -> Unit ) { val resultBus = LocalResultEventBus.current val primaryColor by resultBus.conflateAsState<Color>( resultKey = "primary_color", defaultValue = MaterialTheme.colorScheme.primary ) val accentColor by resultBus.conflateAsState<Color>( resultKey = "accent_color", defaultValue = MaterialTheme.colorScheme.tertiary ) ThemePreviewContent( primaryColor = primaryColor, accentColor = accentColor, onPickPrimary = { onOpenColorPicker("primary_color") }, onPickAccent = { onOpenColorPicker("accent_color") } ) }
You can also observe results using a type-derived key:
import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation3.runtime.result.LocalResultEventBus @Composable fun FilterableProductListScreen( initialFilter: ProductFilter = ProductFilter.All, onOpenFilterPicker: () -> Unit ) { val resultBus = LocalResultEventBus.current // Observe latest filter result as Compose State, starting with initialFilter val activeFilter by resultBus.conflateAsState<ProductFilter>( defaultValue = initialFilter ) ProductListContent( activeFilter = activeFilter, onOpenFilterPicker = onOpenFilterPicker ) }
Hoist ResultEventBus
By default, rememberResultEventBusNavEntryDecorator creates and remembers
its own ResultEventBus internally using
rememberResultEventBus.
You can explicitly create and hoist a ResultEventBus when you need to:
- Pass the
ResultEventBusinstance directly into non-composable components or dependency injection graphs. - Send or observe results from top-level app scaffolding (such as an app bar or navigation drawer) outside the destination hierarchy.
To hoist ResultEventBus, create it using rememberResultEventBus and pass
it to rememberResultEventBusNavEntryDecorator(resultEventBus):
import androidx.compose.runtime.Composable import androidx.navigation3.runtime.result.rememberResultEventBus import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay // Hoist the ResultEventBus at the top level val resultEventBus = rememberResultEventBus() // Pass the hoisted bus to the decorator val resultEventBusNavEntryDecorator = rememberResultEventBusNavEntryDecorator<NavKey>( resultEventBus = resultEventBus ) NavDisplay( /* ... */ entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), resultEventBusNavEntryDecorator ) )
Manage and clear results
When a destination consumes a one-time result, clear it from the event bus using
removeResult. This prevents the bus from re-delivering
past events to new observers when destinations re-enter composition:
import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.result.LocalResultEventBus import androidx.navigation3.runtime.result.ResultEffect @Composable fun NotificationSettingsScreen( viewModel: NotificationViewModel = viewModel() ) { val resultBus = LocalResultEventBus.current ResultEffect<ConfirmationResult>(resultKey = "confirm_permission") { confirmation -> viewModel.onPermissionConfirmed(confirmation) // Clear the result after consumption to prevent re-delivery resultBus.removeResult(resultKey = "confirm_permission") } }
You can clear results by explicit key (resultBus.removeResult(resultKey)) or
by type-derived key (resultBus.removeResult<T>()). For details on key
matching, see Result keys.
Recipes
For complete runnable code examples demonstrating different result-passing strategies, see the following recipes: