'Jetpack Compose + Hilt: java.lang.RuntimeException: Cannot create an instance of class ViewModel
I started trying jetpack Compose recently and used hilt & hilt-navigation-compose for my app. It works fine with the first ViewModel. However, when I try the same code on another ViewModel of my second screen, it always crashes with java.lang.RuntimeException: Cannot create an instance of class. But if I remove the parameter repository(Injected using Hilt) in its constructor, it can be initialized successfully.
Currently in my navigation graph there are four screens.HOME, RECORD, SETTING screen can be navigated to via BottomBar, and their viewModel works properly;NEW screen can be navigated to via FloatingActionButton, but its viewModel cannot be created like the others. I can't figure out the problem. Here is my backstack trace:
java.lang.RuntimeException: Cannot create an instance of class com.eynnzerr.cpbookkeeping_compose.ui.new.NewViewModel
...
Caused by: java.lang.InstantiationException: java.lang.Class<com.eynnzerr.cpbookkeeping_compose.ui.new.NewViewModel> has no zero argument constructor
Seems that Hilt only knows how to create NewViewModel with no-arg constructor and neglects its parameter repository?
Here is my code: (1)Hilt Application class:
@HiltAndroidApp
class CPApplication: Application() {
override fun onCreate() {
super.onCreate()
context = applicationContext
}
companion object {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
}
(2)MainActivity:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookkeepingApp()
}
}
}
@ExperimentalAnimationApi
@Composable
fun BookkeepingApp() {
CPBookkeepingcomposeTheme {
val navController = rememberNavController()
BasicScreen(
navController = navController
)
}
}
@ExperimentalAnimationApi
@Composable
fun BasicScreen(
navController: NavController
) {
...
Scaffold(
topBar = {
//Change according to currentScreen in composable.
CPTopBar(currentScreen)
},
floatingActionButton = {
//only show up when it's HOME screen.
if(currentScreen.value == Destinations.HOME_ROUTE) {
DraggableFloatingButton(
onClick = {
navController.navigateTo(Destinations.NEW_ROUTE)
}
)
} else Unit
},
bottomBar = {
//only show up when it's HOME, RECORD, SETTING screens.
AnimatedVisibility(listState.isScrollingUp()) {
when(currentScreen.value) {
Destinations.HOME_ROUTE,
Destinations.RECORD_ROUTE,
Destinations.SETTING_ROUTE -> FlutterNavigation(navController = navController, items)
else -> Unit
}
}
}
) {
NavGraph(
navController = navController as NavHostController,
listState = listState
)
}
}
(3)NavGraph:
@Composable
fun NavGraph(
navController: NavHostController = rememberNavController(),
listState: LazyListState = rememberLazyListState(),
startDestination: String = Destinations.HOME_ROUTE
) {
NavHost(navController = navController, startDestination = "home") {
composable(Destinations.HOME_ROUTE){
//can create ViewModel.
val homeViewModel: HomeViewModel = hiltViewModel()
...
}
...
composable(Destinations.NEW_ROUTE){
//cannot create ViewModel?
val newViewModel: NewViewModel = hiltViewModel()
...
}
}
}
(4)HomeViewModel(Which works well):
data class HomeUiState(
val billsToday: List<Bill> = emptyList(),
val homeData: HomeData = HomeData()
)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val billRepository: BillRepositoryImpl
): ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState
init {
viewModelScope.launch {
_uiState.update { it.copy(homeData = getAllData(0f)) }
billRepository.getBillsFlow().collect { bills ->
_uiState.update { it.copy(billsToday = bills) }
}
}
}
}
(5)NewViewModel(which cannot be created except for deleting billRepository):
data class TabState(
val amount: String = "¥0",
val selectedIndex: Int = 0
)
@HiltViewModel
class NewViewModel @Inject constructor(
private val billRepository: BillRepositoryImpl //After deleting this, it can work properly
) : ViewModel() {
//uiState for expenseTab
private val _exTabState = MutableStateFlow(TabState())
val exTabState: StateFlow<TabState> = _exTabState.asStateFlow()
//uiState for revenueTab
private val _reTabState = MutableStateFlow(TabState())
val reTabState: StateFlow<TabState> = _reTabState.asStateFlow()
private val _remarkState = MutableStateFlow("add remark")
val remarkState: StateFlow<String> = _remarkState.asStateFlow()
fun updateRemark(remark: String) {
_remarkState.update { remark }
}
fun updateTab(category: Int, amount: String, selectedIndex: Int) {
when(category) {
-1 -> _exTabState.update { it.copy(amount = amount, selectedIndex = selectedIndex) }
1 -> _reTabState.update { it.copy(amount = amount, selectedIndex = selectedIndex) }
}
}
fun insertBill(bill: Bill) {
viewModelScope.launch {
billRepository.insertBills(bill)
}
}
}
(6)billRepository:
class BillRepositoryImpl @Inject constructor() : BillRepository {
private val billDao = BillDatabase.getInstance(context).getDao()
override suspend fun getBillsFlow(): Flow<List<Bill>> = billDao.getAllBills()
override suspend fun insertBills(vararg bill: Bill) = billDao.insertBills(*bill)
override suspend fun deleteBills(vararg bill: Bill) = billDao.deleteBills(*bill)
override suspend fun updateBills(vararg bill: Bill) = billDao.updateBills(*bill)
}
interface BillRepository {
suspend fun getBillsFlow(): Flow<List<Bill>>
suspend fun insertBills(vararg bill: Bill)
suspend fun deleteBills(vararg bill: Bill)
suspend fun updateBills(vararg bill: Bill)
}
(7)build.gradle(project):
buildscript {
ext {
compose_version = '1.1.0-beta02'
hilt_version = "2.37"
work_version = "2.7.1"
datastore_version = "1.0.0"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
(8)build.gradle(app):
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
}
apply plugin: 'kotlin-kapt'//hilt support
apply plugin: 'dagger.hilt.android.plugin'//hilt support
android {
compileSdk 31
defaultConfig {
applicationId "com.eynnzerr.cpbookkeeping_compose"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"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'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.5.31'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation "io.github.vanpra.compose-material-dialogs:datetime:0.6.2"
implementation "androidx.datastore:datastore-preferences:$datastore_version"
implementation "androidx.datastore:datastore:$datastore_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
def room_version = "2.4.0-beta02"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0-beta01"
implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.3.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.material:material-icons-core:$compose_version"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
Solution 1:[1]
I got the answer right after I raised this question... It seems that U can't name a package by "new", which I did in my app: com.eynnzerr.cpbookkeeping_compose.ui.new.NewViewModel For it's reserved and U can't even use it to name your package... After editing it my problem got solved...
Solution 2:[2]
I just had similiar problem and thankfully I found this topic. In my case I named my namespace "messages" and renaming that namespace worked
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Eynnzerr |
Solution 2 | Filip Mijalski |