Android Design Pattern : MVP vs RxJava
Bài đăng này đã không được cập nhật trong 3 năm
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 Layer
và Data 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()
và 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()
và 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áchDomain 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ậtData 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 notifiesBussines Layer
và 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).
All rights reserved