如何在使用Compose中的导航时在芯片内保存UI状态?

huangapple go评论58阅读模式
英文:

How to save UI state within a chip while using navigation in compose?

问题

I have a chip that changes configurations when being clicked on. However, as the user navigates back (popbackstack) or navigates to another screen, the clicked chip re-sets its previous position. As I understood, I will have to use a viewmodel to pass the chips state and in that way save it? I cant find a way though to store the "remembersavable" in a viewmodel.

How can I achieve this? Appreciate any feedback!

My example chip:

@Composable
fun CatsChip() {

    val textChipRememberOneState = rememberSaveable { mutableStateOf(false) }

    TextChip(
        isSelected = textChipRememberOneState.value,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = "Cats",
        selectedColor = LightGreen,
        onChecked = {
            textChipRememberOneState.value = it
        },
    )
}
英文:

I have a chip that changes configurations when being clicked on. However, as the user navigates back (popbackstack) or navigates to another screen, the clicked chip re-sets its previous position. As I understood, I will have to use a viewmodel to pass the chips state and in that way save it? I cant find a way though to store the "remembersavable" in a viewmodel.

How can I achieve this? Appreciate any feedback!

My example chip:

@Composable
fun CatsChip() {

    val textChipRememberOneState = rememberSaveable { mutableStateOf(false) }

    TextChip(
        isSelected = textChipRememberOneState.value,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = "Cats",
        selectedColor = LightGreen,
        onChecked = {
            textChipRememberOneState.value = it
        },
    )
}

答案1

得分: 1

以下是您要翻译的内容:

You can keep the state in a MutableStateFlow in a ViewModel.

To use ViewModels in Compose you need to add the following dependency to your app/build.gradle file

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")

Now you can use the viewModel() function to get the ViewModel instance in your composables.

With ViewModels you do not need to use rememberSaveable anymore, since the state will be kept in the ViewModel, however, if you want the state to persist even across process death (not just configuration changes), then you have to save the state in the SavedStateHandle.

Here is an example of a ViewModel that only keeps the state in memory, but does not save it in the SavedStateHandle.

class MemoryOnlyViewModel : ViewModel () {
    val checkedState = MutableStateFlow(false)

    fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
}

Here is an example of a ViewModel that saves the state in the SavedStateHandle.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)

    companion object {
        private const val CHECKED_STATE_KEY = "checkedState"
    }
}

Then the usage would look like this

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
        
@Composable
fun CatsChip() {
    val vm: SavedStateViewModel = viewModel() // or: val vm = viewModel<SavedStateViewModel>()
    val catsChipState by vm.checkedState.collectAsState()

    TextChip(
        isSelected = catsChipState,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = "Cats",
        selectedColor = LightGreen,
        onChecked = vm::onCheckedChange, // or: onChecked = { vm.onCheckedChange(it) }
    )
}

For more use cases see also the Business Logic section of the Compose State Hoisting documentation

Here is a demo Composable using Compose navigation and showcasing the two view models from above,
comparing it with rememberSaveable, scoping them in two different ways, to the parent context and to
the NavBackStackEntry. This shows how different scopes affect the lifecycle of ViewModels.

Requires the Compose navigation dependency in your app/build.gradle file

implementation("androidx.navigation:navigation-compose:2.5.3")

You can check the demo by calling Demo() in some composable content of your app.
Click the buttons to navigate and see how the backstack changes. The ViewModels and also the rememberSaveable that are scoped to the parent context will preserve the state all the time, whereas those that are scoped to each NavBackStackEntry will preserve state only for their own navigation destinations, which can be seen when navigating back.
Also the state saved in the MemoryOnlyViewModels will not survive process death, which you can check in the following way:

  1. Send the application to the background by pressing the Home button (but do not close it in the app switcher)
  2. Kill the process by running the following command in the IDE Terminal tab with the package name of your application
adb shell am kill <package_name>
  1. Open the application again from the app switcher

If you followed the steps correctly and managed to kill and restore the process in this way, then you should notice that only MemoryOnlyViewModels have lost/reset their state.

Here is the whole demo code. Just copy and paste to a new Kotlin file and call the Demo() composable from a composable content.

英文:

You can keep the state in a MutableStateFlow in a ViewModel.

To use ViewModels in Compose you need to add the following dependency to your app/build.gradle file

implementation(&quot;androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1&quot;)

Now you can use the viewModel() function to get the ViewModel instance in your composables.

With ViewModels you do not need to use rememberSaveable anymore, since the state will be kept in
the ViewModel, however, if you want the state to persist even across process death (not just configuration changes), then you have to save the state in the SavedStateHandle.

Here is an example of a ViewModel that only keeps the state in memory, but does not save it in the SavedStateHandle.

class MemoryOnlyViewModel : ViewModel () {
    val checkedState = MutableStateFlow(false)

    fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
}

Here is an example of a ViewModel that saves the state in the SavedStateHandle.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)

    companion object {
        private const val CHECKED_STATE_KEY = &quot;checkedState&quot;
    }
}

Then the usage would look like this

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
        
@Composable
fun CatsChip() {
    val vm: SavedStateViewModel = viewModel() // or: val vm = viewModel&lt;SavedStateViewModel&gt;()
    val catsChipState by vm.checkedState.collectAsState()

    TextChip(
        isSelected = catsChipState,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = &quot;Cats&quot;,
        selectedColor = LightGreen,
        onChecked = vm::onCheckedChange, // or: onChecked = { vm.onCheckedChange(it) }
    )
}

For more use cases see also the Business Logic section of the Compose State Hoisting documentation

