Android w/o DI

An Actor-based Architecture Approach

Bob Dahlberg
The Startup

--

Android architecture has come a long way in standards and best practices, especially near the UI. I don’t see many apps that don’t use ViewModels, Views, and LiveData to simplify the app. But one step further from the UI, where the business logic resides, there are almost as many implementations as applications.

Still, there seems to be a general idea that dependency injection should be the golden way, but I don’t see it, not yet at least. The popular frameworks have been around for a long time and haven’t convinced so far. Despite the shortcomings of the frameworks, I do like dependency injection automation. There's only one big flaw; it simplifies coupling, which really isn't a problem in itself. But if you’re not paying attention, you might be ending up with all your components coupled to all others just because it’s easier to inject them than to solve a good decoupled architecture.

On the other side of those frameworks, we have the business logic of our app, and each app has its own standard. We got one with Modules, Stores, and Repos. Next one with Services, Models, and Components. It’s like an exploration journey for each app you get the privileges to look in under the hood.

Since I’m yet not swept off my feet with the dependency injection frameworks, I’ve been using an architecture inspired by the Actor model, loose coupling, and reactive programming. This is my approach to Android Architecture with Actors and why I believe this is a good alternative.

I will split the subject into three articles;

Going Reactive

First, we need to replace the DI frameworks with another way of communicating between our ViewModels and our Actors. My solutions have always landed on a reactive stream of some kind. It forces decoupling and makes the architecture reactive by heart. I’ve been a heavy user of Rx in the past, and I still see it as a good solution. Since I’m using Kotlin and coroutines a lot, it makes sense to utilize Channels for this hot stream. If not purely for the reason to have one less external library. But if you’re a heavy Rx user, I’d suggest you start with that. And if you’re stuck with Java, I don’t see that many alternatives.

Update: This piece of the architecture has been updated with a SharedFlow instead of the original BroadcastChannel.
See the updated version here: https://bob-dahlberg.medium.com/android-w-o-di-update-58a218f7139b

This is our AppStream. The heart of it is the BroadcastChannel that all our messages will be passed through. The send function launches a coroutine on each message received and sends it on to the channel. I’ve chosen to do it like this so that the AppStreams send function won't need the suspend keyword. This makes it available for any part of your application to send a message without the need for a coroutine context. The AppStream also exposes a Flow that opens a subscription to the channel each time it’s called. Thanks to the fact that we wrap it in a Flow, it will also be closed when the flow is canceled.

The Actor Model

With the AppStream object, we can now send messages from our ViewModels to our actors. So now we need some actors. But what is the Actor model? Let’s start with this definition from brianstorti.com/the-actor-model/

“An actor is the primitive unit of computation.
It’s the thing that receives a message and do
some kind of computation based on it.”

What appeals to me is that actors have their own private mutable state. And the only one who can mutate that state is the actor itself. It also has a defined way of communicating via messages processed one after the other in the order they are received—this yields thread-safety, decoupling, and reactive, music to my ears.

But it comes with its challenges too, mainly on the architecture side. You need to really commit to your architecture and make thought-through decisions when something is an Actor and when it is something else, like a utility.

I also need to emphasize that this architecture is inspired by actors, it’s not pure actors, and I violate the Actor model here and there. I’ve just stolen the gold from it and adjusted the rest to make it fit in an Android application the best way I see it.

That said, the next building block in our architecture is an abstract Actor, we can start it, and we can stop it from the outside. When starting it, it will create an internal coroutine with a channel (the actor-builder from Kotlin on line six), and then iterate over its channel and call the act-function for each message received. The channel, in this case, is the mailbox representative of the Actor model.

The last thing that happens in the start-function is collecting all messages from the AppStream and pass them on to the actor on line 11.

Implementation

I’ll show two example implementations of the abstract Actor. First, the UserActor, which has been simplified for this example. It has a private mutable state, namely the user. Then it receives messages and only cares about the LoginMessage type.

Upon receiving a LoginMessage, it tries to fulfill that message's intent by calling the private login-function with input from the message. If it successfully logs the user in, it will mutate its state, and otherwise, it will leave it as is. The last thing the UserActor does is to send out a UserState containing the current state and a possible error from the operation performed.

