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