07 February

Clean Architecture – reactive approach

Clean Architecture can be approached in several ways. One solution is to use RxJava 2, and in this post we will take a close look at this approach.In the presentation layer we will use the proven standard Model View Presenter, in the domain layer we will have UseCases with a single responsibility, and in the data layer we will apply Repository Pattern. All of this will be controlled with RxJava streams.

In the following examples we also used Dagger2. It is not necessary, however it makes implementation much easier.

Repository Pattern

Suppose that we have a presenter that gets us users and sets their data by calling the appropriate methods on the view. The presenter should not know where the data is from, therefore, first, let's create UsersRepository, which will use the database and REST services.

@Singleton
public class UsersRepository {

    public static final int DEFAULT_AMOUNT = 5;
    private UsersService usersService;
    private DatabaseManager databaseManager;

    @Inject
    public UsersRepository(UsersService usersService, DatabaseManager databaseManager) {
        this.usersService = usersService;
        this.databaseManager = databaseManager;
    }

    public Flowable<User> loadUsers(String gender) {
        Flowable<User> remoteUsers = usersService.getUsersList(DEFAULT_AMOUNT).flatMap(Flowable::fromIterable);
        Flowable<User> localUsers = databaseManager.getUsers();
        return remoteUsers.concatWith(localUsers).filter(user -> user.getGender().equals(gender));
    }
}

Let's assume here that we are dealing with simplified logic, as if getting users from the repository provided the items from both the server and the database. We can do this by concatenating 2 streams from UsersService and DatabaseManager, objects injected to the repository using Dagger. They both return the data using Flowable, however, the service provides us with a Flowable<List<User>>, and the database with Flowable<User>. Thanks to the operator fromIterable we process stream of a list to stream of items and concatenate both streams through concatWith. In addition, the method loadUsers accepts the argument of sex, which it uses to filter the users.

Use Case

We already have a finished repository, but the presenter should not use it directly. Into presenters we inject primarily UseCases, which can use multiple repositories. UseCases define all the actions that can be performed in the application and each of them has a single responsibility. So let's create a GetUsersUseCase that gets users from the repository. Obviously, naming depends on convention, this is just the one I chose.

public class GetUsersUseCase extends SingleUseCase<List<User>> {

    private UsersRepository userRepository;
    private String gender;

    @Inject
    public GetUsersUseCase(UsersRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    protected Single<List<User>> buildUseCaseSingle() {
        return userRepository.loadUsers(gender).toList();
    }
}

As you can see, we can distinguish the method buildUseCaseSingle(), which we will override from the superior class - SingleUseCase. We will discuss its content in a moment, but for now the most important is that inside buildUseCaseSingle() method we return a stream that will be of interest to the presenter. In this case, it is a Single containing the list of users. If we want to call the method from the repository with selected parameters, we simply create an additional field and setter.

Single Use Case
public abstract class SingleUseCase<T> extends UseCase {

    protected abstract Single<T> buildUseCaseSingle();

    private Single<T> buildUseCaseSingleOnIo() {
        return buildUseCaseSingle().compose(RxUtil.applySingleIoSchedulers());
    }

    public void execute(final Consumer<? super T> onSuccess) {
        disposable = buildUseCaseSingleOnIo().subscribe(onSuccess);
    }

    public void execute(final Consumer<? super T> onSuccess, final Consumer<Throwable> onError) {
        disposable = buildUseCaseSingleOnIo().subscribe(onSuccess, onError);
    }
}

SingleUseCase is a base UseCase returning Single of the chosen type. Single is simply a stream returning only one item, so after subscribing to it, we can handle onSuccess or onError. In this order we create the method execute in 2 versions: when we do not intend to handle the error (it is not advised to use), and for the case when we want to handle the error. As the method buildUseCaseSingle() is abstract, and we have just implemented it in GetUsersUseCase, we should use it at this point. First, we set the appropriate Schedulers, here we are using compose and own method of util class. In short, it boils down to the use of operators subscribeOn(Schedulers.io()) and observeOn(AndroidSchedulers.mainThread()), so that everything will be downloaded asynchronously in the background, and at the end we can use relevant data in the main thread.

public static <T> SingleTransformer<T, T> applySingleIoSchedulers() {
      return single -> single.subscribeOn(Schedulers.io())
              .observeOn(AndroidSchedulers.mainThread());
}

Therefore in the execute methods we subscribe to already implemented method and assign the result to object disposable, which is located in the abstract base class UseCase:

public abstract class UseCase {

    protected Disposable disposable = Disposables.disposed();

    public void dispose() {
        disposable.dispose();
    }
}

We do this in order to be able to stop the whole stream at any moment we need to. We can do it with the method dispose(), which the presenter will call on UseCace when it will be to be permanently destroyed or so. Similarly, we can also create FlowableUseCase or CompletableUseCase.

Presenter

At the end it all comes down to injecting UseCase to the presenter and calling:

getUsersUseCase.setGender(gender);
getUsersUseCase.execute(this::consumeUsers, this::handleError);

We implement methods consumeUsers and handleError in which we can work on the view. And when we destroy our view and disconnect the presenter permanently, we just need to call:

getUsersUseCase.dispose();

We can do this in many ways, for example after injecting a few UseCases to the presenter save them to the list and, if necessary, use:

Flowable.fromIterable(useCases).subscribe(UseCase::dispose);

I intentionally do not show the full implementation of the presenter, as there are many methods of doing MVP, and Clean Architecture can be used in virtually any of them. It is enough that the presenter uses appropriate UseCases.

Summary

I know that this is just an introduction, which is why I show the most important elements of architecture, so as not to hide the idea. Clean Architecture approach can be implemented in many ways, this is just one of them. Thanks to it, we can obtain a coherent, testable application architecture in a simple way.