Android撰写导航和ViewModel生命周期



我只是从撰写开始。乍一看,对我来说,这一切都像是我喜欢的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消息
  • 首先:当从一个屏幕导航到另一个屏幕时,destroy永远不会被调用(显然是因为活动保持活跃),这在大型项目中是完全不可接受的
  • 随后,只要你至少导航到另一个屏幕一次(只需点击按钮),视图模型就开始随着每次屏幕旋转而重新创建,这也是完全不可接受的

我知道compose还不成熟(我看到一些组件还在"alpha";释放)。所以这可能是compose本身的一个bug。

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

任何想法?

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

编辑# 1(2021/09/05)

多亏了这篇文章处理了我的一个问题(链接在下面的评论中),我修复了一个问题:视图模型在旋转屏幕时不再被重新创建(仍然没有线索,为什么)。

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

编辑# 2(2021/09/13)

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

我刚刚禁用了back-stack,这在我的应用程序中是不需要的(在UI和底层模型之间造成很多混乱)功能…

每次旋转屏幕时,从setContent开始重建完整的构图树。

在源代码中,您在每次重组时都订阅了Navigator.route.observe。还有"修复"是将LiveDataFloat转化为复合态。您使用Flow+collectAsState完成了此操作,对于LiveData,使用类似的方法称为observeAsState。在撰写中了解更多关于状态的信息。

所以,每次你旋转设备,navigate被调用。

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

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

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

当相应的视图离开导航堆栈时,视图模型将被释放。如果您单击导航栏上的后退按钮,您将看到它已被释放。

注。我个人认为比起SwiftUI, Compose更加灵活和方便,尽管它的第一个稳定版本一个月前才发布。你只需要更好地了解它。

最新更新