You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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跳转到SECOND时
    Button(onClick = { 
        navController.navigate("SECOND") {
            // 将FIRST目的地弹出返回栈
            popUpTo("FIRST") { inclusive = true }
        }
    }) {
        Text(text = "to SECOND")
    }
    
    此时FIRST被移出返回栈,对应的FirstVM会立刻触发onCleared。
  • 在当前目的地按系统返回键(或调用navController.popBackStack()),当前栈顶的目的地会被弹出返回栈,对应的ViewModel也会触发onCleared。

内容的提问来源于stack exchange,提问作者Running Turtle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 01:57:16