+2

Giới thiệu về Android Architecture Components (Phần II)

Tiếp theo series về Android Architecture Components, ở phần I (https://viblo.asia/p/gioi-thieu-ve-android-architecture-components-phan-i-1Je5EYjL5nL) chúng ta đã có những lý thuyết cơ bản để xây dựng kiến trúc cho app. Trong phần II này, ta sẽ cùng xem 1 sample để có thể hiểu nó rõ hơn.

Kiến trúc ứng dụng được đề xuất

Trong phần này, chúng ta trình bày cách cấu trúc một ứng dụng bằng cách sử dụng các Architecture Components bằng cách làm việc thông qua một ví dụ cụ thể.

Lưu ý: Không thể có một cách viết ứng dụng tốt nhất cho mọi trường hợp. Có nghĩa là, kiến trúc được đề nghị này chỉ là một điểm xuất phát tốt cho hầu hết các trường hợp sử dụng. Nếu bạn đã có cách viết ứng dụng Android tốt, bạn không cần phải thay đổi.

Hãy tưởng tượng chúng ta đang xây dựng một UI hiển thị một hồ sơ người dùng. Hồ sơ người dùng này sẽ được lấy từ backend của chúng ta bằng cách sử dụng REST API.

Xây dựng giao diện người dùng

Giao diện người dùng sẽ bao gồm một fragment UserProfileFragment.java và layout tương ứng user_profile_layout.xml .

Để điều khiển UI, mô hình dữ liệu của chúng ta cần phải giữ hai phần tử dữ liệu.

  • User ID: Mã nhận dạng cho người dùng. Tốt nhất là chuyển thông tin này vào fragment bằng cách sử dụng các argument. Nếu hệ điều hành Android hủy process của bạn, thông tin này sẽ được lưu giữ để id có sẵn trong lần tiếp theo khi ứng dụng của bạn được khởi động lại.
  • Đối tượng người dùng : Một POJO chứa dữ liệu người dùng. Chúng ta sẽ tạo một UserProfileViewModel dựa trên lớp ViewModel để giữ thông tin này.

Một ViewModel cung cấp dữ liệu cho một thành phần giao diện cụ thể, chẳng hạn như một fragment hoặc activity, và xử lý việc giao tiếp với phần xử lý dữ liệu, chẳng hạn như gọi các thành phần khác để tải dữ liệu hoặc chuyển tiếp các sửa đổi của người dùng. ViewModel không biết về View và không bị ảnh hưởng bởi các thay đổi cấu hình như tạo lại activity do rotate.

Bây giờ chúng ta có 3 file.

  • userprofile.xml : Định nghĩa UI cho màn hình.
  • UserProfileViewModel.java : Class chuẩn bị dữ liệu cho giao diện người dùng.
  • UserProfileFragment.java : Bộ điều khiển UI hiển thị dữ liệu trong ViewModel và phản ứng với các tương tác của người dùng.

Dưới đây là triển khai bắt đầu của chúng ta (tệp layout được bỏ qua cho đơn giản):

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}

Lưu ý: Ví dụ trên extends LifecycleFragment thay vì lớp Fragment . Sau khi API của Android Architecture Components trở nên ổn định, lớp Fragment trong Thư viện Hỗ trợ Android sẽ implement LifecycleOwner .

Bây giờ, chúng ta có ba mô-đun, làm thế nào để chúng ta kết nối chúng? Sau khi tất cả các trường người dùng ViewModel được thiết lập, chúng ta cần một cách để thông báo cho UI. Đây là nơi mà lớp LiveData được dùng đến.

  • LiveData là một observable data holder. Nó cho phép các thành phần trong ứng dụng của bạn theo dõi các thay đổi của đối tượng LiveData mà không tạo đường dẫn phụ thuộc rõ ràng và cứng nhắc giữa chúng. LiveData cũng tôn trọng tình trạng vòng đời của các thành phần ứng dụng của bạn (activity, fragment, service) và làm đúng để ngăn chặn các đối tượng bị rò rỉ để ứng dụng của bạn không tiêu tốn nhiều bộ nhớ hơn.

Lưu ý: Nếu bạn đã sử dụng thư viện như RxJava hoặc Agera , bạn có thể tiếp tục sử dụng chúng thay vì LiveData. Nhưng khi bạn sử dụng chúng hoặc các cách tiếp cận khác, hãy đảm bảo rằng bạn đang xử lý vòng đời đúng cách để luồng dữ liệu của bạn tạm dừng khi LifecycleOwner có liên quan bị dừng lại và các luồng bị hủy khi LifecycleOwner bị hủy. Bạn cũng có thể thêm android.arch.lifecycle:reactivestreams artifact để sử dụng LiveData với một thư viện luồng phản ứng khác (ví dụ: RxJava2).

Bây giờ chúng ta thay thế User field trong UserProfileViewModel bằng LiveData<User> để fragment có thể được thông báo khi dữ liệu được cập nhật. Những điều tuyệt vời về LiveData là nó là vòng đời nhận thức và sẽ tự động dọn sạch reference khi chúng không còn cần thiết.

public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

Bây giờ chúng ta sửa đổi UserProfileFragment để quan sát dữ liệu và cập nhật UI.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

Mỗi lần dữ liệu người dùng được cập nhật, callback onChanged sẽ được gọi và giao diện người dùng sẽ được làm mới.

Nếu bạn đã quen thuộc với các thư viện khác nơi mà các observable callback được sử dụng, bạn có thể đã nhận ra rằng chúng ta không phải ghi đè phương thức onStop() của fragment để dừng quan sát dữ liệu. Điều này không cần thiết với LiveData vì nó có thể nhận biết vòng đời, có nghĩa là nó sẽ không gọi hàm callback trừ khi fragment đó đang ở trạng thái hoạt động (nhận được onStart() nhưng không nhận được onStop() ). LiveData cũng sẽ tự động loại bỏ đối tượng quan sát khi fragment nhận được onDestroy() .

