How To Leverage Reactive Programming For Keeping Code Clean In Android Using RxJava And Model-View-ViewModel
Disclaimer: this post focuses on the benefits of this approach in Java. Nevertheless, most, if not all the benefits also apply to standard Kotlin – unless you’re already neck-deep in co-routines. Separation of concerns, IoC and overall simplicity still apply, as you would need complex constructs in Kotlin (such as passing Unit callbacks or defining interfaces) to perform something similar.
In this short article, we provide an example on how to keep your View, Model and ViewModel (and Repository!) Clean, i.e. responsible solely for their purpose, and thus testable, easier to maintain, scale or even document, along with other perks that come from keeping objects simple. As a rule of thumb, each of these should be implementable by their own dev, defined by a contract of the object’s member signatures, and if a conception stage allows for it, you may even already have some tests that will enforce the contract of that object further.
When implementing any architecture to structure App screens that are backed by business objects (data, or the model), no matter if those objects are stored remotely or locally, one of the main focuses is to provide a mechanism to acquire, map those objects and respond to changes in them. There are big incentives currently for handling data in an offline-first approach, even when your app would be targeting remote APIs as a proof of concept, as it ensures that your screens are as responsive when there’s a bad, infrequent or no access at all to your source of truth. But no matter how you believe your app may solely depend on local or remote data, you should always consider that it will eventually source data from mutable sources, and that’s why it’s always nice to start your structure with your own Repository object, for keeping most CRUD operations transparent to whatever consumes data in your app.
And here’s where RxJava presents a solution that leveraging this transparency, while also providing inversion of control (without passing of callbacks), threading à la carte, and a declarative approach that strengthens the contract between each module of the MVVM pattern.
Consider a repository that performs Sign in actions for a user. We don’t need to get into the details of pojos since we just pass String and Intent around in this example.
Let’s see some code (for a better looking version, check it here):
In the current implementation, notice that the ViewModel has no specific logic – it just forwards he original call to the Repository, and in success scenarios passes the flow back to the UI. If the UI transition doesn’t need to transition to another screen, we may just stop the flow right there with an empty Intent (or throw an error), and post some LiveData that is being observed by the Screen. We could also halt flow and take some ViewModel concerns instead:
– map business logic-heavy intents to simpler ones;
– extract some of the data from the repository to keep in ViewModel lifecycle for later usage (e.g. so it survives Screen lifecycle changes);
– change the threading schedulers if ViewModel deems it appropriate for the kind of processing to be done. This is also the case for Repository, e.g. if the local cache is valid and there’s no I/O to be done;
The beauty of this implementation is that it prevents getting business logic on the View (SignInScreen), while it also prevents defining UI logic on the repository, to an extent – we perform a static call to some Ui orchestrator which gets that going. The ViewModel itself also stays contained, in that for testing, we can mock call parameters, then mock the Repository so that it immediately returns a desired Intent, given the specified input. These would be some tests performed on the ViewModel, and something simillar could be achieved for View and Repository:
(in Kotlin, because Kotlin! Looks better here, tho)
The View could be tested just the same way, by mocking the return of ViewModel, while the Repository could be tested with a replacement for the Ui static calls (and further mocking the local cache and remote calls, which is more complex, but irrelevant of any View or ViewModel).
Note that in the View, all we do is Context#startActivity(Intent) and define the observer and subscriber Schedulers (which is important for the blocking operation to happen asynchronously, but the event “finishing” to happen back on the Main thread). Since the event callback is defined directly on the View, we avoid having to keep track of observers in ViewModel, and defining interfaces that enforce the contract. The contract itself is enforced solely by parameters and return types. With the contracts established as such, any dev could work under the assumption of the black box signatures of any given object without boilerplate interfaces, and write code independently for his own object’s concerns, with enough confidence failure occurs if these are not properly handled.
We keep the data flow going from View→ViewModel→Repository and events are propagated back as Repository→ViewModel→View. Flow can be halted and data can be modified in all objects. We can, for example, reduce back-pressure on the ViewModel if the Repository would emit too many events at once (not the case, since we’re handling a Single), handle unexpected Repository errors on the ViewModel so they don’t propagate upstream, etc.
Comments are closed