androidandroid-jetpack-composeandroid-roomviewmodeldagger-hilt

How to fix "Cannot create an instance of class ViewModel" when using Hilt?


I am doing a JetPack Compose course and I am trying to run an example project showcasing use of ViewModel together with Hilt.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            Surface(color = MaterialTheme.colorScheme.background) {
                val noteViewModel: NoteViewModel = hiltViewModel<NoteViewModel>()
                // NoteApp(noteViewModel)
            }
        }
    }
}

To initialize the ViewModel I also tried

 noteViewModel by viewModels()

and

 noteViewModel = hiltViewModel()

The ViewModel code is as follows:

@HiltViewModel
class NoteViewModel @Inject constructor(private val repository: NoteRepository) : ViewModel() {
    //private var noteList = mutableStateListOf<Note>()

    private val _noteList = MutableStateFlow<List<Note>>(emptyList())
    val noteList = _noteList.asStateFlow()

    init {
        //noteList.addAll(NotesDataSource.loadDataNotes())
        viewModelScope.launch(Dispatchers.IO) {
            repository.getAllNotes().distinctUntilChanged().collect { listOfNotes ->
                if (listOfNotes.isEmpty()) {
                    Log.d("JetNote", "EmptyList")
                } else {
                    _noteList.value = listOfNotes
                }
            }
        }
    }

    fun addNote(note: Note) = viewModelScope.launch {
        repository.addNote(note)
    }

    fun updateNote(note: Note) = viewModelScope.launch {
        repository.updateNote(note)
    }

    fun removeNote(note: Note) = viewModelScope.launch {
        repository.deleteNote(note)
    }
}

The AppModule setting for Hilt is as follows:

@InstallIn(SingletonComponent::class)
@Module
object AppModule {

    @Singleton
    @Provides
    fun provideNotesDao(noteDatabase: NoteDatabase) : NoteDatabaseDao = noteDatabase.noteDao()

    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context) : NoteDatabase =
        Room.databaseBuilder(
            context,
            NoteDatabase::class.java,
            "notes_db"
        ).fallbackToDestructiveMigration().build()
}

Code of NoteRepository:

class NoteRepository @Inject constructor(private val noteDatabaseDao: NoteDatabaseDao) {
    suspend fun addNote(note: Note) = noteDatabaseDao.insert(note)
    suspend fun updateNote(note: Note) = noteDatabaseDao.update(note)
    suspend fun deleteNote(note: Note) = noteDatabaseDao.deleteNote(note)
    suspend fun deleteAllNotes() = noteDatabaseDao.deleteAll()
    fun getAllNotes(): Flow<List<Note>> = noteDatabaseDao.getNotes().flowOn(Dispatchers.IO).conflate()
}

The error I get when running the application is:

java.lang.RuntimeException: Cannot create an instance of class com.course.jetnote.screens.NoteViewModel
........................
Caused by: java.lang.NoSuchMethodException: com.course.jetnote.screens.NoteViewModel.<init> []

And this is my Gradle setup:

build.gradle.kts:

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.course.jetnote"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.course.jetnote"
        minSdk = 26
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }

    packaging {
        resources {
            excludes += "/META-INF/gradle/incremental.annotation.processors"
            excludes += "META-INF/androidx/room/room-compiler-processing/LICENSE.txt"
        }
    }

}

configurations.implementation {
    exclude(group = "org.jetbrains", module = "annotations")
}


dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.androidx.lifecycle.viewModelCompose)
    implementation(libs.hilt.android)
    implementation(libs.hilt.compiler)
    implementation(libs.androidx.hilt.navigation.compose)
    implementation(libs.room.ktx)
    implementation(libs.room.runtime)
    implementation(libs.room.compiler)
    implementation(libs.kotlinx.coroutines.android)
    implementation(libs.androidx.room.runtime.android)

    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

lib.versions.toml:

[versions]
agp = "8.8.2"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
composeBom = "2024.04.01"
androidxLifecycle = "2.8.7"

androidxHiltNavigationCompose = "1.0.0"
hilt = "2.56.2"
hiltExt = "1.0.0"

