Practical example using Reactive Clean Architecture approach
In the previous post we explained the principles of the reactive clean architecture that we use in N26. In this post we are going to show how to apply it.
Let’s say we want to create a new feature in the app that shows a list of the credits drafts the user has. The code for this example can be found in this repository.
Data
This is the entry point of the feature, we need to get the data from some API and put it in the reactive store. Let’s see how this process looks like.
1. Connect to API
We use retrofit for the network calls, in this case the service looks like this:
interface CreditService {
@GET("credit/drafts")
Single<List<CreditDraftRaw>> getCreditDrafts();
}
2. Setup the reactive store
The following interface is used for the ReactiveStore
:
public interface ReactiveStore<Key, Value> { void storeSingular(@NonNull final Value model); void storeAll(@NonNull final List<Value> modelList); void replaceAll(@NonNull final List<Value> modelList); Flowable<Option<Value>> getSingular(@NonNull final Key key); Flowable<Option<List<Value>>> getAll();
}
We are not going to show the actual implementation of the ReactiveStore
in this post. A simple reactive store based on memory cache is implemented in the provided repository. As we said in the previous post, there are many ways to implement this component depending on the use case, we are planning to bring more posts in the future to show different possible implementations, stay tuned!
So, assuming we have the ReactiveStore
implemented and ready, we’ll need to instantiate aReactiveStore
for the desiredKey
and Value
. In this case the Value
is CreditDraft
and the Key
is the id of the specific CreditDraft
, represented by a String
. We use Dagger for dependency injection, this is how the resulting methods in the module would look like:
@Provides
@Singleton
Store<String, CreditDraft> provideCache() {
return new Cache<>(CreditDraft::id, CACHE_MAX_AGE);
}
@Provides
@Singleton
ReactiveStore<String, CreditDraft> provideReactiveStore(Store<String, CreditDraft> cache) {
return new MemoryReactiveStore<>(CreditDraft::id, cache);
}
The first method provides a classic memory cache, which takes a function to extract the Key
from the values to store and the max age to indicate when the values should expire. The second method creates the ReactiveStore
, it also takes a function to extract the Key
from the values and the cache.
3. Create the raw to safe entity mapper
Now, looking at the previous code snippets, we can see that the getCreditDrafts()
method from the CreditService
is returning a list of CreditDraftRaw
objects, while the ReactiveStore
is designed to take CreditDraft
objects. We should be storing the objects that come from the API in the store, but as we saw in the first blog post, we don’t want to store these objects straight away, we should process them and do some sanity checks first. This is where the raw to safe entity mapper comes into play:
class CreditDraftMapper implements Function<CreditDraftRaw, CreditDraft> {
@Override
public CreditDraft apply(CreditDraftRaw raw) {
....
}
}
CreditDraftRaw
POJO has all its non primitive type parameters annotated with @Nullable
since there is no guarantee that they’ll be present in the API’s response payload. In the other hand, CreditDraft
has all its non-primitive type parameters annotated with @NonNull
.
There are two ways of handling the presence of the parameters when it comes to the raw to safe mapping. The first one is when the parameter must be present in the raw entity for the feature to function. This is the case of the status
for instance:
@AutoValue
public abstract class CreditDraftRaw { @Nullable
abstract String status();}@AutoValue
public abstract class CreditDraft { @NonNull
abstract CreditDraftStatus status();}
We’ll check if the status
is present in the CreditDraftRaw
and throw a runtime exception if it’s not there. Also it will be mapped from String
to enum
for convenience.
The other way is when the presence of the parameter is optional, this means is fine when is not in the API’s payload, in which case the parameter will be parsed to Option<Parameter>
. This is the case of the CreditRepaymentInfo
:
@AutoValue
public abstract class CreditDraftRaw { @Nullable
abstract CreditRepaymentInfoRaw repaymentInfo();}@AutoValue
public abstract class CreditDraft { @NonNull
abstract Option<CreditRepaymentInfo> creditRepaymentInfo();}
4. The repository
At this point we have all the necessary components for the data layer, we just need to put them together in the repository. Also, the repository is the interface to the upper layer, the only class that should be visible from the data layer with exception of the safe entities.
In this case the CreditDraftRepository
takes the CreditService
, the ReactiveStore<String, CreditDraft>
and the CreditDraftMapper
in its constructor.
CreditDraftRepository(ReactiveStore<String, CreditDraft> store,
CreditService creditService,
CreditDraftMapper creditDraftMapper) {
this.store = store;
this.creditService = creditService;
this.creditDraftMapper = creditDraftMapper;
}
For this example we only need the Get and the Fetch methods:
Flowable<Option<List<CreditDraft>>> getAllCreditDrafts() {
return store.getAll();
}
Completable fetchCreditDrafts() {
return creditService.getCreditDrafts()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
// map from raw to safe
.flatMapObservable(Observable::fromIterable)
.map(creditDraftMapper)
.toList()
// put mapped objects in store
.doOnSuccess(store::replaceAll)
.toCompletable();
}
The Get is fairly simple, it’s the store’s stream from getAll()
method with no mappings or modifications. The Fetch, in the other hand, requires a little bit more of work. When the List<CreditDraftRaw>
comes from the API call, first we need to map them to List<CreditDraft>
, which are going to be put in the store as next step. Finally, the Single
observable is transformed to Completable
.
But why
toCompletable()
? Why not simply return the fetched values asSingle<List<CreditDraft>>
?
The fetchCreditDrafts()
is feeding the reactive store, this means the fetched values are going to be emitted in the Flowable
from the getAllCreditDrafts()
method. This is the concept behind this type of architecture, there is the stream that provides infinite updates of a specific value’s state and there is the action that feeds this stream, they are separate from each other.
Of course we don’t always need a store for the values, that’s why we have the Request method. For instance, if we don’t want to put the fetched List<CreditDraft>
the resulting method would look very similar to the fetchCreditDrafts()
from before but it won’t have the last two steps that puts the results to the store and converts it to Completable
:
Single<List<CreditDraft>> requestCreditDrafts() {
return creditService.getCreditDrafts()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.flatMapObservable(Observable::fromIterable)
// map from raw to safe
.map(creditDraftMapper)
.toList();
}
In N26 we prefer to have a clear separation, so if we want to put the values in the store, we will always read them from the Get’s infinite stream. In the other side, the Fetch will feed the store and never return the fetched value. We use the Request to get the values once from a Single
, but this method will never feed the store. Summarising, we don’t have a method that returns the values once through a Single
and feeds the store at the same time, even though technically is possible.
Get, Fetch and Request are not the only operations in the Repository
, we sometimes also need to send something to our external source (API):
Single<CreditDraft> deleteCreditDraft(CreditDraft draft) {
return creditService.deleteCreditDraft(draft)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.map(creditDraftMapper);
}
Single<CreditDraft> pushCreditDraft(CreditDraft draft) {
return creditService.putCreditDraft(draft)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.map(creditDraftMapper);
}
Domain
At this point our data layer is ready, we have a Repository
that we can inject in the domain layer.
For this specific example we need a RetrieveInteractor
, we are going to call it RetrieveCreditDraftList
. In this case there are no inputs required to perform this operation and the out put is List<CreditDraft>
, which results in RetrieveInteractor<Void, List<CreditDraft>>
. This type of interactor has to implement the getBehaviorStream()
method that should return a Flowable
:
Flowable<List<CreditDraft>> getBehaviorStream(Option<Void> params) {
return creditRepository
.getAllCreditDrafts()
// fetch if emitted value is none
.flatMapSingle(this::fetchWhenNoneAndThenDrafts)
// unwrap if some, filter if none
.compose(UnwrapOptionTransformer.create());
}
private Single<...> fetchWhenNoneAndThenDrafts(Option<...> drafts) {
return fetchWhenNone(drafts).andThen(just(drafts));
}
private Completable fetchWhenNone(Option<...> drafts) {
return drafts.isNone()
? creditRepository.fetchCreditDrafts()
: Completable.complete();
}
The chain starts with the infinite Flowable
that comes from creditRepository.getAllCreditDrafts()
. This stream is designed to never end, complete or error. It also emits right after subscription the stored value or NONE
if the store is empty. Is this interactor’s responsibility to perform the necessary actions to retrieve the List<CreditDraft>
from all the available sources, internal (store) or external (API). To achieve this, it checks the emitted elements by the repository’s Flowable
, if the value isNONE
means that the store is empty and it is required a fetch operation. The last step in the chain is to unwrap the Option
in the case there is some value inside or filter it out when is NONE
.
It is important to note that the chain is built in such a way that if there is an error coming from the fetch operation it will be forwarded, so the resulting Flowable<List<CreditDraft>>
from the interactor is designed to error. This way the presentation layer can decide to show some sort of visual feedback in such a case. Also, the interactor is filtering out the case when the store is empty, this means the Flowable
from the interactor won’t emit anything in such case. The idea is that the presentation layer doesn’t care about this type of details, it just cares about getting the List<CreditDraft>
when is ready. The interactor will make this happen and will abstract the process for the presentation layer or any other consumer.
The RetrieveCreditDraftList
would be enough already for the feature we are working on in this post, but for the sake of having more examples let’s imagine it is required that the CreditDraft
objects in the list must be ordered. The interactor we just created is a first level interactor, it coordinates the base operation of retrieving the List<CreditDraft>
, now this one can serve to many other second level interactors that can build on top and perform some further transformations. For instance, we could have a RetrieveOrderedCreditDraftList
interactor:
Flowable<List<CreditDraft>> getBehaviorStream( Option<OrderingStrategy> params) {
OrderingStrategy strategy = unwrapOrThrow(params);
return retrieveCreditDraftList
.getBehaviorStream(Option.none())
.map(drafts -> orderDrafts(drafts, strategy));
}
Let’s go through another example. Let’s say it is required to delete a CreditDraft
and we have to create an interactor for it. We would then create a DeleteInteractor<DeleteCreditDraftParams, CreditDraft>
that would look like this:
Single<CreditDraft> getSingle(Option<DeleteCreditDraftParams> params) {
DeleteCreditDraftParams deleteParams = unwrapOrThrow(params);
return creditRepository
.deleteCreditDraft(deleteParams.creditDraftId())
.flatMapSingle(this::fetchAndThenDraft);
}
@NonNull
private Single<CreditDraft> fetchAndThenDraft(CreditDraft draft) {
return creditRepository.fetchCreditDrafts()
.andThen(just(draft));
}
Deleting the CreditDraft
with the passed id is not the only thing this interactor does, it also triggers a fetch after a successful delete operation. We expect changes in the List<CreditDraft>
in the external source after a delete operation and therefore we want to update the ReactiveStore
accordingly. The consumers of the RetrieveInteractor
don’t know anything about other parts in the app where a DeleteInteractor
has been triggered, but still they’ll get the new set of data once the ReactiveStore
has been updated.
Presentation
We are almost done with our feature, the last step is to take the List<CreditDraft>
coming from the RetrieveCreditDraftList
interactor and transform it to the view entities.
1. Design the view entities
The view entities must be as easy as possible for the views to consume so they should be designed with the view in mind. For simplicity, we are only going to take care of CreditDraft
objects that are in the in repayment state, so we only need one view entity: the InRepaymentCardViewEntity
. In a real world case we would need to define more view entities for the different states a CreditDraft
can be in.
2. First level mappers: Map the domain objects to view entities
Next step is to create a mapper that transforms the objects coming from the interactors in the domain layer into view entities. This operation is done in mappers, more precisely for this example:
class InRepaymentCardViewEntityMapper implements Function<CreditDraft, InRepaymentCardViewEntity> {
@NonNull
public InRepaymentCardViewEntity apply(CreditDraft draft) {
...
}
}
The mappers can take other dependencies, like providers or utilities, if they are needed to perform the mapping to the final view entity.
3. Second level mappers: Apply common mappings to view entities
For a good design in our code each class should do one thing and one thing only, this is why sometimes higher level mappers are required. Usually this mappers perform some common combinations or transformations to the first level mapper’s output.
Now, to understand why we need a second level mapper here we need to briefly explain how our list is going to work. The goal is to display the view entities in a RecyclerView
. We have a RecyclerAdapter
with a method to update the items that looks like this: update(List<DisplayableItem>)
. The DisplayableItem
is just a wrapper to abstract the actual type of the view entities and be able to show different elements in the same RecyclerView
. How this exactly works is out of the scope of this post but the code can be found in the repository.
With this in mind, there is a final transformation we need to apply to every view entity:
class CreditDisplayableItemMapper implements Function<List<CreditDraft>, List<DisplayableItem>> {
List<DisplayableItem> apply(List<CreditDraft> creditDrafts) {
return Observable.fromIterable(creditDrafts)
.map(inRepaymentCardViewEntityMapper)
.map(this::wrapInDisplayableItem)
.toList()
.blockingGet();
}
}
In this case the transformation is very simple because we are only taking care of the InRepaymentCardViewEntity
. In the case we would have other view entities, the CreditDisplayableItemMapper
would be constructed with all the first level mappers and it would coordinate the process of wrapping each view entity inside the DisplayableItem
.
4. Put the pieces together in the ViewModel
We now have to create the CreditDashboardViewModel
and connect everything together. The ViewModel
is going to bind to the domain layer and update the LiveData
every time there is an emission. This subscription happens when the ViewModel
is created:
CreditDashboardViewModel(RetrieveCreditDraftList interactor,
CreditDisplayableItemMapper mapper) {
this.retrieveCreditDraftList = interactor;
this.creditDisplayableItemMapper = mapper;
// Bind view model
compositeDisposable.add(bindToCreditDrafts());
}
The resulting disposable from the subscription must be stored and disposed when the ViewModel
is cleared. This happens in the onCleared
method that we need to override:
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.dispose();
}
Let’s now see how the binding of the ViewModel
to the interactor happens. The starting point is the Flowable
that is exposed in the RetrieveCreditDraftList
interactor, this Flowable
can emit onNext
or onError
events. When there is a successful update of the data, it will emit onNext
and the emitted List<CreditDraft>
will be transformed into List<DisplayableItem>
thanks to the CreditDisplayableItemMapper
that we just created. Finally, this List<DisplayableItem>
will be passed to the LiveData
object. This chain will keep working as long as there are no errors coming through.
private Disposable bindToCreditDrafts() {
return retrieveCreditDraftList
.getBehaviorStream(none())
.map(creditDisplayableItemMapper)
.subscribe(creditListLiveData::postValue,
this::handleError
}
What happens when there is an error coming from
RetrieveCreditDraftList
?
The RetrieveCreditDraftList
interactor is designed to error. As we saw before, a fetch operation is performed when the repository doesn’t provide the requested values. This fetch operation can fail and we need to propagate this error to the presentation layer so we can decide what to do. There are two things to consider in this scenario:
- Do we want to show something in the view to give feedback about the error? If this is the case, we should then create a
ErrorViewEntity
POJO which will describe the error view as close as possible. In the same way we update theLiveData
with the list of credits when there is anonNext
event, we should then update aLiveData
object with theErrorViewEntity
when there is anonError
event. - Do we want to retry? After the
onError
event the subscription is over, the chain will stop to work and we will not receive further updates. There are operators likeretry()
,retryUntil()
orretryWhen()
that can help us to recover from this state. What they do is to resubscribe to the source in case of error following a strategy that we can specify.
5. Hook the view to the LiveData
The final step is to setup the view to consume the LiveData
provided by the ViewModel
. In this case we have the CreditDashboardFragment
which only consumes the LiveData<List<DisplayableItem>>
:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getDisplayableItemListLiveData()
.observe(this, adapter::update);
}
To know more about LiveData
and ViewModel
check out the official Android Architecture Components page.
This is the end of our journey, the feature is ready! Assuming we wrote unit tests as we made the different implementations, of course ;)
Check out the sample repository if you are interested on seeing how everything fits together in more detail:
Interested in joining one of our teams as we grow?
If you’d like to join us on the journey of building the mobile bank the world loves to use, have a look at some of the Tech roles we’re looking for here.