Exaud Blog
Blog
How To Leverage Reactive Programming For Keeping Code Clean In Android Using RxJava And Model-View-ViewModel
Learn how to implement a clean MVVM architecture in Java and Kotlin, using RxJava for efficient data handling, separation of concerns, and scalable, testable code.Posted onby Tiago BorbaDisclaimer: 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.
Understanding Separation of Concerns: Enhancing Code Maintainability and Testability
In this short article, we provide an example on hoyw 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.
Leveraging RxJava for Simplified Data Handling and Inversion of Control
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):
// Singleton
public class Repository {
private Repository instance = new Repository();
private Repository() {}
public static Repository getInstance() {
return instance == null ? new Repository() : instance;
}
/**
* Creates a {@link Single} which will send a sign in request to remote API, if necessary, and
* return the appropriate UI transition as {@link Intent} with appropriate action and extras.
*
* @param context {@link Context} to create an Intent
* @param context {@link String} something that authenticates the user from input
* @return {@link Single<Intent>} for the requested operation
*/
@NonNull
public Single<Intent> signInOperation(@NonNull Context context, @NonNull String userInput) {
if (!tokenFresh() && isConnectionUp()) {
return remoteSignInSingle(userInput);
} else if (userAuthenticatedLocally(userInput)) {
Intent cachedLogIn = Ui.buildSignedInIntent(context);
cachedLogIn.putExtra(Ui.USER_EXTRA, getAuthenticatedUser());
cachedLogIn.putExtra(Ui.ONLINE_EXTRA, false);
return Single.just(cachedLogIn);
} else {
Intent errorLogIn = Ui.buildErrorIntent(context);
errorLogIn.putExtra(Ui.MESSAGE_EXTRA, "No connection!");
return Single.just(errorLogIn);
}
}
@NonNull
public Single<List<Stuff> getStuffOperation(Context context) {
// TODO
}
}
public class SignInViewModel extends ViewModel {
/**
* Handles intention of submit from a sign in screen, calling remote API. This either loads from
* server, local cache, fails with a proper Intent, or throws an exception if errors unhandled.
*
* @param context context to acquire the next UI Intent
* @param userInput {@link String} for input to pass to remote API (assume no password required)
*/
public Single<Intent> clickSignIn(@NonNull final Context context,
@NonNull final String userInput) {
return Repository.getInstance().signInOperation(context, userInput)
.map(intent -> {
// return unmodified, for now
return intent;
});
}
}
public abstract class SignInScreen extends Fragment {
/**
* ViewModel performs submit operations for sign in. This screen uses the resulting
* {@link Intent} to start next activity, transition to next screen, finish, or show an error.
*
* @param context a context to perform next ui flow call
* @param userInput input to perform the request with
*/
protected Disposable onClickSubmitLanding(@NonNull Context context, String userInput) {
return viewModel.clickSignIn(context, userInput)
.observeOn(Schedulers.io())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe((Consumer<Intent>) context::startActivity);
}
}
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.
Implementing the MVVM Pattern: Streamlining Communication Between View, ViewModel, and Repository
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)
class SignInViewModelTest {
/**
* Tests if clickSignIn calls expected method from Repository. Currently, that's its only task.
*/
@Test
fun testclcikSignIn() {
val mockContext = Mockito.mock(Context::class.java)
val mockRepository = Mockito.mock(Repository::class.java)
val input = "inputHere"
// return something to be expected
Mockito.`when`(mockRepository.signInOperation(mockContext, input))
.thenReturn(Single.just(Intent()))
val origRepository = ReflectionHelpers.getStaticField<Repository>(
Repository::class.java, "instance")
ReflectionHelpers.setStaticField(Repository::class.java, "instance", mockRepository)
// actual test – check output type and verify a Repository operation was called
assertTrue("Method return type wrong", viewModel.clickSignIn.(mockContext, input) is Single<Intent>)
verify(mockRepository, times(1)).signInOperation(mockContext, input)
ReflectionHelpers.setStaticField(Repository::class.java, "instance",
origRepository)
}
}
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.
Found this article helpful? Subscribe to our newsletter and follow us on social media: LinkedIn, Twitter, Facebook.