Chúng ta cũng đã không làm bất cứ điều gì đặc biệt để xử lý thay đổi cấu hình (ví dụ như người dùng xoay màn hình). ViewModel được tự động khôi phục khi cấu hình thay đổi, ngay khi fragment mới xuất hiện, nó sẽ nhận được cùng một thể hiện của ViewModel và callback sẽ được gọi ngay tức thì với dữ liệu hiện tại. Đây là lý do tại sao ViewModel không nên tham chiếu trực tiếp View; chúng có thể sống lâu hơn trong vòng đời của View. Xem thêm Vòng đời của một ViewModel .

Fetching dữ liệu

Bây giờ chúng ta đã kết nối ViewModel với fragment, nhưng làm thế nào để ViewModel tìm nạp dữ liệu người dùng? Trong ví dụ này, chúng ta giả sử rằng backend của chúng ta cung cấp một API REST. Chúng ta sẽ sử dụng thư viện Retrofit để truy cập phần backend của chúng ta mặc dù bạn được tự do sử dụng một thư viện khác nhau với cùng mục đích.

Đây là Webservice giao tiếp với backend của chúng ta:

 public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

Một ViewModel có thể trực tiếp gọi cho Webservice để lấy dữ liệu và gán nó lại cho đối tượng User. Mặc dù nó vẫn hoạt động, ứng dụng của bạn sẽ khó duy trì khi nó phát triển. Nó mang lại quá nhiều việc cho lớp ViewModel và đi ngược lại nguyên tắc tách mối quan tâm mà chúng ta đã đề cập trước đó. Ngoài ra, phạm vi của một ViewModel được gắn với vòng đời Activity hoặc Fragment , do đó mất tất cả dữ liệu khi vòng đời của nó kết thúc là một trải nghiệm người dùng không tốt. Thay vào đó, ViewModel của chúng ta sẽ ủy thác công việc này cho một mô-đun Repository mới.

  • Các module Repository lưu trữ có trách nhiệm xử lý các dữ liệu. Chúng cung cấp một API cho phần còn lại của ứng dụng. Chúng biết nơi để lấy dữ liệu và những gì API gọi để thực hiện khi dữ liệu được cập nhật. Bạn có thể coi chúng như các trung gian giữa các nguồn dữ liệu khác nhau (persistent model, web service, cache, v.v.)..

Lớp UserRepository dưới đây sử dụng WebService để lấy ra các mục dữ liệu người dùng.

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

Mặc dù mô đun Repository trông có vẻ không cần thiết, nó phục vụ một mục đích quan trọng; nó tóm tắt các nguồn dữ liệu từ phần còn lại của ứng dụng. Bây giờ ViewModel của chúng ta không cần biết rằng dữ liệu được tìm nạp bởi Webservice , có nghĩa là chúng ta có thể thay đổi nó bởi các thực thể khác nếu cần.

Lưu ý: Chúng ta đã bỏ qua trường hợp lỗi mạng để cho đơn giản. Đối với một triển khai thay thế cho thấy lỗi và trạng thái tải, hãy xem Phụ lục: hiển thị trạng thái mạng .

Quản lý sự phụ thuộc giữa các thành phần:

Lớp UserRepository ở trên cần một thực thể của Webservice để thực hiện công việc của nó. Nó có thể đơn giản tạo ra thực thể đó nhưng để làm điều đó, nó cũng cần phải biết các phụ thuộc của lớp Webservice để xây dựng nó. Điều này sẽ làm phức tạp và lặp lại mã (ví dụ như mỗi lớp cần một thực thể Webservice sẽ cần phải biết cách xây dựng nó với các thuộc tính phụ thuộc của nó). Ngoài ra, UserRepository có lẽ không phải là lớp duy nhất mà cần một Webservice . Nếu mỗi lớp tạo ra một WebService mới, nó sẽ tốn rất nhiều tài nguyên.

Có 2 pattern bạn có thể sử dụng để giải quyết vấn đề này:

  • Dependency Injection : Dependency Injection cho phép các lớp xác định sự phụ thuộc mà không cần xây dựng chúng. Khi chạy, một lớp khác chịu trách nhiệm cung cấp những phụ thuộc. Google khuyến nghị nên sử dụng thư viện Dagger 2 của Google để dùng dependency injection vào ứng dụng Android. Dagger 2 tự động xây dựng các đối tượng bằng cách đi qua cây phụ thuộc và cung cấp bảo đảm thời gian biên dịch cho các phụ thuộc.
  • Service Locator: Service Locator cung cấp một đăng ký nơi các lớp có thể có được phụ thuộc của chúng thay vì xây dựng mới. Nó tương đối dễ thực hiện hơn Dependency Injection (DI), vì vậy nếu bạn không quen thuộc với DI, hãy sử dụng một Service Locator thay thế.

Các pattern này cho phép bạn scale mã của bạn vì chúng cung cấp các hình mẫu rõ ràng để quản lý các phần phụ thuộc mà không cần nhân đôi mã hoặc thêm sự phức tạp. Cả hai đều cho phép triển khai trao đổi để thử nghiệm; đó là một trong những lợi ích chính của việc sử dụng chúng.

Trong ví dụ này, chúng ta sẽ sử dụng Dagger 2 để quản lý các sự phụ thuộc.

Kết nối ViewModel và Repository

Bây giờ, chúng ta sửa đổi UserProfileViewModel của chúng ta để sử dụng Repository.

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

(Còn tiếp)


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí