Adding a new library to a production project should always be done with extra care.
Jetpack introduced some months ago a ViewModel library with a class of the same name. This new class “store and manage UI-related data in a lifecycle conscious way”. It’s not really limiting to the MVVM pattern actually and can be inherited by your presenters, controllers,…
In this post, I presume you know the basics of ViewModel (if you don’t, there are many good resources out there) and we’ll try to go further and understand the implications of adding this library into a project.
One of the sexiest part of ViewModel, from my point of view, was the idea of ‘a simpler lifecycle’. Android lifecycles are quite complex, so this is quite a big deal for simplifying our code and spend more time on feature than on fixing lifecycle-related issues.
Let’s look at this super simple lifecycle!
This look sexy! All is handled in the body of the ViewModel, I don’t even need to remember lifecycle methods anymore…
And it handles re-creation! And I even have a method to clean stuff if needed!
Unfortunately you’ll always need to have lifecycles to know if you’re drawn, if you’re re-resumed, or re-started to play an animation at the right moment…
But first let’s take a bigger picture with this version:
(picture from The Android Lifecycle cheat sheet by Jose Alcérreca)
Let’s point out some details from this picture:
Activity
is createdActivity
is re-created with onCreate(Bundle)
onDestroy
of the Activity
(not very symetrical to the onCreate)And for the Fragment version
Fragment
is attached or created.onAttach
you cannot know if it exists or not)onDestroy
of the Fragment
(you don’t really know)The Kotlin extensions to retrieve a ViewModel use the lazy mechanism, so you think you deal with a non-null variable in your code but actually you deal with a “it depends” variable if it’s used inside the onAttach
or onCreate
methods. And the “it depends” is directly translatable by “maybe it will crash your app” since it’s generally used from the main thread.
Adds to that Fragment can be retained (onDestroy
/onCreate
no more called when device rotate), that there is unfrequent lifecycle methods like Activity#onRestart
, Activity#onNewIntent
, … or that flags on the Intent actually change the Activity method calls. Lifecycles are complex.
This is not really simplifying the lifecycles but adding a third one to the mix. New developers will still need to understand the Activity
/Fragment
lifecycles + the fact they can’t use their ViewModel in all lifecycle methods.
That’s said, if you limit your code in Activity / Fragment to the onCreateView
/ onStart
/ onResume
and the symmetrical onPause
/ onStop
/ onDestroyView
, you should be safe.
One of the big issue with Android and DI is the fact that the construction of Activities and Fragments are done by the system. As such, it’s impossible to inject our dependencies directly in the constructor, and we have to rely on field injection:
lazy { }
to class members to defer the work -> degrading performances due to additional useless inner classes)If after that you’ve still some doubts, google “field injection”.
Example of code where you’ll experiment a crash (NullPointerException) because using field injection:
class MyActivity: Activity() {
@Inject lateinit var myService: Service
private val url = myService.getUrl() // throws NullPointerException
fun onCreate() {
// Injecting your dependencies
}
}
Well, it’s almost the same story for Jetpack ViewModels by default. ViewModels are created by a Factory, that is by default provided by Android, so you can start immediately, and get system injections like the application Context
or the SavedStateHandle
. You can still use field injection but you’ll have the previous issues…
Thanksfully you can plug your Dagger dependencies to the ViewModelProvider! Right?
So if you use Dagger for example, you’ll have to do some “black magic” to plug your dependencies in the ViewModel. Stuff like handling maps of AbstractViewModelFactory
factories. Yep, factories of factories. Like if DI was already so easy that we feel the urge to add yet an additional layer of complexity.
A simple example of black magic from the android blueprints itself. Indeed if you take some time you’ll understand all the implications of this code, but as it’s not provided by the library, you’ll have to maintain it at some point…
One of the benefit of the Minimalist Dagger (and the multi-modules article) approach I presented before was that most of the modules don’t use kapt while using Dagger 2, and don’t even need to have a @Module
. So you’re reducing a lot the coupling with the DI library. If you “upgrade” to Jetpack ViewModel and use the code provided in the architecture blueprint, you now need a bind method for each ViewModel, like in the blueprint example :
@Module
abstract class StatisticsModule {
@Binds
@IntoMap
@ViewModelKey(StatisticsViewModel::class)
abstract fun bindViewModel(viewmodel: StatisticsViewModel): ViewModel
}
And now all your modules knows about Dagger (@Module
, @Binds
, @IntoMap
+ your custom annotation) and you run kapt in each one of them. For the build time I’m not sure, for the dependency to Dagger I presume it’s “ok-ish” (at best) as Android will invest more on this lib, but for all the developers thinking Dagger is too much verbose…
Did I tell you this black magic code was actually the simplest case?
What if you want to use a SavedStateHandle or inject the App context… or both? This approach is not enough, so you have 2 options: make more black magic DI code, or just make a big switch cases with all the constructors of all your ViewModels.
@Suppress("UNCHECKED_CAST")
class ViewModelFactory constructor(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
private val tasksRepository: TasksRepository
/* Plug all your module dependencies here */
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = with(modelClass) {
when {
isAssignableFrom(StatisticsViewModel::class.java) -> StatisticsViewModel(tasksRepository)
isAssignableFrom(TaskDetailViewModel::class.java) -> TaskDetailViewModel(tasksRepository)
isAssignableFrom(AddEditTaskViewModel::class.java) -> AddEditTaskViewModel(tasksRepository)
isAssignableFrom(TasksViewModel::class.java) -> TasksViewModel(tasksRepository, handle)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
Indeed the blueprints is not super scalable (maybe it’s not even intended to be scalable), but it means than before Jetpack, to add a ViewModel, you created a new class, and referenced the class in the Fragment, so modifying 2 files. Now you also need to create a dedicated module, and update your big switch case.
What if you want to add another dependency to your ViewModel? Again need to edit the “DI file”, add yet another field in the constructor of your ViewModelFactory… Does it go over your code-quality metrics now?
Also let’s say you’ve a DefaultViewModel that can be instantiated, but also 2 other subclasses with slightly different behaviors. Now the big switch case needs to be ordered to avoid a bad match, and documented because it’s not that obvious that order matters here…
Yes there are other approaches to the problem. But no default approach, or no viewmodel-dagger2 lib that does the work for you yet (this when
is pure boilerplate and could be automated), so you’ll have to find a good approach for you before anything else. I’m confident Android engineers will deliver a solution at some point, but when? And with what limitations?
Ok… let’s say we have copy-pasted some files from github or StackOverflow to make Dagger happy, now we’re good on DI right?
Well actually, before Jetpack ViewModel, Dagger2 was checking all dependencies and was ensuring everything was provided. It means than if it builds, you don’t have dependency issue. Is it still true?
No. As you can see in the when
, the ViewModelProvider.Factory
signature is based on a Class:
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
So you don’t have the choice than crash at runtime when the class given in parameters is not mapped in your code. Also, as we’ve seen in the Lifecycles part, you cannot create the ViewModel out of a specific lifecycle without crashing, so clearly the dependency injection with ViewModel is no more checked at compile-time. To get back the analysis at compile-time, you can indeed add another annotation processor library, costing more build time…
Using ViewModel allow to easily interact between Activity and Fragment. Let’s say for example there is a ViewModel at the Activity level and a Fragment want to notify of some changes, dead simple (with fragment-ktx 1.1.0+):
class MyViewModel {
fun getViewState(): MyViewState { ... }
fun updateStuff() { ... }
}
class MyActivity: Activity() {
private val myViewModel by viewModels<MyViewModel>()
override fun onResume() {
updateView(myViewModel.getViewState())
}
}
class MyFragment: Fragment() {
private val myViewModel by activityViewModels<MyViewModel>()
fun onClick() {
myViewModel.updateStuff()
}
}
This is really cool, there will be only one instance of the MyViewModel in this case. Before you were limited to:
Actually for Dagger users, that’s like there were 2 new scopes “ActivityScope” / “FragmentScope” and a dynamic Component for each activity started, so you don’t have to write them!
As a corollary, a custom view can use a ViewModel from its Activity or Fragment, but not from the view itself. You can still create a MyViewModel with Dagger and inject it in the view, but it will not use the ViewModel mechanism, so no SavedStateHandle
or onCleared()
, so MyViewModel doesn’t need to extend ViewModel.
Same for a ViewModel that needs another ViewModel, there is no solution as far as I know.
Note that the scopes are hidden, so if a fragment uses by viewModel<MyViewModel>()
and the activity uses by viewModel<MyViewModel>()
, both will have a distinct MyViewModel instance. Fragment should have used by activityViewModel<MyViewModel>()
to share the same instance. This kind of setup error will probably occurs at some point.
Now let’s say there is an Activity with a “master” ViewModel, and 2 Fragments, each one having its own ViewModel. The master requires the validation of the 2 sub ViewModels. How can we plug that?
class FragmentA : Fragment() {
val masterViewModel by activityViewModels<MasterViewModel>()
val viewModel by viewModels<AViewModel>()
fun onResume() {
masterViewModel.addSlave(viewModel)
// or
viewModel.onChange { masterViewModel.notifyAChanged() }
// or anything doing the glue between both ViewModels
}
}
Whatever the solution that fits your need, there is still a bit of glue. I’d love a solution to ask a ViewModel (if present) from another one, so that I don’t need to change the Activity/Fragment classes anymore. Please share in comments if you know a solution!
By the way, Activity has generally a longer scope than Fragment, so passing the ViewModel of the Fragment to an Activity ViewModel will probably lead to a memory leak.
A major benefits with SavedStateHandle
is the possibility to get the Activity’s intent extras
, or the Fragment’s arguments
from the ViewModel. When it’s pluggued (black magic DI), you’ll have a very pleasant experience:
class MyViewModel(savedStateHandle: SavedStateHandle): ViewModel() {
companion object {
private const val ARG_KEY = "ARG_KEY"
fun bundle(userId: String) = bundleOf(ARG_KEY to userId)
}
private val userId: String? = savedStateHandle.get<String>(ARG_KEY)
init { println(userId) }
}
class MyFragment: Fragment() {
companion object {
fun newInstance(userId: String) = MyFragment()
.apply { arguments = MyViewModel.bundle(userId)}
}
}
A big win here, you can add parameters to the bundle without modifying your view anymore.
This is indeed completely up to you!
But just to sum up my feelings about it:
Topic | without ViewModel | Jetpack ViewModel |
Lifecycle complexity | 😔 | 😔 |
DI complexity | 👌 | 😔 |
Checks DI at compile-time | 😍 | 😔 |
Sharing ViewModel | 😔 | 😍 |
Communication between ViewModel | 😐 | 😐 |
Handle saved states | 😔 | 😍 |
Passing args to ViewModel | 😐 | 😍 |
There are serious drawbacks to go with Jetpack ViewModel, but it’s also helping to clean and reduce your glue code. In my main project, we’ve chosen to go further with ViewModel, but first we worked on some approaches to mitigate the drawbacks. Stay tuned for the next articles on that matter.