我有以下用例:
- 单个
Activity
使用Jetpack Compose
+Compose Navigation
。 - 有3个屏幕:
main
,addresses
(main
的子)和address details
(addresses
的子)。addresses
是使用导航子图定义的(实际上导航图要复杂得多,所以我想要/需要子图),但崩溃与它无关(即,当我把所有的路由"内联"时,它仍然发生)。 addresses
创建了一个AddressesViewModel
作用域到它的NavBackStackEntry
,它以及它的子节点使用视图模型——子节点查找addresses
,NavBackStackEntry
查找模型的相同实例(并且它必须是相同的实例)。原因是我希望视图模型在addresses
屏幕弹出时被销毁。
这在深入导航图时很有效;然而,当"向上"(弹出屏幕)时,弹出addresses
屏幕并进入main
会导致崩溃。原因是,在弹出addresses
之后,@Composable
再次被重组,这将查找视图模型,但此时后堆栈已经没有addresses
路由,并且查找addresses
的后堆栈条目崩溃。
一个丑陋的解决方案是将视图模型挂钩到main
屏幕,但它太高了,其他模块(addresses
的兄弟)也会有它,这是不希望的。
另一个解决方法是捕获异常,不渲染屏幕的内容(但仍然渲染Scaffold
与标题等,以防止闪烁。但这也很难看。
我有几个问题:
- 我注意到,当使用
Compose Navigation
相同的屏幕重组几次(如3,4或更多)-为什么? - 为什么屏幕在被弹出后还要重新合成?
- 我想我不能使用
lifecycle-viewmodel-compose
的viewModel
助手,因为它将视图模型扩展到Activity
/Fragment
(即我不想要的非常广泛的范围),对吧?即使它的作用域是当前路由,这也会有效地阻止我共享整个图的视图模型(因为每个导航返回堆栈条目都会得到自己的实例)。 - 最后,我如何在没有崩溃的情况下实现我的用例,使用导航返回堆栈项的属性范围,没有上述的解决方案?
我使用最新的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
获得它。
视图模型的作用域被绑定到导航路由上,如文档中声明的。