Kiến trúc MVVM kết hợp RxJava 2 với Retrofit 2

Giới thiệu

Bài viết trước về MVVM, RxJava và Retrofit sẽ được cập nhật kịch bản và thư viện mới tại đây. Lần này, chúng ta sẽ sử dụng phiên bản ổn định đầu tiên của RxJava 2 và Retrofit. Trong bài này, chúng ta sẽ xem làm thế nào để sử dụng RxJava 2 trong một ví dụ thực sự với kiến trúc MVVM sử dụng Retrofit. Chúng ta cũng sẽ tìm hiểu về cách cải thiện hiệu suất trong ứng dụng của mình với các Network Request đáp ứng vòng đời của View.

Cấu trúc ứng dụng với network

Tóm tắt các layer:

  • Retrofit Layer: Nơi thực sự tạo ra các Network Request.
  • APIService Layer: Phụ trách tạo network request, parse response trả về và xử lý nó nếu cần.
  • RequestManager Layer: Chuẩn bị data để gửi đi và liên kết các network request khác nhau.
  • ViewModel Layer: Xử lý các logic mà View ứng với nó yêu cầu.
  • View Layer: Xử lý các input từ phía User

Vòng đời gây ra các vấn đề giữa View và ViewModel

Trong bài viết trước sử dụng RxJava 1, chúng ta có Subject trong ViewModel truyền về thông tin cho View đang sử dụng Subscriber để lắng nghe.

Vấn đề chung mà chúng ta gặp phải: Chúng ta không muốn huỷ network request hay tạo ra nhiều network request khi ứng dụng chuyển sang trạng thái background.

Một trong những vấn đề chúng ta đang phải đối mặt là phương thức onNext() hoặc onComplete() của Subscriber/Observer được gọi và View đang không có trên màn hình. Nếu Subscriber cố gọi lại tới View và trong phương thức đó có cập nhật bất kỳ UI Widget nào, ứng dụng sẽ bị crash. Subject đã rất hữu ích khi giữ thông tin cho đến khi View xuất hiện để lấy nó.

Giao tiếp giữa View và ViewModel bằng một interface (callback) gọi là Contract. Điều đó cho phép chúng ta linh hoạt plug and play bất kỳ View nào vào ViewModel. Hãy tưởng tượng rằng bạn có các View khác nhau tuỳ thuộc vào đó là smartphone, tablet hay smartwatch... Tất cả chúng đều chia sẻ cùng một ViewModel nhưng không nhất thiết phải theo cách khác.

Làm thế nào để giải quyết vấn đề vòng đời?

Một interface được định nghĩa để truyền lại cho biết những gì đang xảy ra trong từng thời điểm.

public interface Lifecycle {

    interface View {

    }

    interface ViewModel {

        void onViewResumed();
        void onViewAttached(@NonNull Lifecycle.View viewCallback);
        void onViewDetached();
    }
}

View sẽ gọi phương thức onViewResumed() của Lifecycle.ViewModel trong phương thức onResume() của nó. Phương thức Lifecycle.ViewModel#onViewAttached(this) sẽ được gọi trong phương thức onStart()Lifecycle.ViewModel#onViewDetached() được gọi trong onStop().

Với điều đó, ViewModel sẽ nhận biết được vòng đời và logic để quyết định khi nào hiển thị hay không, như vậy nó có thể hành động phù hợp và thông báo cho View khi nó nhận được thông tin.

Contract giữa View và ViewModel

Contract xác định những gì View cần từ ViewModel và ngược lại. Thông thường chúng ta định nghĩa một Contract dựa vào màn hình, mặc dù có thể dựa vào mỗi chức năng là tốt. Ví dụ, chúng ta có màn hình Home có khả năng refresh dữ liệu người dùng. Chúng ta định nghĩa Contract như sau:

public interface HomeContract {

    interface View extends Lifecycle.View {

        void showSuccessfulMessage(String message);
    }

    interface ViewModel extends Lifecycle.ViewModel {

        void getUserData();
    }
}

Contract này được extend từ Lifecycle, vì vậy ViewModel cũng sẽ nhận thức được vòng đời.

Các loại Reactive Stream của RxJava 2

Với RxJava 2, một số khái niệm mới đã được giới thiệu và một số khái niệm khác đã được đổi tên. Tham khảo tại đây.

Sự khác biệt chính giữa chúng là xử lý back-pressure. Về cơ bản, Flowable là một Observer xử lý back-pressure. Và nhớ rằng, Completable, SingleMaybe không xử lý back-pressure.

Trong ví dụ, chúng ta sẽ để retrofit return về một đối tượng Observable. Nếu chúng ta muốn xử lý back-pressure, hay nếu chúng ta biết kết quả kỳ vọng và muốn tối ưu hoá code để xác định Stream chúng ta muốn có?

Sử dụng Completable

Quan sát ví dụ Registration gọi. Bởi vì RegistrationAPIService đang xử lý thông tin, chúng ta không muốn nhận lại một Stream vì response không cần được sử dụng ở tầng RequestManager. Chúng ta chỉ quan tâm lời gọi có thành công hay không. Vì vậy, chúng ta sẽ return lại một đối tượng Completable, bỏ qua các phần tử mà chúng ta nhận được từ Observable.

