ViewModel Factory (Old vs. Newer DSL)
Old vs. New
IMPORTANT
If you use big Dependency Injection frameworks like Hilt or Koin, you can skip this. But if you're a fan of cleaner, boilerplate-free code in smaller projects, or just hate writing ViewModelProvider.Factory classes, you'll want to see this massive improvement in how we handle ViewModel injection.
Summary
I used to use an older-style Factory method to create my ViewModel which I have now moved away from completely. I'll show you my current pattern using DSL.
Situation
I tend to avoid Dependency Injection frameworks in my project as generally they are quite small. I was often able to create ViewModels without needing any external input and so things were quite easy.
When I did need to inject something into my ViewModel, I simply used a Factory method. This was no big deal and it was quite easy to copy/paste the boilerplate code. The reason I used it for so long was it was quite common for a lot of online code resources to reference this style.
I recently discovered a much nicer way to do this and then further improved on that a little.
Code Examples
Old - Here's what the older factory looked like and how it was created
class Factory(
private val name: String
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
if (modelClass.isAssignableFrom(
OldStyleFactoryViewModel::class.java)) {
val handle = extras.createSavedStateHandle()
@Suppress("UNCHECKED_CAST")
return OldStyleFactoryViewModel(
name = name,
savedStateHandle = handle
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
New - This is the newer DSL version
fun factory(name: String) = viewModelFactory {
initializer {
ModernDSLStyleFactoryViewModel(
name = name,
savedStateHandle = createSavedStateHandle()
)
}
}
Both are created using the same pattern so I'll just include the older one here. I've split it out into a function just so it's a little easier.
@Composable
private fun getOldFactoryVm(): OldStyleFactoryViewModel {
return viewModel(factory = OldStyleFactoryViewModel
.Factory("Old Style ViewModel Factory"))
}
In both cases, we're just injecting a string which becomes the starting value for our UI Title.
Of note is the fact that we're also passing in a savedStateHandle to deal with Process Death (previous post).
Final Improved Version!
We can make this even easier. For the modern DSL example, we can have both the factory and a get function as a companion object.
companion object {
private const val MODERN_UI_STATE_KEY = "modern_ui_state"
@VisibleForTesting
internal fun factory(name: String) = viewModelFactory {
initializer {
ModernDSLStyleFactoryViewModel(
name = name,
savedStateHandle = createSavedStateHandle()
)
}
}
@Composable
fun get(
name: String = "Modern DSL ViewModel Factory"
): ModernDSLStyleFactoryViewModel {
return viewModel(factory = factory(name))
}
}
Notice: I've added @VisibibleForTesting and internal to allow the factory to be accessed outside of a composable.
This in turn makes creating the ViewModel and passing arguments extremely simple. I much prefer grouping everything together like this.
val viewModel= ModernDSLStyleFactoryViewModel.get()
orval viewModel= ModernDSLStyleFactoryViewModel.get("example text")
Full Code List
I've put the full code listing up in case you want to play with this for yourself.
Below is a screenshot of the basic UI. The UI doesn't really do anything interesting, it just proves everything works and you can test that it survives process death if interested.
Dependencies
[versions]lifecycle = "2.10.0" # Originally created on 2.9.4
[libraries]
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
[plugins]
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
plugins {
alias(libs.plugins.kotlin.parcelize) apply false
}
Links
Updated
Bonus - If Needed - External Constructors
fun modernDslStyleVmFactory(name: String) = viewModelFactory {
addInitializer(
ViewModelFactoryComparison.ModernDSLStyleFactoryViewModel::class
) {
val savedStateHandle = createSavedStateHandle()
ViewModelFactoryComparison.ModernDSLStyleFactoryViewModel(
name = name,
savedStateHandle = savedStateHandle
)
}
}
fun oldStyleVmFactory(name: String) = viewModelFactory {
addInitializer(
ViewModelFactoryComparison.OldStyleFactoryViewModel::class
) {
val savedStateHandle = createSavedStateHandle()
ViewModelFactoryComparison.OldStyleFactoryViewModel(
name = name,
savedStateHandle = savedStateHandle
)
}
}
Comments
Post a Comment