Android Design Pattern : MVP vs RxJava

RxJava và MVP patterm là 2 chủ đề luôn luôn được đề cập đến trong các cuộc nói chuyện của các nhà phát triển Android trong 1 2 năm gần đây. Có thể bạn cũng đã từng nói đến nó trong các cuộc nói chuyện với đồng nghiệp hay bạn bè của mình?

Chúng ta dùng NÓ(RxJava hoặc MVP), chúng ta biết về NÓ. Nhưng thực sự chúng ta đã biết lợi ích mà nó mang lại trong việc phát triển ứng dụng. Các pattern và thư viện của chúng ta đã mang lại đủ giá trị cho khách hàng, tốc độ phát triển ứng dụng của chúng ta đã đạt được năng suất tốt nhất?. Hãy tập trung cùng tôi làm việc hợp nhất Reactive code với MVP cùng 2 khía cạnh :

  • Simplicity of the code (Đơn giản trong việc viết mã code).
  • Fully test our business logic (Test toàn bộ bussiness logic).

Ở bài này tôi sẽ focus vào việc có thể đưa unit test vào toàn bộ trong code bussines dựa vào MVP và RxJava

Tại sao ư? Bởi vì bằng việc tập trung vào việc ở trên, việc lặp lại đi lặp lại mã code trong tương lai của chúng ta sẽ được lược bỏ nhanh hơn nhiều.

Bối cảnh

Việc đầu tiên chung ta nên chia code ra thành 3 hoặc nhiều tầng khác nhau

- Data Layer :

Tầng Layer này cung cấp dữ liệu từ mọi nơi (storage,network, database .v.v.). Ví dụ : Dữ liệu lấy từ API về, vị trí người dùng .v.v.

- Domain Layer :

(Thường thường) Tầng kết nối giữa UI LayerData layer . Ví dụ : Người dùng click vào button X trên UI để lấy location.

UI (hoặc còn gọi Presentation) layer :

Hiển thị nội dung và là tầng Layer tương tác với người dùng. Ví dụ tầng này ẩn hiện button.

Dưới dây là hình ảnh mô tả về pattern này :

Trong kiến trúc MVP này, Data Layer phải có phần trăm số lượng code unit testable lớn. Chúng ta thường loại trừ code unit test tại các class Network hoặc class Storage (Các class này nằm trong nội tại Android SDK). Domain Layer nên có 100% Code Unit testable. Cuối cùng, có một rào cản lớn là có thể đặt code unit testable trong UI Layer. Đôi khi là khó khăn khi là code unit test vẫn đặt ở tại các class View , Activity hay Fragment. Với sự hiểu biết cơ bản về làm thế nào để phân chia code ở tầng higher level, Chúng ta có thể kết luận liên kết giữa MVP và Kiến trúc bên trên là MVP là pattern chỉ sử dụng ở tầng UI Layer Để tránh việc MVP chỉ sử dụng ở tầng UI Layer thì ta có thể có giải pháp là di chuyển logic code từ các View (Activity,Fragment) vào trong Presenter là các class java thuần để có thể sử dụng code unit test dễ dàng hơn.

Và sau đây là RxJava

RxJava là thư viện hỗ trợ việc giao tiếp giữa 3 layer UI Layer , Domain Layer , Data Layer. Với tất cả sự hiểu của tôi. Tôi sẽ tạo 1 cấu trúc lớp UI sử dụng MVP :

1. Presenter

Bước đầu tiên là sẽ tạo các State của các View Hiển thị cho người dùng. Chúng ta sẽ không quan tâm View đó là loại gì (activity, view, etc..). Vì chúng ta sử dụng interface (StateView).

1. Khung MVP chung :

Method tối thiểu mà Presenter sẽ có là onStart()onStop(). StateView có thể update một State(hoặc đôi khi có thể là một "Model"). State là trạng thái mà Views sẽ sử dụng để hiển thị.

Presenter.java 
public interface Presenter<S, V extends StateView<S>> {
  void onStart(V view);C
  void onStop();
}
StateView.java
public interface StateView<S> {
	void updateState(S state);
}

2. Implement ví dụ ở trên:

Thứ nhất, chúng ta bắt đầu với Presenter. Presenter sẽ nhận tất cả các thứ cần thiết từ Rx Observables từ Domain Layer và sau đó update sang View. ở ví dụ này chúng ta sẽ check nếu người dùng đã đăng nhập :

accountInteractor.isUserLoggedIn()

Sau đó, chúng ta chuyển đổi các dữ liệu này và đánh giá xem hiển thị trạng thái nào :

accountInteractor.isUserLoggedIn()
     .map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))

Cuối cùng, chúng ta sẽ update view thông qua hàm onStart()

void onStart(StateView<FilterButtonState> view){
  subscription = accountInteractor.isUserLoggedIn()
      .map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))
      .subscribe(view::updateState)
}

Khi presenter gọi vào hàm onStop(). Chúng ta hủy đăng ký để không bị leak các action hoặc object Full source code class Presenter của tôi