<hr>

Here is a demo Composable using Compose navigation and showcasing the two view models from above,
comparing it with rememberSaveable, scoping them in two different ways, to the parent context and to
the NavBackStackEntry. This shows how different scopes affect the lifecycle of ViewModels.

Requires the Compose navigation dependency in your app/build.gradle file

implementation(&quot;androidx.navigation:navigation-compose:2.5.3&quot;)

You can check the demo by calling Demo() in some composable content of your app.
Click the buttons to navigate and see how the backstack changes. The ViewModels and also the rememberSaveable that are scoped to the parent context will preserve the state all the time, whereas those that are scoped to each NavBackStackEntry will preserve state only for their own navigation destinations, which can be seen when navigating back.
Also the state saved in the MemoryOnlyViewModels will not survive process death, which you can check in the following way:

  1. Send the application to the background by pressing the Home button (but do not close it in the app switcher)
  2. Kill the process by running the following command in the IDE Terminal tab with the package name of your application
adb shell am kill &lt;package_name&gt;
  1. Open the application again from the app switcher

If you followed the steps correctly and managed to kill and restore the process in this way, then you should notice that only MemoryOnlyViewModels have lost/reset their state.

Here is the whole demo code. Just copy and paste to a new Kotlin file and call the Demo() composable from a composable content.

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

class MemoryOnlyViewModel : ViewModel () {
    val checkedState = MutableStateFlow(false)

    fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
}

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)

    companion object {
        private const val CHECKED_STATE_KEY = &quot;checkedState&quot;
    }
}
    
@Composable
fun Demo() {    
    @Composable
    fun SimpleChip(
        text: String,
        isSelected: Boolean,
        onChecked: (Boolean) -&gt; Unit,
    ) {
        Surface(
            onClick = { onChecked(!isSelected) },
            modifier = Modifier.padding(4.dp),
            shape = RoundedCornerShape(16.dp),
            color = if (isSelected) Color(0xFF7986CB) else Color.LightGray,
        ) {
            Row(
                modifier = Modifier.padding(8.dp),
                horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(text)
                Icon(
                    imageVector = if (isSelected) Icons.Default.Clear else Icons.Default.AddCircle,
                    contentDescription = null,
                )
            }
        }
    }

    // These VMs are scoped to the lifecycle of the parent context (likely a ComponentActivity)
    val parentMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
    val parentSavedStateVm: SavedStateViewModel = viewModel()
    var parentSaveable by rememberSaveable { mutableStateOf(false) }

    @Composable
    @Suppress(&quot;UnusedReceiverParameter&quot;)
    fun ColumnScope.DemoScreen(text: String) {
        Text(text)

        val parentMemoryOnlyState by parentMemoryOnlyVm.checkedState.collectAsState()

        SimpleChip(text = &quot;MemoryOnly VM (parent scoped)&quot;,
            isSelected = parentMemoryOnlyState, onChecked = parentMemoryOnlyVm::onCheckedChange)

        val navMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
        val navMemoryOnlyState by navMemoryOnlyVm.checkedState.collectAsState()

        SimpleChip(text = &quot;MemoryOnly VM (nav scoped)&quot;,
            isSelected = navMemoryOnlyState, onChecked = navMemoryOnlyVm::onCheckedChange)


        val parentSavedState by parentSavedStateVm.checkedState.collectAsState()

        SimpleChip(text = &quot;SavedState VM (parent scoped)&quot;,
            isSelected = parentSavedState, onChecked = parentSavedStateVm::onCheckedChange)

        val navSavedStateVm: SavedStateViewModel = viewModel()
        val navSavedState by navSavedStateVm.checkedState.collectAsState()

        SimpleChip(text = &quot;SavedState VM (nav scoped)&quot;,
            isSelected = navSavedState, onChecked = navSavedStateVm::onCheckedChange)


        SimpleChip(text = &quot;rememberSaveable (parent scoped)&quot;,
            isSelected = parentSaveable, onChecked = { parentSaveable = it })

        var navSaveable by rememberSaveable { mutableStateOf(false) }

        SimpleChip(text = &quot;rememberSaveable (nav scoped)&quot;,
            isSelected = navSaveable, onChecked = { navSaveable = it })
    }

    val navController = rememberNavController()

    @Composable
    fun BackButton() = Button(onClick = { navController.navigateUp() }) {
        Text(&quot;Go back&quot;)
    }

    @Composable
    fun NavButton(route: String) = Button(onClick = { navController.navigate(route) }) {
        Text(&quot;Navigate to $route&quot;)
    }

    Column {
        val currentEntry by navController.currentBackStackEntryAsState()
        val backStack = remember(currentEntry) {
            navController.backQueue
                .mapNotNull { it.destination.route }
                .joinToString(&quot; &gt; &quot;)
        }
        Text(text = &quot;Backstack: $backStack&quot;)

        NavHost(navController = navController, startDestination = &quot;start&quot;) {
            composable(&quot;start&quot;) {
                Row { NavButton(route = &quot;A&quot;); NavButton(route = &quot;B&quot;) }
            }
            composable(&quot;A&quot;) {
                Column {
                    Row { BackButton(); NavButton(route = &quot;B&quot;) }
                    DemoScreen(&quot;Screen A&quot;)
                }
            }
            composable(&quot;B&quot;) {
                Column {
                    Row { BackButton(); NavButton(route = &quot;A&quot;) }
                    DemoScreen(&quot;Screen B&quot;)
                }
            }
        }
    }
}

huangapple
  • 本文由 发表于 2023年4月19日 17:05:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76052665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定