androidandroid-jetpack-composeandroid-viewmodeldagger-hilt

androidx.lifecycle:lifecycle.viewmodel.compose vs androidx.hilt:hilt.navigation.compose


Following Android documentation from various places:

And this StackOverflow answer: How to add Hilt dependencies on "libs.versions.toml" file in android studio

I tried to make using androidx.lifecycle:lifecycle.viewmodel.compose work, but I still get the following error that I couldn't fogure out even after a few days:

FATAL EXCEPTION: main (Ask Gemini)
                                                                                                    Process: com.app, PID: 5391
                                                                                                    java.lang.RuntimeException: Cannot create an instance of class com.app.ui.login.LoginViewModel
                                                                                                        at androidx.lifecycle.viewmodel.internal.JvmViewModelProviders.createViewModel(JvmViewModelProviders.kt:40)
                                                                                                        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.android.kt:193)
...

Following is my work trying to make a very simple Login page work:

libs.version.toml

[versions]
agp = "8.7.3"
kotlin = "2.0.0"
coreKtx = "1.10.1"
...
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
#navigation
navVersion = "2.8.5"
jsonSerializationVersion = "1.7.3"
#hilt
kspVersion = "2.0.0-1.0.24"
hiltVersion = "2.51.1"
lifecycleViewmodelComposeVersion = "2.8.7"

[libraries]
#default generated
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
...
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-material3 = { group = "androidx.compose.material3", name = "material3" }
# password visibility icon
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended"}
#----
#----
#navigation
androidx-navigation-compose = {group = "androidx.navigation", name = "navigation-compose", version.ref = "navVersion"}
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jsonSerializationVersion"}
#----
#hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltVersion"}
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltVersion"}
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelComposeVersion"}

[plugins]
#default generated
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" }
#----
#navigation
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
#hilt
kotlinAndroidKsp = { id = "com.google.devtools.ksp", version.ref = "kspVersion"}
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hiltVersion"}

build.gradle.kts(App)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    //generated
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
    //----
    //hilt
    alias(libs.plugins.hiltAndroid) apply false
    alias(libs.plugins.kotlinAndroidKsp) apply false
}

build.gradle.kts(:app)

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    //navigation
    alias(libs.plugins.kotlin.serialization)
    //hilt
    alias(libs.plugins.hiltAndroid)
    alias(libs.plugins.kotlinAndroidKsp)
}

android {
    namespace = "com.app"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.app"
        minSdk = 33
        targetSdk = 34
        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
    }
}

dependencies {

    //default generated
    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)
    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)
    // password visibility icon
    implementation(libs.androidx.material.icons.extended)
    //----
    //----
    //navigation
    implementation(libs.androidx.navigation.compose)
    implementation(libs.kotlinx.serialization.json)
    //----
    //hilt
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    //----
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        ...
        tools:targetApi="31"
        android:name=".App">
        <activity
            android:name=".MainActivity"
            ...
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

App.kt


import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App: Application()

MainActivity.kt


import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.app.ui.theme.AppTheme
import com.app.ui.login.Login
import dagger.hilt.android.AndroidEntryPoint

private const val TAG = "MainActivity"
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Main(
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Main(modifier: Modifier = Modifier) {
    val navController = rememberNavController()
    NavHost(navController, startDestination = Login) {
        composable<Login> {
            Login(
                modifier = modifier,
                onRegisterClick = { Log.d(TAG, "Nav to register success")/*navController.navigate(route = Register)*/ },
                onLoginSuccess = { Log.d(TAG, "Login Success") }
            )
        }
    }
}

Login.kt


import androidx.compose.foundation.layout.Arrangement
...
import androidx.lifecycle.viewmodel.compose.viewModel
import com.app.ui.shared.PasswordField
import com.app.ui.shared.UsernameField

@Composable
fun Login(
    modifier: Modifier = Modifier,
    vm: LoginViewModel = viewModel(),
    onRegisterClick: () -> Unit = {},
    onLoginSuccess: () -> Unit = {}
) {
    //Also tried: val vm:LoginViewModel by viewModel()
    val uiState by vm.uiState.collectAsStateWithLifecycle()
    val kbController = LocalSoftwareKeyboardController.current

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        val textFieldModifier = Modifier.fillMaxWidth().padding(16.dp,4.dp)
    
        UsernameField(uiState.username, vm::onUsernameChange, textFieldModifier)
        PasswordField(uiState.password, vm::onPasswordChange, textFieldModifier)
    
        Button(
            onClick = {
                kbController?.hide()
                vm.onLoginClick(onLoginSuccess = onLoginSuccess)
            },
            modifier = Modifier.fillMaxWidth().padding(16.dp,8.dp)
        ) {
            Text(text = "Sign In", fontSize = 16.sp)
        }
        ...
    }

}