@ViewScope // dagger 2 scope
class FilterButtonPresenter implements Presenter<FilterButtonState, StateView<FilterButtonState>> {

 final AccountInteractor accountInteractor;
 Subscription subscription;

 @Inject // dagger 2 dependency injection
 FilterButtonPresenter(AccountInteractor accountInteractor){
   this.accountInteractor = accountInteractor;
 }

 @Override
 void onStart(StateView<FilterButtonState> view){
   subscription = accountInteractor.isUserLoggedIn()
                   .map(isUserLoggedIn -> new FilterButtonState(isUserLoggedIn))
                   .subscribe(view::updateState);
 }

 @Override
 void onStop(){
   subscription.unsubscribe();
 }
 
 @AllArgsConstructor  // lombok
 static class FilterButtonState {
   final boolean enabled;
 }
}

3.Về việc test :

Nó sẽ kiểm tra 2 trường hợp và trả ra kết quả mong muốn. Điểm hợp lệ cũng phải check với các lỗi của Obserable. Trong trường hợp test này tôi sẽ giả định không mong đợi bất kỳ kết quả nào. do đó sẽ không có kế quả nào trả ra.

@RunWith(MockitoJUnitRunner.class)
class FilterButtonPresenterTest {

 @Mock
 AccountInteractor accountInteractor;
 @Mock
 StateView<FilterButtonState> view;
 @InjectMocks
 FilterButtonPresenter testee;

 @Test
 void userIsLoggedIn(){
   given(accountInteractor.isUserLoggedIn())
     .willReturn(just(true));
   
   // When 
   testee.onStart(view);

   // Then
   verify(view)
     .updateState(new FilterButtonState(true));
 }

 @Test
 void userIsLoggedOut(){
   given(accountInteractor.isUserLoggedIn())
     .willReturn(just(false));
   
   // When 
   testee.onStart(view);

   // Then
   verify(view)
     .updateState(new FilterButtonState(false));
 }

}

2. View

Sau khi xác định cacs state có thể mà View có thể hiển thị trong một trường hợp cụ thể. Chúng ta sẽ cập nhật các UI liên quan thông qua method updateState.

class MyActivity extends Activity implements StateView<FilterButtonState> {

 private Button button;
 @Inject
 FilterButtonPresenter presenter;

 @Override
   protected void onCreate(Bundle savedInstanceState) {
   // set some content views
   Component.activity(this).inject(this); // dagger 2 injection
   presenter.start(this);
 }
 
 @Override
 protected void onDestroy() {
     presenter.stop();
 }

 @Override
 public void updateState(FilterButtonState state) {
   button.setEnabled(state.enabled);
 }
}

Một câu hỏi sẽ xuất hiện tại thời điểm này tôi chắc chắn bạn sẽ hỏi là method onStart()onStop() nên được sử dụng trong hàm nào trong life-cycle của một view. Nếu chúng ta gọi method start/stop presenter trong hàm create/destroy của Activity. Chúng ta không cần phải quan tâm đến State onSavedInstance hoặc split screen mode. Nếu không muốn run Activity khi Activity Stop thì hàm onStart/onStop cũng có thể là một lựa chọn.

3. Giải thích thêm

  • Khi bạn lên kế hoạch phát triển theo mô hình MVP, hãy nghĩ đến các trạng thái của UI trước tiên.
  • Nếu bạn muốn ứng dụng đầy đủ Reactive App, Data Layer <-> Domain layer phải giao tiếp với RxJava giống với cách Domain Layer <-> UI layers đã làm. Nó dễ dàng hơn để đẩy các State từ Data sang UI.
  • Nếu 2 hay nhiều Presenter khác nhau cần biết các state của UI(Ví dụ : Người dùng chọn item từ danh sách và bạn muốn cập nhật các chế độ riêng biệt). thì bạn sẽ đẩy xuống Data Layer. Đối tượng này được chọn từ logic. Nó chỉ đơn giản có thể là đối tượng tồn tại trong bộ nhớ tại trong thời gian chạy.
  • Có những trường hợp chúng ta cần cập nhật Domain Layer hoặc thậm chí là Data Layer dựa trên hành động người dùng (Ví dụ như ở trên, nhưng người dùng click items). Đơn giản chỉ cần thêm method vào trong presenter. Gọi chúng từ View. Nếu bạn cũng cần phải cập nhật Data Layer thì tạo thêm delegate của action đó và dispatch ra ngoài. Ví dụ Người dùng click logout button, view gọi presenter, presenter notifies Bussines Layervà xóa tài khoản đã tồn tại.
  • Các tầng layer sẽ có thêm các delegate nhưng tầng logic sẽ có thể dễ dàng hiểu hơn.

Vì vậy sau bài này. Bạn có thể sử dụng unit test toàn bộ app của bạn. Bạn chia nhỏ code logic và bạn có thể lặp lại code một cách nhanh chóng (cả feature và fix bug).