TODO-MVI-RxJava
Contributors
Benoît Quenaudon and David González.
Summary
This version of the app is called TODO-MVI-RxJava. It is based on an Android ported version of the Model-View-Intent architecture and uses RxJava to implement the reactive caracteristic of the architecture. It is initially a fork of the TODO-MVP-RXJAVA.
The MVI architecture embraces reactive and functional programming. The two main components of this architecture, the View and the ViewModel can be seen as functions, taking an input and emiting outputs to each other. The View takes input from the ViewModel and emit back intents. The ViewModel takes input from the View and emit back view states. This means the View has only one entry point to forward data to the ViewModel and vice-versa, the ViewModel only has one way to pass information to the View.
This is reflected in their API. For instance, The View has only two exposed methods:
public interface MviView {
Observable<MviIntent> intents();
void render(MviViewState state);
}
A View will a) emit its intents to a ViewModel, and b) subscribes to this ViewModel in order to receive states needed to render its own UI.
A ViewModel exposes only two methods as well:
public interface MviViewModel {
void processIntents(Observable<MviIntent> intents);
Observable<MviViewState> states();
}
A ViewModel will a) process the intents of the View, and b) emit a view state back so the View can reflect the change, if any.
The User is a function
The MVI architecture sees the user as part of the data flow, a functionnal component taking input from the previous one and emitting event to the next. The user receives an input―the screen from the application―and ouputs back events (touch, click, scroll...). On Android, the input/output of the UI is at the same place; either physically as everything goes through the screen or in the program: I/O inside the activity or the fragment. Including the User to seperate the input of the view from its output helps keeping the code healty.
MVI in details
We saw what the View and the ViewModel were designed for, let's see every part of the data flow in details.
Intent
Intents represents, as their name goes, intents from the user, this goes from opening the screen, clicking a button, or reaching the bottom of a scrollable list.
Action from Intent
Intents are in this step translated into their respecting logic Action. For instance, inside the Tasks module, the "opening the view" intent translates into "refresh the cache and load the data". The intent and the translated action are often similar but this is important to avoid the data flow to be too coupled with the UI. It also allows reuse of the same action for multiple different intents.
Action
Actions defines the logic that should be executed by the Processor.
Processor
Processor simply executes an Action. Inside the ViewModel, this is the only place where side-effects should happen: data writing, data reading, etc.
Result
Results are the result of what have been executed inside the Processor. Their can be errors, successful execution, or "currently running" result, etc.
Reducer
The Reducer is responsible to generate the ViewState which the View will use to render itself. The View should be stateless in the sense that the ViewState should be sufficient for the rendering. The Reducer takes the latest ViewState available, apply the latest Result to it and return a whole new ViewState.
ViewState
The State contains all the information the View needs to render itself.
Observable
RxJava2 is used in this sample. The data model layer exposes RxJava Observable
streams as a way of retrieving tasks. In addition, when needed, void
returning setter methods expose RxJava Completable
streams to allow composition inside the ViewModel.
Observable
is used over Flowable
because backpressure is not (and doesn't need to be in this project) handled.
The TasksDataSource
interface contains methods like:
Single<List<Task>> getTasks();
Single<Task> getTask(@NonNull String taskId);
Completable completeTask(@NonNull Task task);
This is implemented in TasksLocalDataSource
with the help of SqlBrite. The result of queries to the database being easily exposed as streams of data.
@Override
public Single<List<Task>> getTasks() {
...
return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)
.mapToList(mTaskMapperFunction)
.firstOrError();
}
Threading
Handling of the working threads is done with the help of RxJava's Scheduler
s. For example, the creation of the database together with all the database queries is happening on the IO thread.
Immutability
Data immutability is embraced to help keeping the logic simple. Immutability means that we do not need to manage data being mutated in other methods, in other threads, etc; because we are sure the data cannot change. Data immutability is implemented with the help of AutoValue. Our all value objects are interfaces of which AutoValue will generate the implementation.
Functional Programming
Threading and data mutability is one easy way to shoot oneself in the foot. In this sample, pure functions are used as much as possible. Once an Intent is emitted by the View, up until the ViewModel actually access the repository, 1) all objects are immutable, and 2) all methods are pure (side-effect free and idempotent). The same goes on the way back. Side effects should be restrained as much as possible.
ViewModel LifeCycle
The ViewModel should outlive the View on configuration changes. For instance, on rotation, the Activity
gets destroyed and recreated but your ViewModel should not be affected by this. If the ViewModel was to be recreated as well, all the ongoing tasks and cached latest ViewState would be lost.
We use the Architecture Components library to instantiate our ViewModel in order to easily have its lifecycle correctly managed.
Dependencies
Features
Complexity - understandability
Use of architectural frameworks/libraries/tools:
Building an app following the MVI architecture is not trivial as it uses new concepts from reactive and functional programming.
Conceptual complexity
Developers need to be familiar with the observable pattern and functional programming.
Testability
Unit testing
Very High. The ViewModel is totally decoupled from the View and so can be tested right on the jvm. Also, given that the RxJava Observable
s are highly unit testable, unit tests are easy to implement.
UI testing
Similar with TODO-MVP. There is actually no addition, nor change compared to the TODO-MVP sample. There is only some deletion of obsolete methods that were used by the ViewModel to communicate with the View.
Code metrics
Compared to TODO-MVP, new classes were added for 1) setting the interfaces to help writing the MVI architecture and its components, 2) providing the ViewModel instances via the ViewModelFactory
, and 3) handing the Schedulers
that provide the working threads. This amount of code is actually one big downside of this architecture but can easily be tackled by using Kotlin.
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Java 73 1275 1689 4798 (3639 in MVP-RXJAVA)
XML 34 97 338 610
-------------------------------------------------------------------------------
SUM: 107 1372 2027 5408
-------------------------------------------------------------------------------
Maintainability
Ease of amending or adding a feature
High. Side effects are restrained and since every part of the architecture has a well defined purpose, adding a feature is only a matter of creating a new isolated processor and plug it into the existing stream.
Learning cost
Medium as reactive and functional programming, as well as Observables are not trivial.