使用Jetpack Compose在屏幕旋转时保存状态

13

我正在使用Android Jetpack的Compose,并一直在尝试找出如何为方向更改保存状态。

我的想法是将一个类变成ViewModel。因为当我使用Android的传统API时,这通常是有效的。

我已经使用了remember {}和mutableState {}来更新UI当信息被更改时。请验证我的理解是否正确...

remember = 保存变量并允许通过.value进行访问,这允许值被缓存。但它的主要用途是在更改时不重新分配变量。

mutableState = 当有变化时更新变量。

许多博客文章说要使用@Model,但是在尝试该方法时导入会出现错误。因此,我添加了一个:ViewModel()

然而,我相信我的remember {}正在阻止它按预期工作?

我能否得到指点以朝着正确的方向前进?

@Composable
fun DefaultFlashCard() {

    val flashCards = remember { mutableStateOf(FlashCards())}
    

    Spacer(modifier = Modifier.height(30.dp))

    MaterialTheme {


        val typography = MaterialTheme.typography
        var question = remember { mutableStateOf(flashCards.value.currentFlashCards.question) }



        Column(modifier = Modifier.padding(30.dp).then(Modifier.fillMaxWidth())
                .then(Modifier.wrapContentSize(Alignment.Center))
                .clip(shape = RoundedCornerShape(16.dp))) {
            Box(modifier = Modifier.preferredSize(350.dp)
                    .border(width = 4.dp,
                            color = Gray,
                            shape = RoundedCornerShape(16.dp))
                    .clickable(
                            onClick = {
                                question.value = flashCards.value.currentFlashCards.answer })
                    .gravity(align = Alignment.CenterHorizontally),
                    shape = RoundedCornerShape(2.dp),
                    backgroundColor = DarkGray,
                    gravity = Alignment.Center) {
                Text("${question.value}",
                        style = typography.h4, textAlign = TextAlign.Center, color = White
                )
            }
        }

        Column(modifier = Modifier.padding(16.dp),
                horizontalGravity = Alignment.CenterHorizontally) {

            Text("Flash Card application",
                    style = typography.h6,
                    color = Black)

            Text("The following is a demonstration of using " +
                    "Android Compose to create a Flash Card",
                    style = typography.body2,
                    color = Black,
                    textAlign = TextAlign.Center)

            Spacer(modifier = Modifier.height(30.dp))
            Button(onClick = {
                flashCards.value.incrementQuestion();
                question.value = flashCards.value.currentFlashCards.question },
                    shape = RoundedCornerShape(10.dp),
                    content = { Text("Next Card") },
                    backgroundColor = Cyan)
        }
    }
}


data class Question(val question: String, val answer: String) {
}


class FlashCards: ViewModel() {

    var flashCards = mutableStateOf( listOf(
            Question("How many Bananas should go in a Smoothie?", "3 Bananas"),
            Question("How many Eggs does it take to make an Omellete?", "8 Eggs"),
            Question("How do you say Hello in Japenese?", "Konichiwa"),
            Question("What is Korea's currency?", "Won")
    ))

    var currentQuestion = 0

    val currentFlashCards
        get() = flashCards.value[currentQuestion]

    fun incrementQuestion() {
        if (currentQuestion + 1 >= flashCards.value.size) currentQuestion = 0 else currentQuestion++
    }
}

2个回答

20

在Compose中处理配置更改的另一种方法是rememberSaveable。正如文档所述:

虽然remember可帮助您在重新组合时保留状态,但该状态在配置更改时不会保留。为此,您必须使用rememberSaveablerememberSaveable自动保存可以在Bundle中保存的任何值。对于其他值,可以传递自定义saver对象。

似乎Mohammad的解决方案更加健壮,但这个方法似乎更简单。


2
这种方法的问题在于Bundle有两个限制:要存储的对象必须是可序列化的,而且不能太大,否则会出现TransactionTooLarge异常。我想在Composable的生命周期内(比ViewModel的生命周期短)将一个对象存储在内存中,以便在配置更改时保留它。 - Sebas LG
请记得在清单文件中的活动中添加configChanges标签,以使rememberSaveable正常工作。 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" - Ajith M A

14

更新:

在Compose中,有两种内置的方法可以持久化状态:

  1. remember:用于在可组合函数之间保存状态,以便在重组之间保持。

  2. rememberSaveable:仅在重组之间保存状态,并且不处理配置更改进程终止,因此如果要在配置更改和进程终止后继续存在,应该使用rememberSaveable

但是rememberSaveable也存在一些问题:

  1. 它支持原始类型的开箱即用,但对于更复杂的数据,如 data class,您必须创建一个 Saver 来解释如何将状态持久化到 bundle 中。
  2. rememberSaveable 在内部使用 Bundle,因此您可以将多少数据持久化在其中存在限制,如果数据过大,则会遇到 TransactionTooLarge 异常。
上述问题有以下解决方案:
  1. 通过在 Manifest 中设置 android:configChanges,以避免在配置更改时重新创建活动。(在进程死亡期间无效,也无法防止在 Android 12 中更改壁纸时重新创建)
  2. 使用 ViewModel + remeberSaveable 的组合 + 数据持久化存储

=======================================================

与之前一样,您可以使用Architecture Component ViewModel来处理配置更改。
您应该在Activity/Fragment中初始化ViewModel,然后将其传递给Composable函数。
class UserDetailFragment : Fragment() {

    private val viewModel: UserDetailViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        return ComposeView(context = requireContext()).apply {
            setContent {
                AppTheme {
                    UserDetailScreen(
                        viewModel = viewModel
                    )
                }
            }
        }
    }
}

然后您的ViewModel应该通过LiveData或Flow之类的方式公开ViewState。
UserDetailViewModel:
class UserDetailViewModel : ViewModel() {
    private val _userData = MutableLiveData<UserDetailViewState>()
    val userData: LiveData<UserDetailViewState> = _userData


    // or

    private val _state = MutableStateFlow<UserDetailViewState>()
    val state: StateFlow<UserDetailViewState>
        get() = _state

}

现在你可以在你的可组合函数中观察到这个状态:
@Composable
fun UserDetailScreen(
    viewModel:UserDetailViewModel
) {
    val state by viewModel.userData.observeAsState()
    // or
    val viewState by viewModel.state.collectAsState()

}

2
这绝对是在生存配置更改的情况下持久化状态的首选方法。如果可以,请标记为正确!:x - Nicolas Mage
这种方法的问题在于ViewModel存在于Activity/Fragment级别,而remember仅限于Composable本地。如果我们有在同一Activity中出现和消失的可组合UI元素,我希望具有与Composable生命周期一起读取和处理状态的状态,而不是与Activity更长的生命周期一起处理。此外还要考虑到配置更改的影响。 - Sebas LG
请记得在清单文件中的活动中添加configChanges标签,以使rememberSaveable正常工作。 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" - Ajith M A

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接