Search code examples
androidandroid-jetpack-composedagger-hiltandroid-jetpack-navigation

HiltViewModel is not cleared after navigation


After creating a new project in Android Studio and adding the relevant dependencies:

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
}

------------

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
}

I use Hilt to manage dependency injections:

@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() }
}

Then in my MainActivity, I use composition and navigation:

@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()
  }
}

The issue I have is when navigating between FirstScreen and SecondScreen, the logcat shows that the init logs are printed but not the onCleared logs.

According to the documentation here : https://developer.android.com/jetpack/compose/libraries

"if [Screen] is a destination in a navigation graph, call hiltViewModel() to get an instance of [ViewModel] scoped to the destination".

My understanding is that if a viewModel is scoped to a destination, whenever the navcontroller navigates to another destination, the current destination's viewModel should be destroyed ?

Am I understanding incorrectly or am I doing something wrong in the implementation ?


Solution

  • Your understanding is incorrect, when you go from First to Second the First viewmodel remains alive because the First destination is in the backstack.

    When you navigate from Second to First the Second destination is popped from the stack and the Second viewmodel will be destroyed.

    This all works as described in the docs.