1

I am struggling to implement the DataStore preferences library in Android Jetpack Compose to persist some user settings in my app. Whenever I try to access the SettingsViewModel from my Composable component, the app crashes and I receive the following error:

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.boldmethod, PID: 5415
java.lang.RuntimeException: Cannot create an instance of class com.boldmethod.Models.SettingsViewModel
    ...
 Caused by: java.lang.InstantiationException: java.lang.Class<com.packageName.Models.SettingsViewModel> has no zero argument constructor

I'm following the docs to create the DataStore and view model, so perhaps I'm not using them correctly in the Composable. here is the relevant source code:

Gradle

dependencies {

// Kotlin/Core
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.20"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'

// Compose
implementation "androidx.compose.ui:ui:1.0.0-alpha08"
implementation "androidx.compose.material:material:1.0.0-alpha08"
implementation "androidx.compose.runtime:runtime:1.0.0-alpha08"
implementation "androidx.compose.runtime:runtime-livedata:1.0.0-alpha08"
implementation "androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha08"

// Navigation
def nav_compose_version = "1.0.0-alpha03"
implementation "androidx.navigation:navigation-compose:$nav_compose_version"

// Architecture
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
//implementation "android.arch.lifecycle:runtime:2.1.0"

// UI/Material
implementation 'com.google.android.material:material:1.2.1'
implementation "androidx.ui:ui-tooling:1.0.0-alpha07"

// Testing
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

User Preferences repository that creates DataStore

class UserPreferencesRepository private constructor(context: Context) {

data class UserPreferences(
    val resolution: String,
    val theme: String,
)

private val dataStore: DataStore<Preferences> =
    context.createDataStore(name = "userPreferences")

private object PreferencesKeys {
    val RESOLUTION = preferencesKey<String>("resolution")
    val THEME = preferencesKey<String>("theme")
}

// Read from the DataStore with a Flow object.
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get values or a default value.
        val resolution = preferences[PreferencesKeys.RESOLUTION] ?: "HD"
        val theme = preferences[PreferencesKeys.THEME] ?: "Auto"
        UserPreferences(resolution, theme)
    }

// Edit the DataStore.
suspend fun updateResolution(resolution: String) {
    dataStore.edit { preferences ->
        // when statement here to change more than just res.
        preferences[PreferencesKeys.RESOLUTION] = resolution
    }
}
}

Settings ViewModel

class SettingsViewModel(
userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {

private val _resolution = MutableLiveData("HD")
val resolution: LiveData<String> = _resolution

fun onResolutionChanged(newResolution: String) {
    
    when (newResolution) {
        "540p" -> _resolution.value = "720p"
        "720p" -> _resolution.value = "HD"
        "HD" -> _resolution.value = "540p"
    }
}
}

Settings Component

@Composable
fun Settings(
settingsViewModel: SettingsViewModel = viewModel(),
) {

val resolution: String by settingsViewModel.resolution.observeAsState("")

ScrollableColumn(Modifier.fillMaxSize()) {
    Column {
        ...
        Card() {
            Row() {
                Text(
                    text = "Resolution",
                )
                Text(text = resolution,
                    modifier = Modifier.clickable(
                        onClick = { settingsViewModel.onResolutionChanged(resolution) },
                        indication = null))
            }
        }
        ...
    }
}

}

Trying to get it to work with Compose has been a struggle for me and I would love any help. Thanks so much!

0

1 Answer 1

5

Since your SettingsViewModel uses UserPreferencesRepository as a constructor parameter, you have to provide a factory method to instantiate your ViewModel:

class SettingsViewModelFactory(private val userRepository:UserRepository) : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) {
                return SettingsViewModel(userRepository) as T
            }
            throw IllegalArgumentException("Unknown ViewModel class")
        }
    }

val userRepository = UserRepository(context)
val viewModel: SettingsViewModel by viewModels { SettingsViewModelFactory(userRepository)}

If you don't provide a factory to viewModels(), the defaultViewModelProviderFactory will be used, which can only instantiate viewModels that have a zero arg constructor

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

1 Comment

JNS, thanks so much amigo this is the second Jetpack question you've answered for me this week; you're an allstar! Your answer got me rolling on the right track to find the solution, seriously thanks.

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.