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.
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.