12/08/2018, 14:49

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

Giới thiệu Phát triển một ứng dụng Android theo mô hình kiến trúc để dễ dàng maintain và debug là việc không dễ dàng. Hơn nữa, giao tiếp và tương tác Network giữa các View của ứng dụng còn khó hơn. Một trong những điều được đánh giá cao về kiến trúc tốt là khi bạn có một khuyết điểm trong ứng ...

Giới thiệu

Phát triển một ứng dụng Android theo mô hình kiến trúc để dễ dàng maintain và debug là việc không dễ dàng. Hơn nữa, giao tiếp và tương tác Network giữa các View của ứng dụng còn khó hơn.

Một trong những điều được đánh giá cao về kiến trúc tốt là khi bạn có một khuyết điểm trong ứng dụng và bạn biết chính xác nơi sửa nó.

Trong bài viết này chúng ta sẽ học cách để tạo ra một kiến trúc sử dụng cả RxJava và Retrofit. Chúng ta sẽ tập trung và RxJava, có lẽ bạn đã được nghe nói rất nhiều về nó, nhưng khi tìm hiểu đến thực sự có rất nhiều khó khăn.

Kiến trúc

Model View ViewModel (MVVM) là mô hình kiến trúc chúng ta sẽ sử dụng trong bài viết này. Các View sẽ chỉ có trách nhiệm hiển thị thông tin, xử lý các UI logic sẽ là trách nhiệm của ViewModel.

Networking (Phần mạng)

Phần mạng (Networking) có thể là một trong những khó khăn nhất tuỳ thuộc vào các yếu tố khác nhau như: tần suất, có bao nhiêu nơi gọi request tới, cách gọi các request nối tiếp hay kết hợp đồng thời với nhau, v.v... Chúng ta sẽ tạo mỗi class có một mục đích, trách nhiệm riêng. View và ViewModel lần lượt thực hiện nhiệm vụ hiển thị thông tin và xử lý các thông tin cho view hiển thị. Với Networking, chúng ta sẽ cần những gì? Trước hết, chúng ta sử dụng retrofit, chúng ta sẽ phải tạo các Interface cho các nơi gọi (endpoint) khác nhau và một class để thực hiện gọi các request. Ta gọi đây là loại class APIService. Chẳng hạn, cho quá trình đăng ký (registration) chúng ta có class RegistrationAPIService, quá trình đăng nhập (login) có class LoginAPIService,... Class này cũng là nơi xử lý các lỗi khác nhau và logic áp dụng khi gọi thành công. Chúng ta cũng sẽ cần một class phụ trách việc chuẩn bị data cho các request. Class có thể là ViewModel, nhưng... Nếu chúng ta gọi các request tương tự từ các ViewModel khác nhau? Chúng ta sẽ phải lặp lại cùng code và sửa những điều tương tự nếu có gì đó sai. Còn nếu chúng ta phải kết hợp gọi các request cùng nhau? Chúng ta sẽ phải đặt cùng logic một lần nữa trong các ViewModel khác nhau. Như vậy, chúng ta có thể tạo ra một class mà mục đích là chuẩn bị data và liên kết các yêu cầu khác nhau. Ta gọi đây là loại class RequestManager. Như trên, chúng ta có class UserAuthenticationRequestManager phụ trách đăng nhập (login) và đăng ký (registration), class UserDataRequestManager để lấy dữ liệu người dùng... Tóm lại: View hiển thị thông tin. ViewModel xử lý các thông tin cho View tương ứng hiển thị. RequestManagers chuẩn bị data và liên kết các request khác nhau. APIServices thực hiện các request, xử lý các response và error.

Interactions (Tương tác)