This way of acting, to always send a message with the current state, can be seen as a contract for an Actor, that if someone tries to affect the state, they will always receive a state-message knowing the operation is completed.

Another implementation I always use is some logging actor. It receives all messages and logs them. In this example, I put the tag as outgoing, “>>” for all messages that aren’t of type state, and tag all states as incoming “<<” for a nice narrative of my application in the logs.

I usually also log this narrative to Firebase Crashlytics so that when an unexpected crash occurs, I also have the latest logs of what has happened in my app. I can even use the narrative to drive my app during testing. Just be careful when logging in production that you don’t log any sensitive data. More on this in part three of this series.

Wiring it all together

Now we know of all components. On the UI side, we have the View and the ViewModel that communicates through LiveData. The ViewModel sends and receives messages from the AppStream, and the Actors receives and sends messages and states through the AppStream. Now we need to connect all pieces, and one aspect, replace the usual DI framework set up with our own.

Thanks to the fact that we use a reactive approach and have a very decoupled code where each Actor only knows and cares about itself, we have a straightforward setup.

As simple as that our actors are wired up and ready to receive messages. And all you sharp-eyed developers out there can spot that I inject a dependency into my UserActor, namely the API. I could’ve chosen to have a NetworkActor handling all HTTP-calls with message passing as well. But I’ve chosen not to, for two reasons. Firstly, because it’s too much redundant code to do a standardized task. And secondly, because actors have one mailbox, and the messages sent to it are executed one at a time. To do multiple API-calls in parallel, I would have to create an orchestrating actor for all those requests that create a new actor for each request, and that's overly complex for most apps I’ve built.

Another approach I’ve used is to have a Network actor send out the client in a message to all actors using it. But that can also impose some synchronization issues, but the approach is nice.

And then there’s the question of persistent data, and my thoughts are that writing to persistent storage is basically synchronous operations. I tend to have a PrefsActor and a DBActor for those. But this very much depends on how DB-intensive the app is. If it’s only ever one actor that can modify a table or have its own DB.

Let's continue to wire up our ViewModel to our AppStream so that our more standard UI side can communicate with our business logic and react to state changes.

First, I create an internal flow to filter out only UserState messages. Then I use this flow to create a loggedIn-LiveData and an errorMessage-LiveData.

Since the AppStream is an object, and in Kotlin, an object is a Singleton, I can hook into the states-Flow from that. The same holds for the send-function from the AppStream that is imported. It allows me to send a message from anywhere, and since it’s not suspending either, the caller doesn’t have to have a Coroutines scope, which simplifies a lot.

Lastly, let’s have a look at the View to get the complete picture. This should come with no surprises. We inject the ViewModel and observe its LiveData objects to update our UI.

Final thoughts

This part has covered most of the foundation and my thoughts around the architecture approach. I’ve also saved some tips and tricks for the next article where we make use of Kotlin as a language to simplify our code.

What I would want you to take away from this article is that when using dependency injection frameworks, there is a big risk that we as developers let go of the structure and decoupling since it’s not a problem when it comes to dependency handling. But it can become problems when everything is tightly coupled to everything in your app, and you need to refactor some part. Then even the best frameworks won't help you.

This reactive and actor-based architecture helps me stay on top of decoupling, side effects, leaking unintentional data. The narrative part helps me troubleshoot, understand, and even test my application very easily.

It takes some time to get used to, and a simple thing as reading a value becomes more complex having to both send and receive a message. But thanks to coroutines, we can build some shortcuts to make it feel exactly as just reading a value. I’ll show you how in the next article.

The project on GitHub can be found here: https://github.com/dahlbergbob/android-actors-2020-10

Update: An updated version of the AppStream, that uses SharedFlow, can be found here: https://bob-dahlberg.medium.com/android-w-o-di-update-58a218f7139b

--

--

Bob Dahlberg
The Startup

Lead Developer at Qvik, Coach, Agile Thinker, GDG Lead.