Navigation Compose导航切换后HiltViewModel未触发onCleared问题
项目配置与问题复现
基础依赖配置
在Android Studio新建项目后,按如下配置添加依赖:
首先是项目级build.gradle配置:
buildscript { ext { compose_version = '1.1.0-beta01' } dependencies { classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1") } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'com.android.application' version '7.2.1' apply false id 'com.android.library' version '7.2.1' apply false id 'org.jetbrains.kotlin.android' version '1.5.31' apply false } task clean(type: Delete) { delete rootProject.buildDir }
然后是模块级build.gradle配置:
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id "dagger.hilt.android.plugin" } android { compileSdk 32 defaultConfig { applicationId "com.ohmenu.mytestapp" minSdk 25 targetSdk 32 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion compose_version } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation "androidx.compose.ui:ui:$compose_version" implementation 'androidx.compose.material3:material3:1.0.0-alpha01' implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.activity:activity-compose:1.3.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" implementation("androidx.navigation:navigation-compose:2.5.0") implementation("com.google.dagger:hilt-android:2.38.1") kapt("com.google.dagger:hilt-android-compiler:2.38.1") implementation("androidx.hilt:hilt-navigation-compose:1.0.0") } // Allow references to generated code kapt { correctErrorTypes = true }
Hilt依赖注入实现
使用Hilt实现依赖注入的代码如下:
@HiltAndroidApp class MyApp: Application() {} interface IRepoAuth { val authStatus: String } class RepoAuth @Inject constructor(): IRepoAuth { override val authStatus = "" } @Module @InstallIn(SingletonComponent::class) object AppModule { @Singleton @Provides fun provideRepoAuth(): IRepoAuth { return RepoAuth() } }
Compose UI与导航逻辑
在MainActivity中通过Compose实现UI与导航的代码如下:
@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyTestAppTheme { MyNavGraph() } } } } @Composable fun MyNavGraph( navController : NavHostController = rememberNavController(), startDestination: String = "FIRST" ) { NavHost(navController, startDestination) { composable("FIRST") { val firstVM: FirstVM = hiltViewModel() FirstScreen(navController = navController) } composable("SECOND") { SecondScreen(navController = navController) } } } @Composable fun FirstScreen( navController: NavController, vm: FirstVM = hiltViewModel() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "FIRST") Button(onClick = { navController.navigate("SECOND") }) { Text(text = "to SECOND") } } } @Composable fun SecondScreen( navController: NavController, vm: SecondVM = hiltViewModel() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "SECOND") Button(onClick = { navController.navigate("FIRST") }) { Text(text = "to FIRST") } } } @HiltViewModel class FirstVM @Inject constructor( private val repoAuth: IRepoAuth ): ViewModel() { val status = repoAuth.authStatus init { println("AAA - FIRST VM INIT :") } override fun onCleared() { println("AAA - FIRST VM CLEAR :") super.onCleared() } } @HiltViewModel class SecondVM @Inject constructor( private val repoAuth: IRepoAuth ): ViewModel() { val status = repoAuth.authStatus init { println("AAA - SECOND VM INIT :") } override fun onCleared() { println("AAA - SECOND VM CLEAR :") super.onCleared() } }
问题现象
在FirstScreen和SecondScreen之间执行导航操作时,Logcat仅能打印ViewModel的init初始化日志,始终无法输出onCleared方法的回调日志。
根据官方文档说明:如果页面是导航图中的目的地,调用hiltViewModel()获取的ViewModel实例作用域为当前目的地。按照预期,若ViewModel绑定目的地作用域,NavController导航到其他目的地时,原目的地对应的ViewModel应当被销毁并触发onCleared回调,不确定是对机制的理解存在偏差,还是代码实现存在错误。
问题原因
你对ViewModel作用域的理解存在部分偏差,代码本身没有致命错误,问题出在Navigation组件的默认导航行为:
hiltViewModel()绑定的ViewModel作用域确实是当前导航返回栈中的对应目的地条目,但只有当目的地被永久移出导航返回栈时,绑定的ViewModel才会被销毁、触发onCleared回调。- 你调用
navController.navigate("XXX")时的默认行为是将新目的地压入返回栈栈顶,旧的目的地仍然保存在返回栈中(用于后续按返回键回退),因此对应的ViewModel实例会一直保留,不会触发销毁回调。 - 另外你的代码存在一处冗余:在
NavHost的composable("FIRST")块中已经获取了一次FirstVM实例,在FirstScreen参数中又获取了一次,虽然hiltViewModel()同一作用域下会返回同一实例不会造成重复初始化,但属于无意义的冗余写法。
验证方案
如果要触发ViewModel的onCleared回调,有两种常见操作方式:
- 导航时主动将旧目的地弹出返回栈,示例代码如下:
此时FIRST被移出返回栈,对应的FirstVM会立刻触发// 从FIRST跳转到SECOND时 Button(onClick = { navController.navigate("SECOND") { // 将FIRST目的地弹出返回栈 popUpTo("FIRST") { inclusive = true } } }) { Text(text = "to SECOND") }onCleared。 - 在当前目的地按系统返回键(或调用
navController.popBackStack()),当前栈顶的目的地会被弹出返回栈,对应的ViewModel也会触发onCleared。
内容的提问来源于stack exchange,提问作者Running Turtle
相关产品推荐
相关产品推荐