LoginViewModel.kt


import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.app.domain.BasicAuthUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
...

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val basicAuthUseCase: BasicAuthUseCase
): ViewModel() {

    private val _uiState = MutableStateFlow(LoginState())
    val uiState: StateFlow<LoginState> = _uiState.asStateFlow()
    
    fun onUsernameChange(username: String) =
        _uiState.update { it.copy(username = username) }
    fun onPasswordChange(password: String) =
        _uiState.update { it.copy(password = password) }
    fun onLoginClick(onLoginSuccess: () -> Unit) {
        ...
    }
    fun hideAfterDelay(duration: Long){
        ...
    }

}

HiltBindings.kt


import com.app.domain.BasicAuthUseCase
import com.app.domain.BasicAuthUseCaseImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
abstract class HiltBindings {
    @Binds
    abstract fun bindBasicAuthUseCase(impl: BasicAuthUseCaseImpl): BasicAuthUseCase
}

BasicAuthUseCase.kt


interface BasicAuthUseCase {
    suspend fun verifyUser(username: String, password: String): Boolean
    suspend fun isUsernameExist(username: String): Boolean
    suspend fun registerUser(username: String,password: String): Boolean
}

Referring back to a few years ago where I managed to make it work, I made the following changes and managed to make it work by replacing androidx.lifecycle:lifecycle.viewmodel.compose with androidx.hilt:hilt.navigation.compose:

libs.version.toml


[versions]
...
#hilt
kspVersion = "2.0.0-1.0.24"
hiltVersion = "2.51.1"
#lifecycleViewmodelComposeVersion = "2.8.7"
hiltNavigationComposeVersion = "1.2.0"

[libraries]
...
#hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltVersion"}
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltVersion"}
#androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelComposeVersion"}
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationComposeVersion"}

[plugins]
...

build.gradle.kts(:app)


...

dependencies {

    ...
    //hilt
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)

//    implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.hilt.navigation.compose)
//----
}

Login.kt


import androidx.compose.foundation.layout.Arrangement
...
import androidx.hilt.navigation.compose.hiltViewModel
import com.app.ui.shared.PasswordField
import com.app.ui.shared.UsernameField

@Composable
fun Login(
    modifier: Modifier = Modifier,
    vm: LoginViewModel = hiltViewModel(),
    onRegisterClick: () -> Unit = {},
    onLoginSuccess: () -> Unit = {}
) {
...
}

Can I get help understanding what I did wrong with androidx.lifecycle:lifecycle.viewmodel.compose? It is supposedly the recommended solution (Compose and other libraries) but I couldn't make it work.


Solution

  • You need to call hiltViewModel() to obtain a view model when using Hilt, not viewModel().

    The function is located in this dependency, which you need to add to your build setup:

    androidx.hilt:hilt-navigation-compose:1.2.0
    

    Maybe you got that confused with

    androidx.lifecycle:lifecycle-viewmodel-compose
    

    wich you have grouped together with the other Hilt dependencies. This one, however, isn't related to Hilt. It provides the viewModel() function (which you don't need anymore). You can probably remove it, although it will likely still be transitively used.