Compose Navigation在重新组合之前弹出一个路由,寻找这个路由会导致应用崩溃



我有以下用例:

  1. 单个Activity使用Jetpack Compose+Compose Navigation
  2. 有3个屏幕:main,addresses(main的子)和address details(addresses的子)。addresses是使用导航子图定义的(实际上导航图要复杂得多,所以我想要/需要子图),但崩溃与它无关(即,当我把所有的路由"内联"时,它仍然发生)。
  3. addresses创建了一个AddressesViewModel作用域到它的NavBackStackEntry,它以及它的子节点使用视图模型——子节点查找addresses,NavBackStackEntry查找模型的相同实例(并且它必须是相同的实例)。原因是我希望视图模型在addresses屏幕弹出时被销毁。

这在深入导航图时很有效;然而,当"向上"(弹出屏幕)时,弹出addresses屏幕并进入main会导致崩溃。原因是,在弹出addresses之后,@Composable再次被重组,这将查找视图模型,但此时后堆栈已经没有addresses路由,并且查找addresses的后堆栈条目崩溃。

一个丑陋的解决方案是将视图模型挂钩到main屏幕,但它太高了,其他模块(addresses的兄弟)也会有它,这是不希望的。

另一个解决方法是捕获异常,不渲染屏幕的内容(但仍然渲染Scaffold与标题等,以防止闪烁。但这也很难看。

我有几个问题:

  1. 我注意到,当使用Compose Navigation相同的屏幕重组几次(如3,4或更多)-为什么?
  2. 为什么屏幕在被弹出后还要重新合成?
  3. 我想我不能使用lifecycle-viewmodel-composeviewModel助手,因为它将视图模型扩展到Activity/Fragment(即我不想要的非常广泛的范围),对吧?即使它的作用域是当前路由,这也会有效地阻止我共享整个图的视图模型(因为每个导航返回堆栈条目都会得到自己的实例)。
  4. 最后,我如何在没有崩溃的情况下实现我的用例,使用导航返回堆栈项的属性范围,没有上述的解决方案?

我使用最新的Android Studio模板为Jetpack Compose创建了示例,以下是依赖项:

dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.compose.ui:ui:1.0.2'
implementation 'androidx.compose.material:material:1.0.2'
implementation 'androidx.compose.ui:ui-tooling-preview:1.0.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'androidx.navigation:navigation-compose:2.4.0-alpha09'
debugImplementation 'androidx.compose.ui:ui-tooling:1.0.2'
}

下面是代码(导致崩溃的视图模型查找函数在最后,其代码基于https://androidx.tech/artifacts/hilt/hilt-navigation-compose/1.0.0-alpha02-source/androidx/hilt/navigation/compose/HiltViewModel.kt.html):

package com.example.nav_sample
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.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.*
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import com.example.nav_sample.ui.theme.NavsampleTheme
const val main = "main"
const val addressesModule = "addressesModule"
const val addresses = "addresses"
const val addressDetails = "addressDetails"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavsampleTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = main) {
composable(main) {
ScaffoldScreen(
"Main screen",
onBack = { this@MainActivity.onBackPressed() },
) {
Center {
Button(
onClick = { navController.navigate(addressesModule) },
) {
Text("Addresses")
}
}
}
}
navigation(route = addressesModule, startDestination = addresses) {
composable(addresses) {
val addressesViewModel: AddressesViewModel =
navController.scopedViewModel(route = addresses)
Log.wtf(
"NavSample",
"Route: '$addresses', AddressesViewModel instance: $addressesViewModel",
)
ScaffoldScreen(
"Addresses",
onBack = { navController.popBackStack() },
) {
Center {
Column {
Button(
onClick = { navController.navigate(addressDetails) },
) {
Text("Address #1 details")
}
Button(
onClick = { navController.navigate(addressDetails) },
) {
Text("Address #2 details")
}
}
}
}
}
composable(route = addressDetails) {
val addressesViewModel: AddressesViewModel =
navController.scopedViewModel(route = addresses)
Log.wtf(
"NavSample",
"Route: '$addressDetails', AddressesViewModel instance: $addressesViewModel",
)
ScaffoldScreen(
"Address details",
onBack = { navController.popBackStack() },
) {
Center {
Text("Address details")
}
}
}
}
}
}
}
}
}
@Composable
fun ScaffoldScreen(
topBarTitle: String,
onBack: () -> Unit,
content: @Composable () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "back icon",
)
}
},
title = {
Text(text = topBarTitle)
},
)
},
content = { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
) {
content()
}
},
)
}
@Composable
fun Center(
content: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
content = content,
)
}
class AddressesViewModel : ViewModel()
@Composable
inline fun <reified T : ViewModel> NavController.scopedViewModel(route: String): T {
val viewModelStoreOwner = try {
getBackStackEntry(route)
} catch (e: Exception) {
Log.wtf(
"NavSample",
"Thrown looking up route: '$route'",
e,
)
throw e
}
val provider = ViewModelProvider(viewModelStoreOwner)
return provider[T::class.java]
}

由于过渡动画而发生重组。对于这个事实,你无能为力,你的应用应该可以很好地处理这样的重组。

您的本地问题可以很容易地解决:在addresses中通过调用viewModel()创建视图模型,在addressDetails中您将能够使用scopedViewModel获得它。

视图模型的作用域被绑定到导航路由上,如文档中声明的。

相关内容

  • 没有找到相关文章

最新更新