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

Dagger 2 – kilka słów o (sub)komponentach

Wraz z zagłębianiem się w tematykę wstrzykiwania zależności, musimy zrozumieć wiele nowych technik, które umożliwiają nam kontrolę nad tworzonymi obiektami. Dagger pozwala programiście zadeklarować interfejsy, które będą się tym zajmowały. Możemy stworzyć interfejs będący komponentem lub subkomponentem. Jaka jest różnica?
Przeczytaj

ConstraintLayout, czyli jednak nie tak prędko…

Przeglądając ostatnimi czasy nowinki ze świata Androida trafiłam na pojęcie ConstraintLayout. Jest to nowy dostarczony przez Androida i Google layout, wspierający wersje Androida aż od API 9. Wczytując się w możliwości, jakie ma dawać, stwierdziłam, że sprawdzę, jak zachowuje się nowy Layout Builder i jak wygląda posługiwanie się ConstraintLayoutem.
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