Photo by Markus Spiske on Unsplash
I assume the reader has knowledge and strong base concepts of Kotlin, Dagger, and Android App Architecture.
Intro
The purpose of this article is to show an Activity evolution — can also be applied to a Fragment — to accommodate a ViewModel setup. Throughout this journey we’ll see how dependency injection will fit in by using the following approaches:
- Dagger (Vanilla Dagger)
- Dagger Android
- Hilt
This will not teach how to use Dagger nor Hilt, instead, it will show the setup I found useful — on each approach — to provide ViewModels with their dependencies, SavedStateHandle and intent.extras
. I’ll add all the references that helped me achieve this goals as a final chapter. From them, you can learn and understand what’s happening behind the curtains.
Before we start
For both Dagger approaches, Activity : DaggerAppCompatActivity()
and/or Fragment : DaggerFragment()
.
Regarding Application class:
Dagger, the Simple 🗡️
This setup is quite simple and involves less boilerplate code, for it we’ll need:
AppComponent.kt
AppModule.kt
ActivityBindingModule.kt
note: for every activity we need a new entry in this class.
MyActivityModule.kt
BaseActivityModule.kt
And the setup it’s done, to create and retrieve a ViewModel we simply:
Conclusion
- Except for SavedStateHandle, all ViewModel dependencies will have to be field injected or externally set;
- For each new Activity, a respective [x]ActivityModule.kt class will have to be created and a new entry added to ActivityBindingModule.kt;
- No ViewModelFactory code was needed.
Dagger Android, the Complex ⚔️
This approach is more verbose and complex thanks to the ViewModelFactorylogic that will have to be created and managed. Fortunately we can benefit from code generation tools and — in my opinion — this is where we take the most from a “Dagger setup template”. After we understand what’s happening we just have to take care of the manual boilerplate.
Let’s see how the classes earlier created will change and how many will be added.
First we create an AssistedModule.kt
and add it to AppModule.kt
Our [x]ActivityModule.kt classes will also include this AssistedModule and the Factory setup:
Next, lets move on to Factories.
Factories
- ViewModelAssistedFactory: will help us with the SavedStateHandle logic;
- ViewModelFactoryByInjection: it will be our “auto-factory”;
- ViewModelFactory: it will be our “manual-factory”.
note: usually I go with ViewModelFactoryByInjection but I think it’s useful to show how to use the ViewModelFactory. Latter you’ll see how the setup can work with both.
ViewModelAssistedFactory.kt
ViewModelFactoryByInjection.kt
ViewModelFactory.kt
Back to our setup, let’s finish it.
ViewModels now will have the SaveStateHandle
assisted injected, thus Inject
will change to AssistedInject
and Assisted
will be added. Remaining dependencies will be constructor injected. Finally, we add the Factory interface
:
Our Activity
And there you go, our ViewModel creation it’s done. Let’s see what would have changed in our Activity if we used ViewModelFactory instead:
Simple right?
Conclusion
- New module created;
- For each new Activity, a respective [x]ActivityModule.kt class will have to be created and a new entry added to ActivityBindingModule.kt. The[x]ActivityModule.kt is more verbose since now we have to add the Factory Bind setup;
- All ViewModel dependencies are constructor injected, and the Factoryinterface needs to be added;
- ViewModelFactory classes created;
- Activity needs to inject the Factory before creating its ViewModel.
Bonus
Before we jump into Hilt, let’s create a BaseActivity so it can accommodate all Dagger approaches:
Dagger & AndroidDagger
Our Activity (Dagger) becomes:
Our Activity (DaggerAndroid) becomes:
Pretty cool right? 😎
Hilt, the Illusionist 🔮
Because Hilt will be using Dagger under the hood all the previous setup will be simplified, in fact, we’ll delete almost all classes 😆.
Application.kt will be simplified into:
As you can see, HiltAndroidApp
annotation replaces AndroidInjector
code. They’re both responsible for starting the dependency graph generation.
Yet replaces is not the best word, hides should be used instead, but why? Well, Hilt annotations will generate Hilt_MyClassName.java
classes, those classes will contain all the code that we previously had to configure ourselves. After a successful build we’ll have a Hilt_MyApp.java
and so on.
Next we have AndroidEntryPoint
which works just like HiltAndroidApp
.
Activity.kt (without BaseActivity.kt) will be simplified into:
Activity.kt (with BaseActivity.kt) will be simplified into:
BaseActivity.kt will be simplified into:
Hilt will generate something like:
Next we have ViewModelInject
which replaces Inject
. This one wont generate code, instead, it’ll enable constructor injection in our ViewModels.
ViewModel.kt will be simplified into:
Closer look into dependencies
Let’s take a closer look on how our dependencies are being provided. First we create an AppProviders module and finish it by adding InstallIn
annotation:
And that’s it. Now, if we put a breakpoint in ViewModelProvider(this).get(getViewModelClass()
, when follow the call chain, we’ll see:
ViewModelProvider.kt (get() line 170, line 185)> AbstractSavedStateViewModelFactory.kt (create() line 66, line 69) > HiltViewModelFactory.kt (create() line 62) > DaggerMyApp_HiltComponents_ApplicationC.kt (get()) > ActivityCImpl.this.getMyActivityViewModel_AssistedFactory()
which contains the dependencies generated by Hilt, example:
So that’s why, AppComponent.kt, AppModule.kt, AssistedModule.kt, ActivityBindingModule.kt, ViewModelAssistedFactory.kt, ViewModelFactoryByInjection.kt can be deleted 🥳.
Intent.extras
Regarding intent extras provided in runtime by our factories, you may ask. Well, again, Hilt automagically (shown above) injects them inside SavedStateHandle for us, thus, we only have to:
Final Thoughts
Hilt works mainly based on annotations and code generation, that’s why it looks so easy and appealing. Can be seen as a helper layer between the developer and Dagger powerful tools.
By the time I’m writing this article, Hilt version is still in alpha so, personally, until it becomes more stable (at least for production projects), I’ll stick with the Dagger Android approach. Nevertheless, migrating to Hilt was super simple and I ended up with a killer setup! 🤩
Can this be a step closer to a simpler Dependency Injection framework with an easier learning curve? I believe it will, let’s see how it evolves.
I hope you find this article useful, thanks for reading.
References
- Elye: Comparing Dagger 2, Koin and Service Locator approaches
- Tomáš Mlynarič: Connecting The Dots
- David Liu: AssistedInject and AssistedModules - PR, issue
- Manuel Vivo: Dependency Injection on Android with Hilt
- Dagger and Hilt docs