Reaktywne podejście do Clean Architecture

Reaktywne podejście do Clean Architecture
Do Clean Architecture można podejść na kilka sposobów. Jednym z rozwiązań jest zastosowanie RxJavy 2 i w tym poście przyjrzymy się bliżej tej opcji. W warstwie prezentacji użyjemy sprawdzonego wzorca Model View Presenter, w warstwie domenowej będziemy mieć UseCase'y z pojedynczą odpowiedzialnością, natomiast w warstwie danych Repository Pattern. Wszystko to będzie sterowane streamami z RxJavy.

Repository Pattern

Przypuśćmy, że mamy presenter, który pobiera nam użytkowników i ustawia ich dane wywołując odpowiednie metody na widoku. Presenter nie powinien wiedzieć, skąd są dane, dlatego początkowo stwórzmy UsersRepository, które będzie korzystać z bazy danych oraz serwisu REST.

W poniższych przykładach użyty został także Dagger2. Nie jest on niezbędny, jednak znacznie ułatwia implementację.

@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));
    }
}

Załóżmy tutaj uproszczoną logikę, jakoby ładowanie użytkowników z repozytorium dostarczało elementy pobrane z serwera, jak i z bazy danych. Możemy to zrobić przez skonkatenowanie 2 streamów pochodzących z UsersService i DatabaseManager, obiektów wstrzykiwanych za pomocą Daggera do repozytorium. Oba zwracają dane za pomocą Flowable, jednak serwis dostarcza nam Flowable<List<User>>, zaś baza danych Flowable<User>. Dzięki operatorowi fromIterable przerabiamy stream listy na stream elementów i konkatenujemy oba streamy poprzez concatWith. Dodatkowo metoda loadUsers przyjmuje w argumencie płeć, po jakiej ma filtrować użytkowników.

Use Case

Mamy już gotowe repozytorium, ale presenter nie powinien korzystać z niego bezpośrednio. Do presenterów wstrzykujemy przede wszystkim UseCase’y, które to mogą korzystać z wielu repozytoriów. UseCase’y definiują nam wszystkie akcje, jakie można wykonywać w aplikacji, a każdy z nich ma pojedynczą odpowiedzialność. Stwórzmy sobie więc GetUsersUseCase, który pobiera użytkowników z repozytorium. Nazewnictwo zależy oczywiście od konwencji, ja wybrałem poniższą.

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();
    }
}

Jak widać, możemy tu wyszczególnić metodę buildUseCaseSingle(), którą nadpisujemy z klasy nadrzędnej – SingleUseCase. To co tam się znajduje omówimy za chwilę, natomiast na razie najważniejsze jest to, aby wewnątrz metody buildUseCaseSingle() zwrócić stream, który będzie interesował presentera. W tym przypadku jest to Single zawierający listę użytkowników. Jeśli chcemy wywołać metodę z repository z wybranymi parametrami po prostu tworzymy dodatkowe pole i 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 to bazowy UseCase zwracający Single danego typu. Single to po prostu stream zwracający tylko jeden element, po jego zasubskrybowaniu jesteśmy więc w stanie obsłużyć onSuccess bądź onError. Tworzymy w tym celu metodę execute w 2 wariantach: gdy nie mamy zamiaru obsługiwać błędu (jest to niezalecane) oraz w przypadku, gdy chcemy błąd obsłużyć.

Jako, że metoda buildUseCaseSingle() jest abstrakcyjna, a my właśnie ją zaimplementowaliśmy w GetUsersUseCase, należy z niej w tym miejscu skorzystać. Najpierw ustawiamy odpowiednie Schedulery, tutaj robimy to za pomocą compose i własnej metody z klasy utilowej. W skrócie sprowadza się to do użycia operatorów subscribeOn(Schedulers.io()) oraz observeOn(AndroidSchedulers.mainThread()), dzięki czemu wszystko będzie się pobierać asynchronicznie w tle, a na koniec odpowiednie dane wykorzystamy na głównym wątku.

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

W metodach execute subskrybujemy więc zaimplementowaną już metodę i przypisujemy wynik do obiektu disposable, który znajduje się w bazowej abstrakcyjnej klasie UseCase:

public abstract class UseCase {

    protected Disposable disposable = Disposables.disposed();

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

Robimy to po to, aby w każdej chwili móc przerwać cały stream kiedy tego potrzebujemy. Pozwoli nam na to metoda dispose(), którą presenter będzie wywoływać na UseCase wtedy kiedy np. będzie permanentnie niszczony. Analogicznie możemy stworzyć także FlowableUseCase czy CompletableUseCase.

Presenter

Na koniec wszystko sprowadza się do wstrzyknięcia UseCase do presentera i wywołaniach:

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

Implementujemy metody consumeUsers i handleError, w których możemy działać na widoku. Kiedy zaś niszczymy nasz widok i odłączamy presentera permanentnie wystarczy wywołać:

getUsersUseCase.dispose();

Możemy to zrobić na wiele sposobów, np. po wstrzyknięciu kilku UseCase’ów do presentera zapisywać je do listy, a w razie potrzeby użyć:

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

Celowo nie pokazuję pełnej implementacji presentera, bo sposobów na wykonanie MVP jest wiele, a Clean Architecture można użyć praktycznie w każdym z nich. Wystarczy, że presenter będzie używał odpowiednich UseCase’ów.

Zdaję sobie sprawę, że jest to tylko pewne wprowadzenie, dlatego pokazuję najważniejsze elementy architektury, aby nie zaciemniać idei. Podejście Clean Architecture można zaimplementować na wiele sposobów, to jest po prostu jeden z nich. Dzięki niemu w prosty sposób możemy otrzymać spójną, testowalną architekturę aplikacji.

Dowiedz się więcej

5 powodów, dla których warto budować UI w ConstraintLayoucie

ConstraintLayout to popularne rozwiązanie, z którego twórcy aplikacji na Androida korzystają na co dzień. I nie bez powodu! Jeśli dopiero zaczynasz przygodę z programowaniem, poznaj najważniejsze zalety tego layoutu. Dzięki temu szybciej zrozumiesz jego działanie, a budowa aplikacji stanie się łatwiejsza.
Przeczytaj

Jak wykorzystać feature flags, żeby zyskać większą kontrolę nad aplikacją?

Chyba każdy, kto zajmuje się budową oprogramowania może opowiedzieć kilka historii o niedziałających funkcjach. Starannie tworzymy aplikacje z niewielkich elementów, stosujemy zaawansowane wzorce architektury, ale i tak czasem coś odmawia posłuszeństwa. Skutkuje to błędami, a nawet awarią systemu. Wtedy sytuację może uratować feature toggling. Sprawdź, jak wdrożyć feature flags i zwiększ stabilność swojej aplikacji.
Przeczytaj

Wycena projektu

Sprawdź, jak wykorzystujemy naszą wiedzę w praktyce i stwórz z nami swój projekt.

Dlaczego warto rozwijać z nami projekty?

Logo Mobile Trends Awards

Mobile Trends Awards 2017

Nominacja w kategorii
M-COMMERCE

17

opinii klientów

Clutch logo
Logo Legalni bukmacherzy

Nagroda Legalnych Bukmacherów 2019

Najlepsza aplikacja mobilna

60+

zrealizowanych projektów