Chúng ta sẽ thấy rõ ràng hơn về kiến trúc thông qua ví dụ sau:

  1. User tap vào button để đăng ký.
  2. Fragment nói với ViewModel (VM) rằng button đã được tap.
  3. ViewModel xử lý và nói với UserAuthenticationRequestManager (RM) để đăng ký.
  4. RM chuẩn bị data (được truyền từ VM) và nói cho RegistrationAPIService để đăng ký user với data đó.
  5. RegistrationAPIService tạo request, xử lý error hay nhận response trả về (chẳng hạn lưu trữ lại thông tin). Sau đó nó trả lời với RM rằng mọi thứ đã được thực hiện và user đã được đăng ký.
  6. Khi đó, RM xác nhận việc đăng ký đã thành công. Bây giờ, chẳng hạn theo yêu cầu của ứng dụng, phải đăng nhập với user vừa được đăng ký đó. Như vậy, RM phải chuẩn bị dữ liệu đăng nhập để tạo request đó. Khi dữ liệu đã được chuẩn bị, nó gọi tới LoginAPIService.
  7. LoginAPIService tạo request và xử lý tương tự như RegistrationAPIService. Sau đó cũng trả về cho RM kết quả của việc đăng nhập.
  8. Bây giờ, RM mới nói cho ViewModel và ViewModel thông báo cho View rằng user đã đăng ký và đăng nhập thành công.
  9. Fragment (View) hiển thị kết quả cho user.

Implementation với RxJava

Tầng Retrofit

public interface IRegistrationAPI {

    @POST("/registration")
    Observable<RegistrationResponse> register(@Body RegistrationRequest request);
}

Vì chúng ta muốn nhận được kết quả trả về, chúng ta mong đợi một đối tượng Response. Retrofit sẽ gọi onNext (với đối tượng Response) và onComplete khi nó kết thúc. Nếu có lỗi xảy ra, nó sẽ gọi onError.

Tầng APIService

APIService chịu trách nhiệm tạo các request API, xử lý các lỗi trả về, parsing response đăng ký và xử lý các response đó. UserAuthenticationRequestManager sẽ gọi phương thức register với đối tượng RegistrationRequest như một parameter.

public Observable<RegistrationResponse> register(RegistrationRequest request) {

        return registrationAPI.register(request)
                .doOnSubscribe(() -> isRequestingRegistration = true)
                .doOnTerminate(() -> isRequestingRegistration = false)
                .doOnError(this::handleRegistrationError)
                .doOnNext(registrationResponse -> processRegistrationResponse(request, registrationResponse));
    }

Biến isRequestingRegistration được sử dụng để nhận biết request đăng ký có đang được gọi hay không. Thiết lập giá trị true trong phương thức doOnSubscribe().

private void handleRegistrationError(Throwable throwable) {

        if (throwable instanceof RetrofitError) {

            int status = ((RetrofitError) throwable).getResponse().getStatus();

            if (status == 401) {
                throw new RegistrationNicknameAlreadyExistsException();
            } else {
                throw new RegistrationTechFailureException();
            }

        } else {
            throw new RegistrationTechFailureException();
        }
    }

Phương thức trên sẽ được gọi trong doOnError(). Đây là nơi xử lý các error xảy ra khi gọi request.

Khi có respone trả về đối tượng RegistrationResponse, chúng ta có thể xử lý nó và áp dụng các logic cần thiết. Chẳng hạn trong ví dụ này, chúng ta lưu lại nickname đã đăng ký. Thực hiện gọi phương thức này trong doOnNext().

private void processRegistrationResponse(RegistrationRequest registrationRequest, RegistrationResponse registrationResponse) {
        privateSharedPreferencesManager.storeUserNickname(registrationRequest.getNickname());
    }

Tầng RequestManager

RequestManager sẽ chuẩn bị dữ liệu cho các request và liên kết các request với nhau như mong muốn. Chẳng hạn, chúng ta cần đăng ký và nếu đăng ký thành công chúng ta muốn tiếp tục đăng nhập vào ứng dụng với tài khoản vừa đăng ký xong. Để thực hiện được điều đó, chúng ta phải nối tiếp các request khác nhau: registration và login.

