Restituzione di un risultato (basato sullo stato serializzabile)
Questa ricetta mostra come restituire un risultato da una schermata a una precedente utilizzando un approccio basato sullo stato che sopravvive alle modifiche alla configurazione e all'interruzione del processo sfruttando Kotlin Serialization e rememberSerializable.
Come funziona
Questo esempio si basa su ResultEventBus e introduce una funzione di estensione personalizzata conflateAsSerializableState.
- ResultEventBusNavEntryDecorator: un
NavEntryDecoratorche fornisce unResultEventBustramiteLocalResultEventBus. ResultEventBus: viene creato unResultEventBuse reso disponibile ai componenti componibili tramiteLocalResultEventBus. Questo EventBus invia e riceve i risultati.conflateAsSerializableState: una funzione di estensione personalizzata suResultEventBusche utilizzarememberSerializableper creare un contenitore di stato eResultEffectper ascoltare i nuovi risultati e renderli persistenti.- Invio del risultato: la schermata che produce il risultato chiama
resultBus.sendResult(person)per inviare nuovamente i dati. - Osservazione del risultato: la schermata che richiede il risultato chiama
LocalResultEventBus.current.conflateAsSerializableState<Person?>(null)per ottenere un oggettoState. La UI osserva questo stato e si ricompone ogni volta che il risultato cambia.
Questo approccio è adatto quando il risultato deve sopravvivere alle modifiche alla configurazione e all'interruzione del processo, mentre lo standard conflateAsState non lo fa.
Supporto dei tipi Nullable
La funzione standard rememberSerializable ha un vincolo generico di limite superiore di T : Any, che impedisce la conservazione diretta dei tipi Nullable.
Per supportare i tipi che accettano valori null (ad esempio Person?), conflateAsSerializableState aggira questa limitazione eseguendo il wrapping interno del valore in una classe @Serializable generica, NullableWrapper:
@Serializable
private data class NullableWrapper<T>(val value: T)
Serializzazione dei tipi personalizzati
Poiché questo approccio utilizza rememberSerializable dal pacchetto androidx.compose.runtime.saveable, qualsiasi classe personalizzata utilizzata come risultato (come Person) deve essere contrassegnata con l'annotazione @Serializable di Kotlin Serialization:
@Serializable
data class Person(val name: String, val favoriteColor: String)
La funzione di estensione conflateAsSerializableState recupera automaticamente il KSerializer appropriato tramite l'helper serializer<T>() quando viene utilizzata la versione reificata:
@Composable
inline fun <reified T> ResultEventBus.conflateAsSerializableState(
defaultValue: T,
vararg inputs: Any?,
configuration: SavedStateConfiguration = SavedStateConfiguration.DEFAULT,
): State<T>
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.common import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { var person by mutableStateOf<Person?>(null) }
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.common import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable @Serializable data object Home : NavKey @Serializable class PersonDetailsForm : NavKey
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.common import kotlinx.serialization.Serializable @Serializable data class Person(val name: String, val favoriteColor: String)
/* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.common import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import com.example.nav3recipes.content.ContentBlue import com.example.nav3recipes.content.ContentGreen @Composable fun HomeScreen( person: Person?, onNext: () -> Unit ) { ContentBlue("Hello ${person?.name ?: "unknown person"}") { if (person != null) { Text("Your favorite color is ${person.favoriteColor}") } Spacer(Modifier.height(16.dp)) Button(onClick = dropUnlessResumed(block = onNext)) { Text("Tell us about yourself") } } } @Composable fun PersonDetailsScreen( onSubmit: (Person) -> Unit ) { ContentGreen("About you") { val nameTextState = rememberTextFieldState() OutlinedTextField( state = nameTextState, label = { Text("Please enter your name") } ) val favoriteColorTextState = rememberTextFieldState() OutlinedTextField( state = favoriteColorTextState, label = { Text("Please enter your favorite color") } ) Button( onClick = dropUnlessResumed { val person = Person( name = nameTextState.text.toString(), favoriteColor = favoriteColorTextState.text.toString() ) onSubmit(person) }, enabled = nameTextState.text.isNotBlank() && favoriteColorTextState.text.isNotBlank() ) { Text("Submit") } } }
/* * Copyright 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.serializable import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSerializable import androidx.navigation3.runtime.result.ResultEffect import androidx.navigation3.runtime.result.ResultEventBus import androidx.savedstate.serialization.SavedStateConfiguration import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.serializer @Serializable private data class NullableWrapper<T>(val value: T) /** * Reusable extension function on [ResultEventBus] to provide a single [State] that preserves * its value across configuration changes and process death using [rememberSerializable]. * * @param T The type of the result value. * @param resultKey The unique key associated with this result. * @param defaultValue The default initial value to be remembered and saved when no previously saved state exists. * @param inputs A set of inputs such that, when any of them have changed, the state will reset and re-initialize with [defaultValue]. * @param stateSerializer A [KSerializer] used to serialize and deserialize the state value. * @param configuration Optional [SavedStateConfiguration] to customize how the serialization is * handled, such as specifying a custom format (e.g., JSON). * @return A [State] containing the current value of the result, which updates dynamically when new results are received. */ @Composable fun <T> ResultEventBus.conflateAsSerializableState( resultKey: String, defaultValue: T, vararg inputs: Any?, stateSerializer: KSerializer<T>, configuration: SavedStateConfiguration = SavedStateConfiguration.DEFAULT, ): State<T> { val wrapperSerializer = remember(stateSerializer) { NullableWrapper.serializer(stateSerializer) } val wrapper = rememberSerializable( inputs = inputs, stateSerializer = wrapperSerializer, configuration = configuration, ) { mutableStateOf(NullableWrapper(defaultValue)) } // ResultEffect's internal LaunchedEffect does not restart when the onResult lambda changes. // If inputs change, rememberSerializable recreates the savedState instance. Using // rememberUpdatedState ensures that the ongoing collection coroutine inside ResultEffect // always writes to the latest savedState instance without needing to cancel and restart. // https://issuetracker.google.com/531709234 val currentWrapper = rememberUpdatedState(wrapper) ResultEffect<T>(resultKey = resultKey, resultEventBus = this) { result -> currentWrapper.value.value = NullableWrapper(result) } // Return a custom State wrapper rather than using derivedStateOf. Since unwrapping NullableWrapper // is O(1), an anonymous State avoids the snapshot read-tracking overhead and extra allocations // of derivedStateOf while maintaining a stable reference for the caller. return remember(wrapper) { object : State<T> { override val value: T get() = wrapper.value.value } } } /** * Reified version of [conflateAsSerializableState] using the class name as the key. * * @warning Do not use this overload for generic types (e.g., [List], [Map]) because * JVM type erasure will cause key collisions. Instead, use the version with an explicit `resultKey`. */ @Composable inline fun <reified T> ResultEventBus.conflateAsSerializableState( defaultValue: T, vararg inputs: Any?, configuration: SavedStateConfiguration = SavedStateConfiguration.DEFAULT, ): State<T> = conflateAsSerializableState( resultKey = T::class.toString(), defaultValue = defaultValue, inputs = inputs, stateSerializer = serializer<T>(), configuration = configuration, )
/* * Copyright 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.nav3recipes.results.serializable import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.result.LocalResultEventBus import androidx.navigation3.runtime.result.rememberResultEventBusNavEntryDecorator import androidx.navigation3.ui.NavDisplay import com.example.nav3recipes.results.common.Home import com.example.nav3recipes.results.common.HomeScreen import com.example.nav3recipes.results.common.Person import com.example.nav3recipes.results.common.PersonDetailsForm import com.example.nav3recipes.results.common.PersonDetailsScreen import com.example.nav3recipes.ui.setEdgeToEdgeConfig class ResultSerializableActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { setEdgeToEdgeConfig() super.onCreate(savedInstanceState) setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(Home) NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberResultEventBusNavEntryDecorator() ), entryProvider = entryProvider { entry<Home> { val resultState = LocalResultEventBus .current .conflateAsSerializableState<Person?>(null) val person = resultState.value HomeScreen( person = person, onNext = { backStack.add(PersonDetailsForm()) } ) } entry<PersonDetailsForm> { val resultBus = LocalResultEventBus.current PersonDetailsScreen( onSubmit = { person -> resultBus.sendResult(result = person) backStack.removeLastOrNull() } ) } } ) } } } }