public Completable register(RegistrationRequest request) {

    return registrationAPI.register(request)
            .doOnSubscribe(disposable -> isRequestingRegistration = true)
            .doOnTerminate(() -> isRequestingRegistration = false)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .onErrorResumeNext(this::handleRegistrationError)
            .doOnNext(registrationResponse -> processRegistrationResponse(request, registrationResponse))
            .ignoreElements();
}

Sử dụng Maybe

Nếu chúng ta muốn truyền response về tầng RequestManager nhưng vì nó là network request, chúng ta biết rằng sẽ nhận được chỉ một đối tượng, chúng ta có thể sử dụng Maybe (body của nó có thể rỗng, vì vậy chúng ta sử dụng Maybe để tránh ngoại lệ khi các đối tượng null).

Hãy nhớ sử dụng toán tử singleElement() chứ không dùng firstElement(). Nếu sử dụng toán tử firstElement() và chúng ta không có gì, nó sẽ ném một ngoại lệ vì nó luôn cố gắng để truy cập vào thành phần đầu tiên ngay cả khi nó không có.

public Maybe<LoginResponse> login(LoginRequest request) {

    return loginAPI.login(request.getNickname(), request.getPassword())
            .doOnSubscribe(disposable -> isRequestingLogin = true)
            .doOnTerminate(() -> isRequestingLogin = false)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .onErrorResumeNext(this::handleLoginError)
            .doOnNext(this::processLoginResponse)
            .singleElement();
}

Sử dụng Flowable

Như chúng ta đã nói, một Flowable sẽ có hành vi tương tự một Observable nhưng có xử lý back-pressure. Vì vậy, khi chuyển đổi từ một Observable đến một Flowable, chúng ta phải xác định chiến lược (strategy) mà chúng ta muốn sử dụng.

Có các strategy khác nhau: BUFFER (buffer tất cả các giá trị onNext cho đến khi downstream sử dụng nó), DROP (drop giá trị onNext gần nhất nếu downstream không thể theo kịp), ERROR (báo hiêu một lỗi MissingBackpressureException trong trường hợp downstream không thể theo kịp) là hành vi tương tự Observable, LATEST (giữ lại duy nhất giá trị onNext mới nhất, overwrite bất kỳ giá trị nào trước đó nếu downstream không thể theo kịp), MISSING (event onNext được viết mà không có bất kỳ một buffer hay drop nào).

Ví dụ lấy dữ liệu Games, chúng ta sẽ sử dụng BUFFER vì chúng ta không muốn bỏ mất bất kỳ game nào trong trường hợp downstream không thể theo kịp. Nó có thể chậm hơn nhưng tất cả dữ liệu sẽ được giữ lại.

public Flowable<GamesResponse> getGames(GamesRequest request) {

    return gamesAPI.getGamesInformation(request.getNickname())
            .doOnSubscribe(disposable -> isRequestingGames = true)
            .doOnTerminate(() -> isRequestingGames = false)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doOnError(this::handleAccountError)
            .toFlowable(BackpressureStrategy.BUFFER);
}

Sử dụng toán tử Zip để tạo các network request khác nhau tại cùng thời điểm

Nếu chúng ta muốn thực hiện các network request khác nhau cùng một lúc và chỉ muốn nhận thông báo (notify) khi tất cả các yêu cầu đã thành công, sử dụng toán tử Zip. Đây thực sự là một tính năng mạnh mẽ.

#UserDataRequestManager.java
public Flowable<Object> getUserData() {

    return Flowable.zip(
                getAccount(),
                getGames(),
                this::processUserDataResult);
}
private Flowable<AccountResponse> getAccount() {

    return accountAPIService.getAccount(createAccountRequest());
}

private Flowable<GamesResponse> getGames() {

    return gamesAPIService.getGames(createGamesRequest());
}

Nối tiếp các network request khác nhau

Chúng ta có thể thấy mỗi request network trả về một loại Stream khác nhau. Hãy xem chúng ta có thể nối tiếp chúng như thế nào. Ý tưởng là tạo Registration request, Login và sau đó là UserData.

UserData return một Flowable. Tuy nhiên, Login request lại return một Maybe. Chúng ta phải nối tiếp chúng lại với nhau:

#AuthenticationRequestManager.java
private MaybeSource<Object> makeGetUserDataRequest(LoginResponse loginResponse) {

    return userDataRequestManager.getUserData().singleElement();
}

Login request sẽ nhận được UserData nếu response thành công. Chúng ta chuẩn bị getUserDataRequestMethod để return một Maybe, chúng ta có thể nối tiếp chúng với toán tử flatMap().

#AuthenticationRequestManager.java
public MaybeSource<Object> login() {

    return loginAPIService.login(createLoginRequest())
            .flatMap(this::makeGetUserDataRequest);
}

Bây giờ nếu chúng ta muốn thực hiện lời gọi Registration và sau đó là Login request, chúng ta chỉ cần gọi nó sau khi Completable kết thúc. Chúng ta làm điều này với toán tử andThen().

#AuthenticationRequestManager.java
public MaybeSource<Object> register() {

    return registrationAPIService.register(createBodyForRegistration())
            .andThen(makeLoginRequest());
}
private MaybeSource<Object> makeLoginRequest() {

    return login();
}

Kết luận

Hãy chắc chắn rằng khi bạn chuyển code tới RxJava 2 mà bạn sử dụng Stream và Observer như đã nói đến.

Đây là một bản tóm tắt về cách cấu trúc ứng dụng của bạn bằng cách sử dụng kiến trúc MVVM và xử lý vòng đời của View một cách hiệu quả.