LoginAPIService sẽ thực hiện chính xác như những gì RegistrationAPIService làm: nó sẽ dùng LoginRequest nhận được từ RM truyền tới, gọi request API và trả về Observable<LoginResponse> nếu thành công.

Cách để nối tiếp các request khác nhau là sử dụng thao tác flatMap.

public Observable<Object> register() {
        return registrationAPIService.register(createBodyForRegistration())
                .flatMap(this::makeLoginRequest);
    }
    
private Observable<Object> makeLoginRequest(RegistrationResponse registrationResponse) {
    return login();
}

public Observable<Object> login() {
    return loginAPIService.login(createLoginRequest())
            .flatMap(this::makeGetUserDataRequest);
}

private Observable<Object> makeGetUserDataRequest(LoginResponse loginResponse) {
    return userDataRequestManager.getUserData();
}

Ví dụ trên cho thấy cách nối tiếp và xử lý đồng bộ các request khác nhau. Bây giờ, giả sử chúng ta cần xử lý bất đồng bộ các request khác nhau. Tức là, chúng ta gọi tới cùng lúc 2 request khác nhau, và chỉ khi cả 2 cùng có response thì mới xử lý kết quả trả về. Kịch bản này được thực hiện nhờ Observable.zip().

public Observable<Object> getUserData() {
    return Observable.zip(
                getAccount(),
                getGames(),
                this::processUserDataResult);
}

Chúng ta có thể thấy getAccount và getGames là 2 phương thức trả về một Observable. Chúng ta đã kết hợp chúng.

Tầng ViewModel

Tầng này có thể nói là khó khăn nhất. Bởi vì: Nếu chúng ta thực hiện subscribe trong tầng View, khi View xuống background và chúng ta unsubscribe Observable, nó sẽ huỷ request đang gọi. Đấy là điều xảy ra khi gọi unsubscribe ở onPause. Để request vẫn được gọi, chúng ta sẽ để ViewModel subscribe tới Observable mà RequestManager trả về. Sử dụng Subjects để làm điều đó.

private AsyncSubject<Object> registrationSubject;

    public RegistrationViewModel(AuthenticationRequestManager authenticationRequestManager) {

        this.authenticationRequestManager = authenticationRequestManager;
        registrationSubject = AsyncSubject.create();
    }

    public AsyncSubject<Object> createRegistrationSubject() {

        registrationSubject = AsyncSubject.create();
        return registrationSubject;
    }

    public AsyncSubject<Object> getRegistrationSubject() {

        return registrationSubject;
    }

    public void register() {

        authenticationRequestManager.register()
                .subscribe(registrationSubject);
    }

Tầng View

private Subscription registrationSubscription;

@Override
public void onResume() {
    super.onResume();
    subscribeForNetworkRequests();
    ...
}

@Override
public void onPause() {
    super.onPause();
    unsubscribeFromNetworkRequests();
    ....
}

@Override
protected void subscribeForNetworkRequests() {
    registrationSubscription = registrationViewModel.getRegistrationSubject()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new RegistrationSubscriber());
}

@Override
protected void unsubscribeFromNetworkRequests() {
    if (registrationSubscription != null) {
        registrationSubscription.unsubscribe();
    }
}

Kết luận

Mọi người đều đã nghe về RxJava, nhưng không có nhiều Developer muốn giải quyết nó. Bởi vì nó là khái niệm khó và chúng ta không đề cập đến nó khi mới làm quen với việc phát triển ứng dụng Android. Thực sự là khá khó khăn khi mới làm quen với nó nhưng càng tìm hiểu chúng ta sẽ càng thấy hấp dẫn. Nhớ rằng RxJava là một công cụ mạnh mẽ làm đơn giản code của bạn. Hãy sử dụng khi chúng ta cần tới nó.

0