Search code examples
androidkotlinmvvmandroid-jetpack-composeandroid-viewmodel

Android: Correct way to initialize ViewModel with dependencies, ViewModelProviderFactory


I have two ViewModels that have dependencies on other objects that in turn might have dependencies on context (SettingsDataStore). Now to keep the context out of my ViewModels I am following the inversion of control principle.

My code looks something like this:

// MainActivity.kt
private val Context.dataStore by preferencesDataStore(name = "settings")
val newsRepository = NewsRepository()
val newsDetailViewModel = NewsDetailViewModel()

lateinit var newsViewModel: NewsViewModel
lateinit var settingsViewModel: SettingsViewModel


@Composable
fun Navigation() {
    val context = LocalContext.current
    val settingsDataStore = SettingsDataStore(context.dataStore)

    if (!::settingsViewModel.isInitialized) {
        settingsViewModel = viewModel(initializer = { SettingsViewModel(settingsDataStore) })
    }

    if (!::newsViewModel.isInitialized) {
        newsViewModel =
            viewModel(initializer = { NewsViewModel(newsRepository, settingsDataStore) })
    }
...

So as you can see I am creating the SettingsDataStore in the composable function because I need access to context. Then I am constructing the ViewModels with their dependencies. However, I suspect this kind of injection is faulty because of plenty of reasons -- invalidation of the context and accidental creation of ViewModels if you are not careful comes to mind.

Therefore, I did some research found this article of the official docs recommending the usage of ViewModelProvider.Factory. Unfortunately, I did not figure out how to properly create my NewsViewModel using a factory:

class NewsViewModel(
    private val repo: NewsRepository,
    private val settingsDataStore: SettingsDataStore
) : ViewModel() {

    ...
    
    // Define ViewModel factory in a companion object
    companion object {

        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel> create(
                modelClass: Class<T>,
                extras: CreationExtras
            ): T {
                // Get the Application object from extras
                val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
                // Create a SavedStateHandle for this ViewModel from extras
                val savedStateHandle = extras.createSavedStateHandle()

                return NewsViewModel(
                    // TODO HOW DO I ACCESS/CREATE THE DEPENDENCIES ??
                ) as T
            }
        }
    }
}

So my question is how do I create my NewsViewModel with that factory? I want to stick to the Android docs and not use some sort of DI framework.

Sorry for the long question but I wanted be detailed because a lot of people following proper MVVM architecture will run into this.


Solution

  • I ended up solving this problem by using the ViewModelProvider helper class in my Jetpack Compose app. If you are still using the classic XML approach have a look at @Tenfour04s approach. Also keep in mind that you should consider using a DI framework for bigger projects. So anyways, here is how I created my ViewModels properly with their respective dependencies:

    The Factory itself needed to be changed to take the depedencies as arguments in its constructor:

        class NewsViewModelFactory(
            private val newsRepository: NewsRepository,
            private val settingsDataStore: SettingsDataStore
        ) : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                if (modelClass.isAssignableFrom(NewsViewModel::class.java)) {
                    @Suppress("UNCHECKED_CAST")
                    return NewsViewModel(
                        settingsDataStore = settingsDataStore,
                        repo = newsRepository
                    ) as T
                }
                throw IllegalArgumentException("Unknown ViewModel class")
            }
        }
    

    Now we can just call ViewModelProvider in the Composable function and pass the viewModelStoreOwner and concrete factory to obtain the correct ViewModel:

    newsViewModel = ViewModelProvider(
            viewModelStoreOwner,
            NewsViewModel.NewsViewModelFactory(
                newsRepository, settingsDataStore
            )
        ).get()
    

    This will make sure that the ViewModel is correctly created. Every subsequent call will return the already created ViewModel which exactly what we want.

    The complete example looks like this now:

    // MainActivity.kt
    private val Context.dataStore by preferencesDataStore(name = "settings")
    val newsRepository = NewsRepository()
    val newsDetailViewModel = NewsDetailViewModel()
    
    lateinit var newsViewModel: NewsViewModel
    lateinit var settingsViewModel: SettingsViewModel
    
    
    @Composable
    fun Navigation() {
        val context = LocalContext.current
        val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        }
    
        val settingsDataStore = SettingsDataStore(context.dataStore)
    
        settingsViewModel = ViewModelProvider(
            viewModelStoreOwner,
            SettingsViewModel.SettingsViewModelFactory(settingsDataStore)
        ).get()
    
    
        newsViewModel = ViewModelProvider(
            viewModelStoreOwner,
            NewsViewModel.NewsViewModelFactory(
                newsRepository, settingsDataStore
            )
        ).get()
    ...