Manual Dependency Injection For Android Jetpack Navigation (aka Don’t Shoot a Fly With a Bazooka)
I was reading again my article about Jetpack Navigation, in particular the part regarding the parameters passed to destinations. I can pass small serializable objects, but what can I do to pass more complex stateful objects?
How to pass the baton to next destination?
When you navigate to a destination, a fragment is automatically instantiated with direction parameters archived in a bundle. We have not an initializer to call, so we cannot pass the baton to next fragment. Google states it clearly:
In general, you should strongly prefer passing only the minimal amount of data between destinations. For example, you should pass a key to retrieve an object rather than passing the object itself, as the total space for all saved states is limited on Android. If you need to pass large amounts of data, consider using a
Ok, say we have a master fragment containing a list of news. A user taps a news in order to disclose it and to read full article text. So, we navigate to a detail fragment that should download article’s content having its identifier. How to do that? Using iOS — or every other platform I programmed on before Android, honestly — I would pass a news object and a gateway to access remote API. Now, I have to change this approach, since fragment instances are volatile. I don’t want to abuse shared view model pattern. I want to separate concerns as much as I can to promote isolation, testability, composability, clarity and every aspect we all learned at CS 101.
So, I will accept this limitation: I cannot create a fragment composing the pieces it needs to work properly. I will embrace Android recommended architecture: each fragment should be driven by a view model that
[…] provides the data for a specific UI component, such as a fragment or activity, and contains data-handling business logic to communicate with the model. For example, the
ViewModelcan call other components to load the data, and it can forward user requests to modify the data. The
ViewModeldoesn't know about UI components, so it isn't affected by configuration changes, such as recreating an activity when rotating the device.
Wonderful. Now the problem is only shifted since view model is not created directly by invoking its constructor. I don’t want view model to pull my gateway or my repository from a global state. Computer science is one and I know for sure this way to code leads to unclarity, untestability and, ultimately, to spaghetti code and bugs.
How to create a view model with custom constructor
I was very surprised that such a basic thing is so obscure. I googled a lot and it’s like Android community is accustomed to this incredible limitation. Many people seem to think that pulling objects from global state is a good idea. No offense, but it is not what other developer communities are used to. So I have come back to Google official documentation and I have finally found a page to evangelize to benefits of dependency injection. A complex name for a simple concept, as James Shore explains:
Dependency Injection is a 25-dollar term for a 5-cent concept. […]
Dependency injection means giving an object its instance variables.
[…] we could pass the variable into the constructor. That would inject the dependency into the class.
Automatic dependency injection: Dagger
Google strongly suggests to use Dagger to automate dependency injection:
Dagger is a popular dependency injection library for Java, Kotlin, and Android that is maintained by Google. Dagger facilitates using DI in your app by creating and managing the graph of dependencies for you. It provides fully static and compile-time dependencies addressing many of the development and performance issues of reflection-based solutions […]
Ok, Google. How can I pass this parameters to my custom view model? Curiously, I haven’t found a complete snippet on official developer documentation. They only show injection for a non-architecture components ViewModel: they state it’s for simplicity. It’s not a joke, but you can laugh.
I found this great post that illustrates how to setup automatic injection with Dagger 2. I steal only a snippet to illustrate the concept:
This is madness. I refuse write code I can only partially understand (or grasp) to create a simple view model using a constructor. Period.
Automatic injection alternative: Koin
What is Koin?
A pragmatic lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin using functional resolution only: no proxy, no code generation, no reflection!
If you scroll their home page they state they are ready to inject view models out of the box:
Ok, this is clear and not exotic. But this comes to a cost:
Koin isn’t a dependency injector but a service locator with a clever reified trick that you can use to manually perform dependency injection, the boilerplate will scale disproportionally.
Service locator is another (anti) pattern to store dependency in a global central registry. It’s not as good as Dagger, but Koin does a great job to mask its ugly dark side. The real problem is another: Koin does not provide compile time safety. This means you can register crashes at runtime caused by wrong graph injection.
Manual dependency injection
Why have I to shoot a fly with a bazooka? I only want to create my view models with some custom parameters, not to build a Mars colony with naked hands. I have always written my code with dependency injection, without even knowing what I was doing was called dependency injection. So, back to the basics: how do I get a view model from my fragment?
Ok, so what is
viewModels()? It is a function that takes an owner and a factory producer, and returns a lazy-generated model. Ok, cool, finally I can pass a factory — exactly a
ViewModelProvider.Factory that is a simple interface:
Everything is straightforward. I can even extend
AbstractSavedStateViewModelFactory in order to support view models with
SavedStateHandle. Its constructor is easy to understand:
Since my view model will be bound to a fragment, I can even simplify it:
Please mind this constructor will be called everytime fragment is recreated. If you need extra parameters, you can use producers:
Note that view model will have access to navigation directions arguments, too. Now I only need to override
create() method like this, supposing
MasterViewModel takes those two parameters:
Please note this method will be invoked only once in a lifecycle, not everytime
MasterFragment will build a factory:
Manual injection of shared view model
Same pattern, but view model will be kept alive by single activity of our app.
And we can still use Kotlin delegates as usual:
Manual injection of navigation graph view model
Again, if we want to attach view model to navigation graph lifecycle (e.g.: modal navigation sub-flows, like wizard, checkout, …) we can use same pattern:
Note we are using activity as owner, since we are in a single activity environment. Kotlin delegate usage is simple:
What if I cannot instantiate dependencies inside
create()? That is the case of a global coordinator, like a database connection coordinator. You can just inject the singleton instance:
This way, view model will not be aware that parameter is a singleton, preserving isolation and testability.
There are other cases of shared state that may emerge, but you can always coordinate them using proper architecture, leveraging the possibilities of composing shared view models instead of abusing global state.
This article is far from being exaustive, but it is more like an advice to myself to strive for simplicity. It is not always better to write short brilliant code, when some lines of boilerplate help to improve readability. What is more, it is crucial to me, in particular during debugging, to be able to follow logic flow of my code and to know where dependencies of my isolated modules come from. That’s why I cannot accept to use Dagger while such a small request like mine leads to that convoluted configuration.