This is a series of posts about my experience leveling up my Android development skills with Udacity's Nanodegree program. The entire series can be found under the udacity tag here.
Project 2 - Popular Movies Stage 2 is done! This project was about optimizing the layout for tablets, showing reviews and trailers, favoriting movies, and allowing offline access to favorites. Demo GIF on Nexus 9 tablet (the messed up colors are because GIF only supports a limited set of them):
(If you like those sweet transitions, take a look at my previous post for how to implement those)
Since this concludes my work on the Popular Movies app, it's the perfect time to talk about a very important part of the development process: Android app architecture.
Let's talk about Android app architecture
Over the last year I've gravitated towards a simple event-based architecture which has a number of nice characteristics, one of which is that it makes it easy to add an offline mode. It works well for small apps made by a single developer (yours truly). I've used it for 2 apps now: Popular Movies (this project) and Quill (an app for the Ghost blogging platform, on which my blog runs).
It will probably need further refinement for teams working on larger apps. And of course it's not perfect even for me, no architecture can make a claim to that ideal. I will talk about the downsides later, and where I think refinements will be needed.
Throughout this discussion I will use some terms borrowed from the MVC architecture — especially the term Model which simply refers to the component that deals with the storage and retrieval of data in the app.
How it works
The UI and Model communicate indirectly via what's called an event bus. An event is any object that represents a message. The event bus is a central hub through which components can send or receive events without any knowledge of the other components involved. This last part is the key — components simply send their events to the bus, which takes care of forwarding it to any other component(s) interested in (aka "listening to") that event. This is what makes event buses powerful in some ways (but cripples them in other ways, as we shall see later).
Let's take an example. Imagine that we want to display details about a movie (poster, synopsis, rating, reviews, trailers, etc) from the MovieDB API, which is one of the core use-cases in Popular Movies. Here's how it might work within this architecture:
-
The UI fires a
LoadMovieEvent
withmovieId
set to1234
. (code) -
The model, which is "listening" for this event on the bus, gets the event. It makes an HTTP request (on a background thread, of course) to the Movie DB API to fetch this movie and in the response callback, fires a
MovieLoadedEvent
which contains the details. (code) -
The event is delivered to the UI, which is listening for a
MovieLoadedEvent
. Now the movie can be displayed. (code)
Ok, so far it's pretty simple, and there's nothing spectacular about this — we could've achieved this with simple method calls. But now we can improve the user experience in a way that's not possible with a dumb method call.
Let's say we've loaded and displayed the movie once, the user's happy, she quits the app and comes back to it a few days later. We could make another HTTP request to display this movie again, but while that request is ongoing it'd be better to initially display the old details, no? Especially since many of these details (e.g., the poster and synopsis) rarely change, if ever. And what if the user is in a subway train with spotty or absent internet connectivity? Well then she'll be stuck with an ugly error message! The event bus architecture can help us here:
-
UI fires a
LoadMovieEvent
. (code) -
Model gets the event, immediately loads the old movie details from a cache / database and sends it back in a
MovieLoadedEvent
. This initial load happens in the space of a few milliseconds and the user can start interacting with the data, read older reviews, etc. Meanwhile the model also kicks off an HTTP request on a background thread to fetch the latest details. (code) -
UI gets the
MovieLoadedEvent
and displays the old data. (code) -
The HTTP response comes back eventually (could be a few msec, or a second, or maybe never), and is sent to the UI in another
MovieLoadedEvent
which displays it. The updated movie is also stored back in our cache / database and the old one is discarded. (code)
This is where the architecture shines: (a) the UI doesn't need to care at all where the response comes from (memory, disk cache, network, ...), and, (b) the UI also doesn't need to continuously request the latest data from the model, because the model can simply fetch new data in the background and fire another event to let the UI know about updates, like we're doing in steps 3 and 4 above.
Dumb method calls simply cannot do these things (getting an RxJava Observable
as a return value from a method doesn't count as a "dumb method call" for the purposes of this discussion, we'll look at this separately below).
Lastly, if you want to look at some actual code, you can visit my Popular Movies project on Github.
Good things
As we've seen, the complete decoupling of the UI from the model and the fire-and-forget nature of an event bus enables some nice UX improvements. Let's look at what benefits we get over simple method calls:
-
Model doesn't have to know about
Activity
/Fragment
lifecycle: We've all been through this, it's a common cause of crashes when usingAsyncTask
s. The Activity starts anAsyncTask
, the app is minimized or moved off-screen, then theAsyncTask
completes and tries to update the non-existent UI — boom, instant crash. With an event bus,Activity
s andFragment
s can start or stop listening to events according to their lifecycle, so that no UI updates are attempted when they are not visible. The model can now run independently of the UI lifecycle, it can fire events and the UI will only receive them if it is ready to. -
Background jobs cannot leak the
Activity
anymore: This is tied to the above point, and is also a very common mistake when usingAsyncTask
in anActivity
. In the events-based architecture leaking theContext
is pretty much impossible as the model doesn't hold an implicit or explicit reference to theActivity
, unless you pass aContext
via an event, which you should never need to do.
Perfect internet connectivity on mobile is a Utopian dream that isn't going to come true in the next few years
-
Easier to make the app offline-first: Building offline-first apps was the big topic of discussion at this year's Android Dev Summit, and rightly so. Perfect (or even 90% perfect) internet connectivity on mobile is a Utopian dream that isn't going to come true in the next few years. We need to design apps to work completely offline from the ground up. Shoehorning an "offline mode" onto the app after it is completely built isn't likely to work out well. The event-based architecture makes building offline-first apps significantly easier, as we saw in the example with the initial cache load.
-
Easier to refactor the UI or model: since the two components never call each other directly, they can be refactored in any way, including breaking them up into smaller classes, renaming methods, moving them around, etc. The model could be moved into a
Service
and the UI wouldn't care. The UI could be refactored to useFragment
s instead of giantActivity
s and not a single line of model code would have to change. -
The virtue of simplicity, compared to more complex architectures like MVVM and MVP: this is more of a meta observation about this kind of architecture, in that there is hardly any rigid structure and that can be a good thing. The only defining contract here is the decoupling of UI and non-UI code via events. My point is that "architecture" is not necessarily a good thing, and more of it can actually harm the maintainability of your code. There is a cost associated with every additional contract you introduce, and you do pay that cost, especially when someone new starts working on your app. I advocate using as simple an architecture as possible, for as long as possible. Don't try to force a particular architecture just because you heard it's great.
Bad things
As promised, this architecture isn't a silver bullet of any sort. Here are the most important downsides to be aware of:
-
Difficulty of debugging and navigating the maze of events (vs. method calls): Android Studio makes it a breeze to follow the flow of code: Ctrl+Click to follow a method call. Debugging is also straightforward: on a method call, click the Step Into action to go inside the method. Both of these things are made harder by introducing events. Now you have to go through an additional step: figuring out which components listen to that event, or in the case of debugging, stepping into the event bus code and out on the listener's side. This is why I recommend not using events for things like
Fragment
-Activity
communication — interfaces work much better there. For UI-model communication, events make more sense because the many benefits we get outweigh the cost. -
Time of registering on the Bus: I usually start listening on the event bus in my
BaseActivity
'sonStart()
, so I don't have to do it in every singleActivity
I make. But I often make the mistake of firing the event likeLoadMoviesEvent
inonCreate()
, forgetting that it will not work because it's not registered on the event bus yet. Then it usually takes a couple of minutes of frantic debugging to figure out why I'm not getting a response to my event. So there is a slight cognitive burden of always remembering to never fire events beforeonStart()
executes.
Potential improvements
If you want to take this architecture further, perhaps if working in a team, here's some areas where I think it could benefit from modification:
-
RxJava in place of simple events: RxJava is a powerful library that works on the principle of streams of events (which are called
Observable
s), and provides dozens of operators to manipulate these event streams. In the context of the architecture discussed above, anObservable
is like an event bus on steroids. Definitely worth looking into. -
Splitting UI code further into View and ViewModel components: View here refers to the architecture term, not an Android
View
object. As your app grows and the UI gets more and more complex, it will tend to accumulate in giantFragment
s orActivity
s. This can be mitigated by separating out UI logic into a ViewModel, leaving yourFragment
s to become pure UI containers with no logic. This kind of split is used in the MVVM architecture.
That's all, folks! Let me know in the comments if you have questions or concerns.