【问题标题】:Android Compose Navigation and ViewModel lifecycleAndroid Compose 导航和 ViewModel 生命周期
【发布时间】:2021-09-05 14:06:00
【问题描述】:

我刚开始使用 Compose。乍一看,对我来说,这一切都像是我喜欢的 SwiftUI 的副本。但是当我开始真正使用它时,我很快就遇到了很多问题。显然,我需要找到正确的方法来使用它来从中受益......

这是我的问题之一。

package org.test.android.kotlin.compose.ui

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.test.android.kotlin.compose.ui.theme.MbiKtTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MbiKtTheme {
                val navController = rememberNavController()
                // <Edit #1>
                // Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also { navController.navigate(it) }
                // Navigator.route.observe(this, { route -> navController.navigate(route) })
                // </Edit #1>
                // <Edit #2>
                Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also { 
                    navController.popBackStack()
                    navController.navigate(it)
                }
                // </Edit #2>
                Surface(color = MaterialTheme.colors.background) {
                    NavHost(
                        navController = navController,
                        startDestination = "setup"
                    ) {
                        composable(route = "setup") {
                            SetupScreen()
                        }
                        composable(route = "progress") {
                            ProgressScreen()
                        }
                    }
                }
            }
        }
    }
}

// This is unnecessary here in this simple code fragment, but a MUST for large modular projects
object Navigator {
    // <Edit #1>
    val route = MutableSharedFlow<String>(0, 1, BufferOverflow.DROP_OLDEST)
    //val route: MutableLiveData<String> = MutableLiveData()
    // </Edit #1>
}

class SetupViewModel : ViewModel() {
    init {
        Log.d(toString(), "Create")
    }

    override fun onCleared() {
        Log.d(toString(), "Destroy")
    }

    override fun toString(): String {
        return "SetupViewModel"
    }
}

@Composable
fun SetupScreen(model: SetupViewModel = viewModel()) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(all = Dp(8f))
    ) {
        Text(text = "Setup")
        Spacer(modifier = Modifier.weight(1f))
        Button(onClick = { Navigator.route.tryEmit("progress") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Register") }
    }
}

class ProgressViewModel : ViewModel() {
    init {
        Log.d(toString(), "Created")
    }

    override fun onCleared() {
        Log.d(toString(), "Cleared")
    }

    override fun toString(): String {
        return "ProgressViewModel"
    }
}

@Composable
fun ProgressScreen(model: ProgressViewModel = viewModel()) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(all = Dp(8f))
    ) {
        Text(text = "Progress")
        Spacer(modifier = Modifier.weight(1f))
        Button(onClick = { Navigator.route.tryEmit("setup") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Abort") }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MbiKtTheme {
        SetupScreen()
    }
}

当然,我的实际情况要复杂得多,我尽了最大努力将其简化,但这已经说明了我的问题:

  • 在两个屏幕(可组合)之间导航并旋转屏幕
  • 并观察 LogCat 中两个视图模型的 Created/Destroyed 消息
  • 首先:从一个屏幕导航到另一个屏幕时永远不会调用 Destroyed(显然是因为 Activity 保持活动状态),这在大型项目中是完全不能接受的
  • 随后,只要您至少导航一次到另一个屏幕(只需点击按钮),每次屏幕旋转都会开始重新创建视图模型,这也是完全不可接受的

我知道 compose 还不成熟(我看到一些组件仍处于“alpha”版本)。所以这可能是撰写本身的一个错误。

或者这可能只是我对如何在大型和模块化项目中使用 Compose 的误解......

有什么想法吗?

(为了完整起见,我仔细检查了我使用的是当前可用的最新版本。)

编辑 #1 (2021/09/05)

感谢处理我的一个问题的文章(下面的评论中的链接),我解决了其中一个问题:在旋转屏幕时不再重新创建视图模型(仍然不知道为什么)。

所以剩下的问题是视图模型没有遵循预期的生命周期。

编辑 #2 (2021/09/13)

感谢下面的答案(不幸的是,我没有找到任何方法让它接受答案 - SF UI 对我来说仍然有点不清楚),我能够真正让视图模型生命周期按预期工作。

我刚刚禁用了后台堆栈,这在我的应用程序中无论如何都是不需要的(在 UI 和底层模型之间造成很多混乱)功能...

【问题讨论】:

  • 同时,我发现这篇有趣的文章似乎在处理我在 Compose 中的一个架构问题(显然,幸运的是,我不是唯一一个):medium.com/google-developer-experts/…跨度>

标签: android kotlin android-jetpack-compose


【解决方案1】:

每次屏幕旋转时都会重建完整的合成树,从setContent开始。

在您的源代码中,您在每次重组时都订阅了Navigator.route.observe。而“修复”是将LiveDataFloat 转换为复合状态。您使用Flow + collectAsState 执行此操作,使用LiveData 类似的方法称为observeAsState。详细了解state in compose

因此,每次您旋转设备时,都会调用navigate

navigate 不会用新的目的地改变当前屏幕。相反,它将新视图推送到堆栈上。所以每次你navigate - 你将一个新屏幕推送到导航堆栈上并为其创建一个模型。当您在没有collectAsState 的情况下旋转设备时,您会将另一个屏幕推送到堆栈上。在documentation 中查看有关撰写导航的更多信息。

您可以使用NavOptionsBuilder 更改此行为,例如:

navController.navigate(route) {
    if (route == "setup") {
        popUpTo("setup")
    }
}

当相应的视图离开导航堆栈时,视图模型将被释放。如果你点击导航栏上的返回按钮,你会看到它已经被释放了。

附言我个人觉得 Compose 与 SwiftUI 相比更加灵活方便,尽管第一个稳定版本仅在一个月前发布。你只需要更好地了解它。

【讨论】:

  • 或者我可以在 navController.navigate(it) 之前调用 navController.popBackStack(),这似乎让我摆脱了混乱的 backstack Android 功能......谢谢!
猜你喜欢
  • 2018-04-07
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-17
  • 2020-02-10
  • 2018-10-13
相关资源
最近更新 更多