Search code examples
androidkotlinmvvmsharedpreferencesviewmodel

Cannot create an instance of class SharedPrefViewModel in Kotlin


I have an app where I'm using SharedPreferences. When I try to get the saved name this error occurs: java.lang.RuntimeException: Cannot create an instance of class com.meetme.viewmodel.SharedPrefViewModel

What could be wrong in code below? Any ideas?

SharedPrefViewModel class:

class SharedPrefViewModel(context: Context): ViewModel() {
    private var sharedPrefRepository: SharedPrefRepository = SharedPrefRepository(context.applicationContext)
 
    fun putString(name: String) {
        sharedPrefRepository.putString(name)
    }
    fun getString() : String? {
        return sharedPrefRepository.getString() <-- here is the error
    }
}

SharedPrefRepository class:

class SharedPrefRepository(context: Context) {
    private val pref: SharedPreferences =
        context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)

    var name: String?
    get() = pref.getString(PREF_LOGGED_IN, "")
    set(value) = pref.edit().putString(PREF_LOGGED_IN, value).apply()
    
    fun putString(name: String) {
        this.name = name
    }

    fun getString() : String? {
        return this.name
    }
}

Composable, where I'm using prefs to save a name:

@Composable
fun BasicInfoScreen(navController: NavHostController) {
    var context = LocalContext.current.applicationContext
    val sharedPrefViewModel = SharedPrefViewModel(context)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(16.dp)
    ) {
        ...
        Button(
            onClick = {
                sharedPrefViewModel.putString("default") <---- here
                navController.navigate(RootGraphItem.Location.route)
            },
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
                .background(Color(0xFF0090FF)),
            colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF0090FF))
        ) {
            Text(text = "Next", style = TextStyle(color = Color.White))
        }
    }
}

Composable that gets a value from prefs:

@Composable
fun LocationScreen(navController: NavHostController) {
    val sharedPrefViewModel: SharedPrefViewModel = viewModel()

    println(sharedPrefViewModel.getString()) <-- get name

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(16.dp)
    ) {
       ...
    }
}

Solution

  • All ViewModels are created using a Factory. If your ViewModel constructor doesn't fit something that the default factory can handle, then you have to create your own ViewModelProvider.Factory implementation that can create your ViewModel, and you must pass it to the viewModel() function in your composable.

    You must never directly call your ViewModel constructor except inside its factory! This: val sharedPrefViewModel = SharedPrefViewModel(context) will cause problems because your ViewModel is not created through the ViewModelProvider, so it is not registered and so it will not have a proper lifecycle and it will not use a single shared instance by different screens/composables that use it.

    The default factory is only capable of creating instances of your ViewModel if your ViewModel constructor's arguments are one of the following:

    • constructor() Empty constructor (no arguments)
    • constructor(savedStateHandle: SavedStateHandle)
    • constructor(application: Application)
      • Also requires that you subclass AndroidViewModel, not just ViewModel.
    • constructor(application: Application, savedStateHandle: SavedStateHandle)
      • Also requires that you subclass AndroidViewModel, not just ViewModel.

    Notice, your constructor takes a single Context argument, so it doesn't fit any of the above. If you want to avoid having to create a factory, which is a lot of boilerplate, I would change your class as follows so it fits the third one above:

    class SharedPrefViewModel(application: Application): AndroidViewModel(application) {
        private val context: Context = application.applicationContext
    
        //...
    }
    

    And don't forget to fix the line where you called your constructor instead of calling viewModel()!