Compose Navigation 在重新组合之前弹出一条路线,寻找这条路线会使应用程序崩溃
Compose Navigation pops a route before recomposing it again and looking for this route crashes the app
我有以下用例:
- 单个
Activity
使用 Jetpack Compose
+ Compose Navigation
.
- 有 3 个屏幕:
main
、addresses
(main
的child)和address details
(child 的addresses
). addresses
是使用导航 sub-graph 定义的(实际上导航图要复杂得多,所以我 want/need 子图),但崩溃与它无关(即它仍然发生在我把所有路线 'inline').
addresses
创建一个范围为 NavBackStackEntry
的 AddressesViewModel
并且它和它的 children 使用视图模型 - children 寻找addresses
NavBackStackEntry
来查找模型的相同实例(并且它必须是相同的实例)。原因是我希望在弹出 addresses
屏幕时销毁视图模型。
深入导航图时效果很好;但是,当转到 'up'(弹出屏幕)时,弹出 addresses
屏幕并转到 main
会导致崩溃。原因是 pop 之后 addresses
@Composable
又被重新组合,它查找视图模型,但此时返回堆栈已经没有 addresses
路由,并查找 addresses
崩溃的返回堆栈条目。
一个丑陋的解决方法是将视图模型挂接到 main
屏幕,但是它太高了,其他模块(addresses
的兄弟)也会有它,这是不希望的。
另一种解决方法是捕获异常并且不渲染屏幕内容(但仍然渲染带有标题等的 Scaffold
以防止闪烁。但这也很丑陋。
我有几个问题:
- 我注意到在使用
Compose Navigation
时,相同的屏幕会被重组几次(例如 3 次、4 次或更多次)- 为什么会这样?
- 为什么弹屏后还是重新构图?
- 我认为我不能使用
lifecycle-viewmodel-compose
中的 viewModel
助手,因为它将视图模型的范围限定为 Activity
/ Fragment
(即我的范围非常广泛不想),对吗?即使它确实将其范围限定为当前路线,这也会有效地阻止我共享整个图形的视图模型(因为每个导航返回堆栈条目都会获得自己的实例)。
- 最后,在没有上述解决方法的情况下,如何使用导航返回堆栈条目 属性 来实现我的用例而不会崩溃?
我使用 Jetpack Compose 的最新 Android Studio 模板创建了示例,这里是依赖项:
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
中您可以使用 [=13= 获取它].
视图模型范围绑定到导航路线,如 documentation 中声明的那样。
我有以下用例:
- 单个
Activity
使用Jetpack Compose
+Compose Navigation
. - 有 3 个屏幕:
main
、addresses
(main
的child)和address details
(child 的addresses
).addresses
是使用导航 sub-graph 定义的(实际上导航图要复杂得多,所以我 want/need 子图),但崩溃与它无关(即它仍然发生在我把所有路线 'inline'). addresses
创建一个范围为NavBackStackEntry
的AddressesViewModel
并且它和它的 children 使用视图模型 - children 寻找addresses
NavBackStackEntry
来查找模型的相同实例(并且它必须是相同的实例)。原因是我希望在弹出addresses
屏幕时销毁视图模型。
深入导航图时效果很好;但是,当转到 'up'(弹出屏幕)时,弹出 addresses
屏幕并转到 main
会导致崩溃。原因是 pop 之后 addresses
@Composable
又被重新组合,它查找视图模型,但此时返回堆栈已经没有 addresses
路由,并查找 addresses
崩溃的返回堆栈条目。
一个丑陋的解决方法是将视图模型挂接到 main
屏幕,但是它太高了,其他模块(addresses
的兄弟)也会有它,这是不希望的。
另一种解决方法是捕获异常并且不渲染屏幕内容(但仍然渲染带有标题等的 Scaffold
以防止闪烁。但这也很丑陋。
我有几个问题:
- 我注意到在使用
Compose Navigation
时,相同的屏幕会被重组几次(例如 3 次、4 次或更多次)- 为什么会这样? - 为什么弹屏后还是重新构图?
- 我认为我不能使用
lifecycle-viewmodel-compose
中的viewModel
助手,因为它将视图模型的范围限定为Activity
/Fragment
(即我的范围非常广泛不想),对吗?即使它确实将其范围限定为当前路线,这也会有效地阻止我共享整个图形的视图模型(因为每个导航返回堆栈条目都会获得自己的实例)。 - 最后,在没有上述解决方法的情况下,如何使用导航返回堆栈条目 属性 来实现我的用例而不会崩溃?
我使用 Jetpack Compose 的最新 Android Studio 模板创建了示例,这里是依赖项:
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
中您可以使用 [=13= 获取它].
视图模型范围绑定到导航路线,如 documentation 中声明的那样。