傳回結果 (可序列化狀態)
本食譜說明如何使用狀態式方法,將結果從一個畫面傳回上一個畫面。這種方法會運用 Kotlin 序列化和 rememberSerializable,在設定變更和程序終止後繼續運作。
運作方式
這個範例以 ResultEventBus 為基礎,並介紹自訂擴充功能函式 conflateAsSerializableState。
- ResultEventBusNavEntryDecorator:透過
LocalResultEventBus提供ResultEventBus的NavEntryDecorator。 ResultEventBus:系統會建立ResultEventBus,並透過LocalResultEventBus提供給可組合函式。這個 EventBus 會傳送及接收結果。conflateAsSerializableState:ResultEventBus的自訂擴充功能函式,使用rememberSerializable建立狀態容器,並使用ResultEffect監聽新結果並保留。- 傳送結果:產生結果的畫面會呼叫
resultBus.sendResult(person),將資料傳送回去。 - 觀察結果:需要結果的畫面會呼叫
LocalResultEventBus.current.conflateAsSerializableState<Person?>(null)來取得State物件。UI 會觀察這個狀態,並在結果變更時重新組合。
如果結果需要在設定變更和程序終止後繼續保留,就適合使用這個方法,標準 conflateAsState 則不適用。
支援可為空值的型別
標準 rememberSerializable 函式具有 T : Any 的泛型上限限制,因此無法直接保留可為空值的型別。
為支援可為空值的型別 (例如 Person?),conflateAsSerializableState 會在內部將值包裝在泛型 @Serializable 類別 NullableWrapper 中,藉此規避這項限制:
@Serializable
private data class NullableWrapper<T>(val value: T)
自訂型別的序列化
由於這個方法會使用 androidx.compose.runtime.saveable 套件中的 rememberSerializable,因此任何做為結果的自訂類別 (例如 Person) 都必須標示 Kotlin 序列化的 @Serializable 註解:
@Serializable
data class Person(val name: String, val favoriteColor: String)
使用具體化版本時,conflateAsSerializableState 擴充功能函式會透過 serializer<T>() 輔助程式自動擷取適當的 KSerializer:
@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() } ) } } ) } } } }