ViewModels và LiveData: Patterns + AntiPatterns

1. Views and ViewModels

Distributing responsibilities

Lý tưởng nhất là ViewModels không nên biết gì về Android. Điều này cải thiện khả năng test, leak safety và tính mô đun. Nguyên tắc chung là đảm bảo rằng không có android. * import trong ViewModels (với các ngoại lệ như android.arch. *). Điều tương tự cũng áp dụng cho presenter.

❌ Không để cho ViewModels (và Presenters) biết về các class của Android framework.

Các condition statement, các vòng lặp và các general decision phải được thực hiện trong ViewModels hoặc các lớp khác của một ứng dụng, chứ không phải trong các activity hoặc các fragment. View thường không được unit test (trừ khi bạn sử dụng Robolectric) vì vậy ít dòng code hơn. View chỉ nên biết cách hiển thị dữ liệu và gửi các sự kiện của người dùng đến ViewModel (hoặc Presenter). Đây được gọi là mô hình Passive View.

✅ Giữ logic trong Activities và Fragments ở mức thấp nhất.

View references trong ViewModels

ViewModels có phạm vi khác so với các activities hay fragments. Trong khi ViewModel vẫn còn và đang chạy, một activity có thể ở bất kỳ trạng thái vòng đời nào của nó. Các activities và fragments có thể bị phá hủy và tạo ra một lần nữa trong khi ViewModel không biết.

Pasing một tham chiếu của View (activity hoặc fragment) với ViewModel là một nguy cơ nghiêm trọng. Giả sử rằng ViewModel yêu cầu dữ liệu từ network và dữ liệu quay lại sau một thời gian. Vào thời điểm đó, tham chiếu View có thể bị phá hủy hoặc có thể là một activity cũ không còn nhìn thấy được nữa, tạo ra sự rò rỉ bộ nhớ, và có thể là một crash.

✅ tránh references tới View trong ViewModel.

Cách được đề nghị để giao tiếp giữa ViewModels và Views là observer pattern, sử dụng LiveData hoặc các observables từ các thư viện khác.

Observer Pattern

Một cách rất thuận tiện để thiết kế lớp presentation trên Android là để View (activity or fragment) quan sát (subscribe các thay đổi) của ViewModel. Khi ViewModel không biết về Android, nó không biết Android làm thế nào để kill View thường xuyên. Điều này có một số lợi thế:

  • ViewModels vẫn tiếp tục tồn tại trong quá trình thay đổi cấu hình, do đó không cần phải truy vấn lại nguồn bên ngoài cho dữ liệu (chẳng hạn như database hoặc network) khi rotation xảy ra.
  • Khi activity dài hạn kết thúc, các observables trong ViewModel được cập nhật. Không quan trọng nếu dữ liệu đang được quan sát hay không. Không có ngoại lệ con trỏ null xảy ra khi cố gắng cập nhật View không tồn tại.
  • ViewModels không tham chiếu các lượt xem vì vậy có ít nguy cơ rò rỉ bộ nhớ.
private void subscribeToModel() {
  // Observe product data
  viewModel.getObservableProduct().observe(this, new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) {
        mTitle.setText(product.title);
      }
  });
}

-> Thay vì push data vào UI, hãy để UI observe thay đổi từ nó.

2. Fat ViewModels****

Bất cứ điều gì cho phép bạn tách mối quan tâm là một ý tưởng hay. Nếu ViewModel của bạn đang nắm giữ quá nhiều code hoặc có quá nhiều trách nhiệm, hãy xem xét:

  • Di chuyển một số logic ra một presenter, với scope giống như ViewModel. Nó sẽ liên lạc với các phần khác của ứng dụng và cập nhật các LiveData holder trong ViewModel.
  • Thêm một lớp Domain và sử dụng Clean Architecture. Điều này dẫn đến kiến trúc testable và maintainable. Nó cũng tạo điều kiện thoát khỏi main thread một cách nhanh chóng.

✅ Distribute responsibilities, thêm một domain layer nếu cần.

Sử dụng một data repository

Hầu hết các ứng dụng đều có nhiều nguồn dữ liệu, chẳng hạn như:

  • Remote: network hoặc cloud
  • Local: cơ sở dữ liệu hoặc file
  • In-memory cache

Bạn nên có một layer data trong ứng dụng, hoàn toàn không biết đến presentation layer. Các thuật toán để giữ bộ nhớ cache và cơ sở dữ liệu đồng bộ với network không phải là tầm thường. Đề nghị nên có một repository riêng biệt như một điểm nhập cảnh duy nhất liên quan đến sự phức tạp này.

Nếu bạn có nhiều mô hình dữ liệu và rất khác nhau, hãy cân nhắc việc thêm nhiều repositories.

✅ Thêm repositoriesu làm mục nhập đơn vào dữ liệu của bạn.

Dealing với data state

Xem xét trường hợp này: bạn đang quan sát một LiveData được hiển thị bởi một ViewModel có chứa một danh sách các item để hiển thị. Làm cách nào để View khác nhau giữa dữ liệu được tải, lỗi mạng và danh sách trống?

  • Bạn có thể expose một LiveData <MyDataState> từ ViewModel. Ví dụ: MyDataState có thể chứa thông tin về dữ liệu hiện đang tải, đã tải thành công hay không thành công.

Bạn có thể wrap dữ liệu trong một lớp có trạng thái và các metadata khác như thông báo lỗi.

