【问题标题】:Jetpack Compose: Why initially loaded data is reloaded after screen rotationJetpack Compose:为什么最初加载的数据在屏幕旋转后重新加载
【发布时间】:2021-08-28 21:08:28
【问题描述】:

最小的可重现示例,因此屏幕旋转后显示的随机值发生了变化:

package composesandbox

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*

val rnd = Random()

suspend fun loadRandomNumber(): Int {
    delay(1000L) //emulating network delay
    return rnd.nextInt(1000)
}

class RandomNumberViewModel : ViewModel() {
    val num = mutableStateOf(0)

    fun reload() {
        viewModelScope.launch {
            num.value = loadRandomNumber()
        }
    }
}

@Composable
fun RandomNumberScreen() {
    val vm = viewModel<RandomNumberViewModel>()

    Column {
        LaunchedEffect(Unit) {
            vm.reload()
        }
        Text(text = "Random Number:\n ${vm.num.value}")
    }
}

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            RandomNumberScreen()
        }
    }
}

我希望数据(viewmodel.num)只加载ONCE,尽管屏幕旋转。请解释一下,为什么数据会在轮换时重新加载。

【问题讨论】:

  • 如果您只想加载一次数据,为什么不在 ViewModel 的 init 中加载它?
  • @ianhanniballake,最短的解释是在实际示例中我应该使用某个实体的id 来加载。而在 ViewModel 的 init 上,这个 id 尚不为人所知 [它作为 @Composable fun EntityScreen(id: Int) 的参数传递]
  • 如果您在那里使用 Navigation Compose 和参数,那么这些参数 在构造时通过 SavedStateHandle 传递给您的 ViewModel。您在使用导航撰写吗?
  • 谢谢,伊恩,我在阅读文档时错过了这一点!这解决了问题 - 数据可以加载到 ViewModel.init(args)

标签: android android-jetpack-compose


【解决方案1】:

在屏幕旋转上onCreate 再次被调用。

这意味着 Activity 中的所有内容都被重构了。

在重新组合时,您的 Composed 函数 RandomNumberScreen() 被再次调用,这将再次调用您的 reload() 函数,这会更改 num 的值。

请记住,在重组时,整个 Composable 会再次执行以使状态保持最新。

您的意图可能是,每次num 发生变化时,Composable 都应该重新组合。您不应该在 recompose 时触发再次更改 num 的函数。

您可以在视图模型中使用变量之类的东西来确保 num 已初始化并且在初始化时不会更改。在执行reload之前检查变量是true还是false

if (!isInitialized) reload() 类似这样的东西。

【讨论】:

  • 不错的方法。
【解决方案2】:

这是因为您在 LaunchedEffect 中调用 reload 方法,该方法在第一次合成时执行。当你旋转屏幕时,整个屏幕都会被破坏。所有的 Composable 都会重新渲染自己,因此会产生不一致的值。

好的,你可以这样做

class RandomNumberViewModel : ViewModel() {
    val num = mutableStateOf(0)

    fun reload(): Int { //return something so that it can be saved
        viewModelScope.launch {
            num.value = loadRandomNumber()
        }
        return 0
    }
}

@Composable
fun RandomNumberScreen() {
    val vm = viewModel<RandomNumberViewModel>()

    Column {
        rememberSaveable {
            vm.reload()
        }
        Text(text = "Random Number:\n ${vm.num.value}")
    }
}

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            RandomNumberScreen()
        }
    }
}

【讨论】:

  • 请注意,这可能不是官方的做法,特别是在涉及屏幕旋转的情况下,我不记得有任何帮助。如果有人有更清洁的方法,请分享。
  • 这给出了 `java.lang.IllegalArgumentException: kotlin.Unit 无法使用当前的 SaveableStateRegistry 保存。默认实现仅支持可以存储在 Bundle 中的类型。请考虑为此类实现自定义 Saver 并将其传递给 rememberSaveable()。 ` 此外,我不想使用 SavedInstanceState 保存数据 - 我希望从 viewModel 中获取数据(因为数据可能是不符合 Bundle 大小限制且不可打包或可序列化的大容量列表)
  • 哦,是的,很抱歉我其实有点困。是的,rememberSaveable 在这里不起作用,但原因很清楚,即为什么没有达到预期的行为
  • 为什么不直接将值保存在视图模型中?
  • 我认为你应该改变方法。
猜你喜欢
  • 2022-11-07
  • 2023-01-31
  • 2014-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多