Compose Navigation 在重新组合之前弹出一条路线,寻找这条路线会使应用程序崩溃

Compose Navigation pops a route before recomposing it again and looking for this route crashes the app

我有以下用例:

  1. 单个 Activity 使用 Jetpack Compose + Compose Navigation.
  2. 有 3 个屏幕:mainaddressesmain 的child)和address details(child 的addresses). addresses 是使用导航 sub-graph 定义的(实际上导航图要复杂得多,所以我 want/need 子图),但崩溃与它无关(即它仍然发生在我把所有路线 'inline').
  3. addresses 创建一个范围为 NavBackStackEntryAddressesViewModel 并且它和它的 children 使用视图模型 - children 寻找addresses NavBackStackEntry 来查找模型的相同实例(并且它必须是相同的实例)。原因是我希望在弹出 addresses 屏幕时销毁视图模型。

深入导航图时效果很好;但是,当转到 'up'(弹出屏幕)时,弹出 addresses 屏幕并转到 main 会导致崩溃。原因是 pop 之后 addresses @Composable 又被重新组合,它查找视图模型,但此时返回堆栈已经没有 addresses 路由,并查找 addresses 崩溃的返回堆栈条目。

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

另一种解决方法是捕获异常并且不渲染屏幕内容(但仍然渲染带有标题等的 Scaffold 以防止闪烁。但这也很丑陋。

我有几个问题:

  1. 我注意到在使用 Compose Navigation 时,相同的屏幕会被重组几次(例如 3 次、4 次或更多次)- 为什么会这样?
  2. 为什么弹屏后还是重新构图?
  3. 我认为我不能使用 lifecycle-viewmodel-compose 中的 viewModel 助手,因为它将视图模型的范围限定为 Activity / Fragment(即我的范围非常广泛不想),对吗?即使它确实将其范围限定为当前路线,这也会有效地阻止我共享整个图形的视图模型(因为每个导航返回堆栈条目都会获得自己的实例)。
  4. 最后,在没有上述解决方法的情况下,如何使用导航返回堆栈条目 属性 来实现我的用例而不会崩溃?

我使用 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 中声明的那样。