In the first part, I talked generally about the problems I see with dependency injection frameworks for Android today. I also explained a reactive approach to Android architecture containing a central stream to pass messages on between the business logic and the view logic parts of an application.
First I just want to mention that I wrote an update to the first part where I recommend that you use SharedFlow instead of BroadcastChannel.
I explain it further in this article.
This reactive approach is excellent for some use cases but can feel overly complex in others. For example, having an authentication-state changed broadcasted when a user gets logged out from some part of the app is spot on. You probably want to get notified in a navigation component, and in the current view to be able to show something in the UI or even in a background service tracking app usage. And triggering the logging out can come from various places too. It can be a request getting a 401 back, a logout button clicked, a central cloud service sending a push notification requiring a refreshed authentication. In this case, your app needs to be reactive in some sense so it’s a perfect match.
On the other side, when having a simple problem like getting the current user to display a username on the screen, then it feels overly complex. And I do agree that in these cases, an injected model with a property to read from is a much simpler mental model. And it doesn’t feel natural to have to send a message and listen for a state to fulfill the intent. It can make your ViewModel look like it has gone through a shredder.
I do have three different solutions to this problem that I use depending on the flexibility I want for every particular use case.
Stick to the architecture
When I need one state that might change sometime in the future I rather make it reactive and listen for all updates and send an event that triggers an initial dispatch.
When I need to get a value from a
SharedPreference or just the user name or something as simple as a value I tend to utilize the
CompletableDeferred. It is similar to a
Channel with the difference that it only passes one value between coroutines. I wrap it in an
AwaitMessage as shown in the code below, then I can call the suspending await function on it and the code reads similar to sequential code where you read a property.
AwaitMessage class only wraps the
CompletableDeferred so that it’s easier to invoke. One can complete it or await its result. If you rather expose the entire
CompletableDeferredinterface I’d use a delegate, so essentially exchange line 3 to this and omit the body:
) : Message, CompletableDeferred<T> by deferred
Next, we extend the class with a specific message class and use it in a
ViewModel in a coroutine.
As you can see, the code now reads sequential and you can work on the value instead of having to listen to the stream dispatching states. The last piece of the puzzle is the
Actor that needs to call the complete function on the message to make the await call receive a value.
And as long as you only share values and not references from your actors, your actors are still thread-safe and only mutates the state from within the actor.
The biggest trade-off on the approach above is that you won't get the response on the AppStream, which means that it won't be logged. So I have a strict rule to only use it for purely reading states/values from an actor.
Utilize an ID
In my current project, each message or event has a correlation id. I use it to be able to see the chain of events over the course of a user’s intent, and also being able to map it to a backend request. For example when a user signs in, it triggers a LoginMessage, that will send a request to the backend, and when the response comes back it will send a UserState containing the logged-in user, or failure.
The LoginMessage, request to the backend and resulting UserState all have the same correlation ID. Knowing that all states triggered by a specific message will have the same correlation ID makes it easy to filter the flow of messages on that specific ID.
Here I filter on both the type and the ID, and then I only listen for the first one coming through. The
onStart operation is a great way to trigger the event when the flow is being collected. That way there are no synchronization issues where you have to be careful about when to send the message and when to start collecting the flow. Just beware that if you don’t call any terminal operation on the flow, it will remain cold, and not send the event at all. In the example above, the
first() operation is terminal, hence starting the flow.
Making the switch
The more standard way in Android is either to call a repository directly from your ViewModel or to have modules injected into the view model to be able to call and retrieve processed data from them.
And those approaches mostly work fine. The reasons why I’ve chosen to go about a slightly different approach is;
- I don’t think the dependency injection solutions offered for Android are good enough.
- I’ve always loved reactive programming concepts.
- It’s very loosely coupled and modular, leading to speed on changes.
- Simplifies thread-safety thought work.
But I also understand it’s a big hurdle to pass, similar to when you first start to work with Rx or moving from Rx to coroutines. But it’s also a fun challenge to take on.
And in extension, this concept makes an almost perfect fit for implementing Flutter to native communication, or KMM to native communication for those interested in cross-platform techniques. I’m actually working on a sample app with has Flutter as UI and KMM as business logic.
At times, the code gets a bit verbose in the samples. Thanks to extension functions in Kotlin we can simplify the code greatly. When I started passing messages over my stream I created the message, and then I called
AppStream.send(message) which is both cumbersome and not as slim as I would like it, sure I could’ve made a global function to slim it down a bit but I wanted more. So thanks to extension functions in Kotlin I switched it around to become
It’s a very clean solution that also gives you the simplicity of creating and sending without having a local reference to the message.
What I also included in my latest version was in the spirit of the builder-pattern that the extension-function also returned the message created. That way I can send it and store a reference to it in the same call.
My main reason for returning the message itself is the
AwaitMessage explained earlier. When returning the message on sending, I can actually await it directly like this:
ReadMessage().send().await(). Another solution is to create a new extension function on the
As you see, we can go about many different directions on the sending mechanism in our architecture. Choose the one that fits your project the best.
For the approach with a correlation ID filtering a single response, I’ve done the following extension function on my flow. This is a very specific implementation for my usage and instead of having it multiple times in multiple places I made a generic extension function to cover the flow.
This article mainly addresses the hardest part of adopting this architecture. On how to do synchronous style programming for simple tasks. And a few tips on how to simplify and generify your code.
And it’s mainly thanks to Kotlin’s concepts like extension functions and coroutines you can mix what feels like synchronous and asynchronous styles in the same architecture. It’s more about deciding where the result is needed directly and can be distinguished from the rest, or when we want it reactively that will dictate your system design.
The next, and final article in this series will cover logging and testing. To give you a hint, that part is heavily inspired by event sourcing.