Dagger-Hilt is fine and all.
Unfortunately, it arrived a bit too late in the android dev lifecycle. If I had to guess, most projects out there are still stuck using plain Dagger2.
The official google android docs use Hilt (it really helps on brevity 😅) to showcase Jetpack Compose code.
What if you are stuck in Dagger2 limbo though?
TL;DR
Housekeeping
This post will mainly explore how to scope a ViewModel to a destination using old-school Dagger2 and Navigation Compose.
To jolt your memory, an activity can instantiate a ViewModel with the by viewModels() Kotlin property delegate and pass it as a parameter into any composable.
However, this would result in an activity scoped ViewModel!
If this is what you need, feel free to stop reading.
If not, then read on.
Hilt refresher
Providing a ViewModel with Hilt is pretty straightforward.
Annotate the ViewModel with @HiltViewModel and provide it via the viewModel() function.
Navigation Compose library also provides hiltViewModel() to help with scoping a ViewModel to a destination.
Dagger2 + ViewModel speedrun
A ViewModel factory is needed.
This factory can be provided by the application component and injected at the activity/fragment level.
It doesn’t know how to build any our ViewModels though.
Dagger multibindings help make the solution generic enough for all possible ViewModels that could be used in a project.
Declare an annotation.
MainViewModel is good enough in order to try this out. (SomeDependency could be anything)
To round things off, a dagger module is needed to give instructions how to provide this MainViewModel.
In action
Time to wire up a few routes into a NavHost.
Notably, they all have MainViewModel as a parameter. The actual instance used for every route is important (this can be checked by printing the hashCode in the init block).
Every destination/route composable is the owner of its own MainViewModel instance.
Meaning:
The specific instance is scoped to the destination.
The same instance is retrieved when rotating the device.
When the composable leaves the stack entirely, for example when pressing back from ThirdRouteScreen to the SecondRouteScreen, the the MainViewModel instance scoped to ThirdRouteScreen will be destroyed.
What about the activity
Some routes in the MainActivity will do for now. More details on the why/how can be found in the docs.
The end?
One could say that all this works well enough and accomplishes the original goal.
For the bonus round — the “fun” part — let’s dig a little bit deeper.
Composable metrics
If you haven’t already, I would strongly suggest reading through the excellent Composable metrics article by Chris Banes.
TLDR: The compose compiler can export metrics on how “performant” composables are.
In layman’s terms — depending on the composable parameters, the compiler would know in advance to avoid recomposition if the inputs have not changed.
This would be nice! We don’t want composable functions being called again for no reason.
The goal? The compiler really likes composables declared as restartable skippable in the ...-composables.txt file.
Let’s try it
After running the metrics (via gradle command) on the release build, the composables can be found in the output.
They are just restartable 🫠.
The viewmodelFactory is considered unstable. The compiler really does not know what to do with this interface coming from the lifecycle library.
Is it stable? Is it immutable? As it cannot know, it marks it as unstable.
Let’s fix that then
Compose compiler loves functions. Retrieving the factory from a function should do the trick, theoretically.
Change the viewModelFactory into a function and use remember on it. Pass it as parameter to the composables.
The route composables have to change too, i.e:
Confirming via metrics
Running the metrics again (with --rerun-tasks, to ensure that the Compose Compiler runs, even when cached), the output has now changed.
Everything is restartable skippable 😊.
As for unstable viewModel... , I really am not too sure what to do about it.
Leave a comment if you know more about how this inline viewModel() function is treated by the compiler. It is marked as @dynamic and it doesn’t seem to affect the metrics output negatively is all I can see.🤷♂️
Was this even worth it?
This could really be called a micro-optimization. It can be useful as an exercise to the reader. Probably not much more than that.
In a real world project with a few hundred thousand LOC, it’s debatable if one should spend time on this instead of actually implementing a feature.
Personally, I have found that “route” composables are pretty much impossible to keep restartable and skippable. Things get out of hand quickly and you end up passing 30 different parameters of debatable stability. Oh well.
The end (for real this time)
Later.
Comments