1

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" }
4
  • I added the code for the NoteRepository - it seems to me that it is set for dependency injection Commented Apr 27 at 17:01
  • The code works for me. Apart from the strange Flow handling (which doesn't affect your current issue) everything seems to be in order. You probably messed something up with your dependencies. Please edit the question to also provide your build files, including the version catalog. Commented Apr 27 at 17:26
  • I added build.gradle and versions.toml. In order to compile the project I had to add the packaging {resources {} } and configuration.implementations sections in build.gradle. Can this be the problem ? Commented Apr 27 at 17:40
  • Your app's build script does not have the Hilt Gradle Plugin applied, visit the link for more information on how to set it up (see also the Adding dependencies section of the Hilt doc on Android Developers) Commented Apr 27 at 17:53

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.