Sonuç döndürme (serileştirilebilir duruma dayalı)
Bu tarifte, Kotlin Serialization ve rememberSerializable'dan yararlanarak yapılandırma değişikliklerine ve işlem sonlandırmaya karşı dayanıklı, duruma dayalı bir yaklaşımla bir ekrandan önceki ekrana nasıl sonuç döndürüleceği gösterilmektedir.
İşleyiş şekli
Bu örnek, ResultEventBus üzerine kurulmuştur ve özel bir uzantı işlevi olan conflateAsSerializableState'yi tanıtır.
- ResultEventBusNavEntryDecorator:
LocalResultEventBusaracılığıylaResultEventBussağlayan birNavEntryDecorator. ResultEventBus:ResultEventBusoluşturulur veLocalResultEventBusaracılığıyla composable'lara sunulur. Bu EventBus, sonuçları gönderir ve alır.conflateAsSerializableState:ResultEventBusüzerinde, durum kapsayıcısı oluşturmak içinrememberSerializable'ı, yeni sonuçları dinlemek ve kalıcı hale getirmek içinResultEffect'ı kullanan özel bir uzantı işlevi.- Sonucu gönderme: Sonucu üreten ekran, verileri geri göndermek için
resultBus.sendResult(person)işlevini çağırır. - Sonucu gözlemleme: Sonuca ihtiyaç duyan ekran,
Statenesnesini almak içinLocalResultEventBus.current.conflateAsSerializableState<Person?>(null)işlevini çağırır. Kullanıcı arayüzü bu durumu gözlemler ve sonuç değiştiğinde yeniden oluşturur.
Bu yaklaşım, sonucun yapılandırma değişikliklerinden ve işlem sonlandırmadan etkilenmemesi gerektiği durumlarda uygundur. Standart conflateAsState ise bu durumlarda uygun değildir.
Nullable türleri destekleme
Standart rememberSerializable işlevinde, boş değer atanabilir türlerin doğrudan korunmasını engelleyen T : Any genel üst sınır kısıtlaması vardır.
Boş değer atanabilir türleri (ör. Person?) desteklemek için conflateAsSerializableState, değeri dahili olarak genel bir @Serializable sınıfına (NullableWrapper) sarmalayarak bu kısıtlamayı aşar:
@Serializable
private data class NullableWrapper<T>(val value: T)
Özel Türlerin Serileştirilmesi
Bu yaklaşım, androidx.compose.runtime.saveable paketindeki rememberSerializable öğesini kullandığından sonuç olarak kullanılan tüm özel sınıflar (ör. Person) Kotlin Serialization'ın @Serializable ek açıklamasıyla işaretlenmelidir:
@Serializable
data class Person(val name: String, val favoriteColor: String)
conflateAsSerializableState uzantı işlevi, somutlaştırılmış sürüm kullanılırken serializer<T>() yardımcı programı aracılığıyla uygun KSerializer değerini otomatik olarak alır:
@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() } ) } } ) } } } }