room = "2.6.0"

kotlinxCoroutines = "1.10.0"
kotlinxDatetime = "0.6.0"
kotlinxSerializationJson = "1.8.0"
roomRuntimeAndroid = "2.7.1"

[libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
 junit = { group = "junit", name = "junit", version.ref = "junit" }
 androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
 androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
 androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
 androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
 androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
 androidx-ui = { group = "androidx.compose.ui", name = "ui" }
 androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
 androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
 androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
 androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
 androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
 androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
 androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }


hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }



#Database
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

#Kotlin Coroutines, serialization, datetime...
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
androidx-room-runtime-android = { group = "androidx.room", name = "room-runtime-android", version.ref = "roomRuntimeAndroid" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

Solution

  • Hilt (and Room) isn't properly set up in your Gradle files. To fix it, follow these steps:

    1. First off, you need the Kotlin Symbol Processing (KSP) plugin so Hilt can generate source code during compilation. You need this in your version catalog:

      [versions]
      ksp = "2.0.0-1.0.24"
      
      [plugins]
      ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
      

      The KSP version consists of two parts. The first part must always match your Kotlin version. Since you are using Kotlin 2.0.0, this is the version you must use. The second part is the KSP version itself. Whenever you update your Kotlin version, you must also update KSP. You can find a list of all KSP versions here: https://github.com/google/ksp/releases

      Now use it in your gradle files like you do with the other plugins:

      alias(libs.plugins.ksp) apply false
      

      respectively

      alias(libs.plugins.ksp)
      
    2. Now that you have KSP available, you need to switch your dependency

      implementation(libs.hilt.compiler)
      

      to

      ksp(libs.hilt.compiler)
      

      You also need to add the Hilt Gradle plugin, like you added KSP above:

      [plugins]
      hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
      

      and

      alias(libs.plugins.hilt.android) apply false
      

      respectively

      alias(libs.plugins.hilt.android)
      

    This should do it, Hilt should work now.


    You still have issues with your Room dependencies, though, so your app still won't work.

    First off, you mix the different Room versions room = "2.6.0" and roomRuntimeAndroid = "2.7.1". This won't work, you always need to use the same version. So let's replace both with this:

    room = "2.7.1"
    

    Also, you don't need to explicitly use the library androidx-room-runtime-android, so you can simply remove it from the version catalog, and it's usage implementation(libs.androidx.room.runtime.android) in your gradle file.

    Now, you have a similar issue here as you had with Hilt: You need to replace

    implementation(libs.room.compiler)
    

    with

    ksp(libs.room.compiler)
    

    You already have KSP in place, so this is all that is needed. Your app should run now.


    With regards to the packaging and configurations.implementation blocks in your gradle file that you mentioned in the comments, I do not see how that's necessary. You should probably remove them both for now and keep this kind of optimization for later, if at all.


    Just two additional points of interest you might want to have a look at:

    1. There is also a Room Gradle plugin available. Although not mandatory, it helps you with database migrations from one version to another.

    2. As already mentioned in the comments, your Flow handling is way off. The only thing you need in the view model is this:

      val noteList: StateFlow<List<Note>> = repository.getAllNotes().stateIn(
          scope = viewModelScope,
          started = SharingStarted.WhileSubscribed(5.seconds),
          initialValue = emptyList(),
      )
      

      This simply converts the database flow from the repository into a StateFlow. That's all it needs, you can remove the init block and the MutableStateFlow _noteList.
      As a rule of thumb, never collect flows in the view model. If you need to process the content of a flow (which you do not do here, but which you might want to in the future), you can use the various transformation functions available for flows, like map, flatMapLatest, combine and so on.

      In a similar vein, .flowOn(Dispatchers.IO).conflate() is redundant in your repository. With Kotlin coroutines, the part of the code that actually performs the critical work is responsible to switch to the proper dispatcher. In this case that is something that happens internally in the Room library. So you don't need to specify the dispatcher later on. And since StateFlows are always conflated (and you do nothing expensive until the database flow is converted to a StateFlow), you also don't need to explicitly do this here.