✅ Expose information về trạng thái dữ liệu của bạn bằng cách sử dụng một wrapper hoặc một LiveData khác.

Saving activity state

Activity state là thông tin bạn cần để tạo lại màn hình nếu một activity đã mất, có nghĩa là activity đã bị phá hủy hoặc quá trình này đã bị kill. Rotation là trường hợp rõ ràng nhất và chúng ta đã có được cover bởi ViewModels. State là an toàn nếu nó được giữ trong ViewModel.

Tuy nhiên, bạn có thể cần phải khôi phục lại trạng thái trong các kịch bản khác, nơi ViewModels cũng biến mất: khi hệ điều hành có ít tài nguyên và giết chết quá trình của bạn.

Để tiết kiệm và khôi phục hiệu quả trạng thái giao diện người dùng, sử dụng kết hợp với persistence, onSaveInstanceState () và ViewModels.

Events

Một sự kiện xảy ra một lần. Ví dụ, các sự kiện điều hướng hoặc hiển thị các tin nhắn Snackbar là các hành động chỉ được thực hiện một lần.

Khái niệm Event không phù hợp với cách LiveData lưu trữ và khôi phục dữ liệu. Xem xét một ViewModel với các field sau đây:

LiveData <Chuỗi> snackbarMessage = new MutableLiveData <> ();

Một activity bắt đầu quan sát điều này và ViewModel kết thúc một activity vì vậy nó cần cập nhật thông báo:

snackbarMessage.setValue ("Item saved!");

Activity nhận được giá trị và hiển thị Snackbar. Nó hoạt động, rõ ràng.

Tuy nhiên, nếu người dùng xoay điện thoại, activity mới được tạo ra và bắt đầu quan sát. Khi bắt đầu quan sát LiveData, activity ngay lập tức nhận được giá trị cũ, làm cho thông báo hiển thị lại!

Chúng ta đã mở rộng LiveData và tạo ra một lớp gọi là SingleLiveEvent như là một giải pháp cho điều này. Nó chỉ gửi các cập nhật xảy ra sau khi subscribing Lưu ý rằng nó chỉ hỗ trợ có một observer.

✅ Sử dụng một observable được như SingleLiveEvent cho các sự kiện như điều hướng hoặc các tin nhắn Snackbar.

Leaking ViewModels

Reactive paradigm hoạt động tốt trên Android vì nó cho phép kết nối thuận tiện giữa UI và các lớp còn lại của ứng dụng. LiveData là thành phần chính của cấu trúc này vì vậy các activity và fragment của bạn sẽ thực hiện các sự kiện LiveData.

Làm thế nào ViewModels giao tiếp với các thành phần khác là tùy thuộc vào bạn, nhưng cần xem xet leak và các edge case. Xem xét sơ đồ này, nơi Presentation layer đang sử dụng observe và Data Layer đang sử dụng callback:

Nếu người dùng thoát khỏi ứng dụng, View sẽ biến mất để ViewModel không được quan sát nữa. Nếu repository là một singleton hoặc được gán cho ứng dụng, repository sẽ không bị phá hủy cho đến khi quá trình này bị giết. Điều này sẽ chỉ xảy ra khi hệ thống cần tài nguyên hoặc người dùng tự kill ứng dụng. Nếu repository một tham chiếu đến một callback trong ViewModel, ViewModel sẽ bị tạm thời bị leak.

Leaknày không phải là một vấn đề lớn nếu ViewModel là light hoặc activity được đảm bảo để hoàn thành nhanh chóng. Tuy nhiên, không phải luôn luôn như vậy. Lý tưởng nhất là ViewModels nên được tự do đi bất cứ khi nào không có bất kỳ View nào quan sát nó:

LiveData trong repositories

Để tránh bị leak ViewModels và callback hell, các repository có thể được quan sát như sau:

khi ViewModel được cleared hoặc khi lifecycle của view finished, subscription sẽ được cleared:

Có một sự bắt đầu nếu bạn thử cách tiếp cận này: làm thế nào để bạn đăng ký vào repository từ ViewModel nếu bạn không có quyền truy cập LifecycleOwner? Sử dụng Tranformations là một cách rất thuận tiện để giải quyết vấn đề này. Transformations.switchMap cho phép bạn tạo một LiveData mới phản ứng với sự thay đổi của các thể hiện LiveData khác. Nó cũng cho phép mang thông tin vòng đời của observe qua chuỗi:

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
        if (repoId.isEmpty()) {
            return AbsentLiveData.create();
        }
        return repository.loadRepo(repoId);
    }
);

Trong ví dụ này, khi trigger được cập nhật, function sẽ được áp dụng và kết quả được gửi ngược dòng. Một activity sẽ observe repo và cùng một LifecycleOwner sẽ được sử dụng cho call repository.loadRepo (id).

Extending LiveData

Trường hợp sử dụng phổ biến nhất cho LiveData đang sử dụng MutableLiveData trong ViewModels và expose chúng dưới dạng LiveData để làm cho chúng không thay đổi từ các observe.

Nếu bạn cần nhiều chức năng hơn, việc mở rộng LiveData sẽ cho bạn biết khi có những observer active. Điều này rất hữu ích khi bạn muốn bắt đầu nghe một vị trí hoặc dịch vụ cảm biến, ví dụ.

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // Initialize service
    }

    @Override
    protected void onActive() {
        // Start listening
    }

    @Override
    protected void onInactive() {
        // Stop listening
    }
}

Nguồn: https://medium.com/google-developers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54