Jetpack ViewModel initialization
ViewModels in Android Jetpack is an awesome approach for decoupling business logic from Activities/Fragments. And is extremely easy to get started with.
You just extent a class with ViewModel()
and then you use a delegated property to retrieve it: val model by viewModels<MyScreenViewModel>()
.
The challenging bit comes when you need to use it in a real-world situation. You would need to initialize the ViewModel
and possibly pass some initial values. But where do you place the initialize logic and how do you pass those initial values when you don't create the object yourself?
What not to do
You might think that to be consistent you should create a custom initialization method that you can call, either with parameters or without.
This will require choosing a lifecycle method in your Activity/Fragment to call that initialization method. But this beats the purpose of the Jetpack ViewModel
: to survive destruction and recreation of those system view classes (e.g. on device rotation). This means that your initialization method will be called more than once per ViewModel
instance (e.g. each time the device is rotated, instead of a single time when the ViewModel
is created).
You might solve this by keeping an isInitialized
state in ViewModel but this is just extra overhead that can be avoided.
Initialize with no parameters
Just use the ViewModel constructor. This is what is recommended in the official docs and can work quite well with coroutines. Start the async work here and use LiveData
to deliver the results to the UI when ready.
class MyViewModel: ViewModel() {
val someLiveData = MutableLiveData<String>()
init {
viewModelScope.launch {
// This coroutine will be canceled when the ViewModel is cleared.
someLiveData.value = SomeDataRepository.fetchData()
}
}
}
For even less boilerplate, you could use the liveData
builder. This allows you to call a suspending function and return the results as a LiveData
. No need to manually create and maintain the LiveData
/ MutableLiveData
.
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(SomeDataRepository.fetchData()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
Initialize with parameters
Now, what happens if you want to initialize your ViewModel with some initial value? This might be a passed argument to a new screen for instance.
In this case, you can add the parameters in the ViewModel
's constructor.
class MyScreenViewModel(private val someData: String) : ViewModel() {
[...]
}
But now, val model by viewModels<MyScreenViewModel>()
won't work anymore (since the framework won't be able to create an instance for you - what's the value of someData
?).
To instruct the framework on how to create ViewModel
instances for you, you need to define a ViewModel factory.
class MyScreenViewModelFactory(val someData: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
modelClass.getConstructor(String::class.java)
.newInstance(someData)
}
Then in your Activity/Fragment retrieve the ViewModel
by providing this factory as well.
private val model by viewModels<MyScreenViewModel> {
MyScreenViewModelFactory("Hello world")
}
Note that this work great with Jetpack's Navigation arguments (e.g. MyScreenViewModelFactory(args.myArgument)
).
Of course, there are ways to reduce the required boilerplate (check this extension function approach by Pzychotix) but I prefer the explicit way for not adding another layer of abstraction.
There are other more unconventional ways to initialize a ViewModel
but I find the official way the most efficient and reliable, even if it requires a bit more boilerplate.
Happy coding!