Hướng dẫn sử dụng mô hình MVP với Presenter độc lập với Android Class

Một trong những lý do ra đời của mô hình MVP đó là tăng khả năng Unit test của ứng dụng. Để làm được điều này thì phải tạo Presenter trong MVP độc lập với Android Class như: Context, Broadcast, Bundle… Cùng tìm hiểu cách làm điều này trong bài viết hôm nay nhé!
Table of Contents
1. Mở đầu
Khi sử dụng mô hình MVP, lỗi hay gặp nhất đó là sử dụng Context trong Presenter.
Nhưng tôi bắt buộc phải cần đến Context thì sao?
Tốt nhất là nên hạn chế sử dụng Context trong Presenter. Trước khi trả lời cầu hỏi trên thì bạn cần trả lời câu hỏi: Tại sao bạn lại cần đến Context?
Bạn cần Context để truy xuất các tài nguyên của Android như: Shared Preference, Resource để tương tác với xml… Bạn nên dùng Context trên View thôi.
Còn một vấn đề nữa, đó là việc các View cần phải lưu state (trạng thái của view). Có rất nhiều trường hợp cần phải lưu state như xoay device, thay đổi ngôn ngữ điện thoại…
Với mô hình MVP thì việc khôi phục state tương đối khó khăn, và đặc biệt là khi Presenter cần biết về state của View.
Trên đây chỉ là 2 ví dụ về việc Presenter bị phụ thuộc vào Android Class. Từ đó làm giảm tính dễ Unit Test của MVP. Bài viết này, mình sẽ hướng dẫn các bạn cách tối ưu để Presenter độc lập với Android class.
2. Khởi tạo cấu trúc dự án ban đầu theo MVP
Bài viết này mình sẽ cố gắng giải quyết vấn đề khôi phục State của View bằng cách tạo Presenter trong MVP có thể có State.
Chính vì điều này sẽ giúp bạn không cần truyền state qua Bundle vào Presenter nữa. Những Presenter này tùy ý nhận State từ View. Ngoài ra, chúng còn cung cấp hàm để View có nhận State từ Presenter bất kể lúc nào.
Trong Android, View thường là một Activity, Fragment hoặc một custom View…Tất cả để có cơ chế khôi phục state, ví dụ qua hàm onConfigChange(). Chúng ta sẽ sử dụng cơ chế đó khôi phục state từ Presenter.
Ok, tạm giải thích như vậy thôi. Chúng ta bắt đầu code nhé!
Các dự án sử dụng MVP thường sẽ có cấu trúc như sau:
// BaseView.java
interface BaseView {}
// BaseModel.java
interface BaseModel {}
// BasePresenter.java
interface BasePresenter {
void subscribe(@NonNull V view);
void unsubscribe();
}
Với cấu trúc này, chúng ta sẽ thêm một State Representation và một Presenter có thể nhận và cung cấp State cho View.
// BaseState.java
interface BaseState {}
// BaseStatefulPresenter.java
interface BaseStatefulPresenter extends BasePresenter {
void subscribe(@NonNull V view, @Nullable S state);
@NonNull S getState();
}
Về lý thuyết thì tất cả chỉ có như vậy thôi.
Tuy nhiên, nếu chỉ viết như này thì có lẽ bạn sẽ thấy khó hiểu và không thể ứng dụng vào dự án của mình được. Để hiện thực hóa lý tưởng, chúng ta sẽ cùng nhau xây dựng một ứng dụng demo nhé.
3. Tạo ứng dụng demo Presenter trong MVP độc lập với Android class
Giả sử, mình có một TabLayout để hiển thị các tab, mỗi tab là một màn hình. Và mình muốn khôi phục state của TabLayout khi Activity bị destroy do xoay màn hình. Đặc biệt, mình còn muốn khôi phục cả vị trí tab đã hiển thị trước đó.
3.1. Cài đặt
Đầu tiên, chúng ta định nghĩa một Contract (Interface) đơn giản cho xử lý tab.
interface TabsContract {
interface View extends BaseView {
void setTabItems(List items);
void setTabPosition(int position);
}
interface State extends BaseState {
List getLastTabItems();
int getLastTabPosition();
}
interface Model extends BaseModel {
List provideTabItems();
}
interface Presenter extends BaseStatefulPresenter {
void onTabPositionChange(int position);
}
}
3.2. State
Trong ví dụ này, chúng ta muốn khôi phục tab items và vị trí của tab được selected trước đó.
class TabsState implements TabsContract.State {
private final List tabItems;
private final int tabPosition;
public TabsState(List tabItems, int tabPosition) {
this.tabItems = tabItems;
this.tabPosition = tabPosition;
}
@Override public List getTabItems() {
return tabItems;
}
@Override public int getTabPosition() {
return tabPosition;
}
}
3.3. Stateful Presenter trong MVP
Một Presenter trong MVP có thể nhận state từ View khi nó subcribes. Trong ví dụ này, chúng ta sẽ kiểm tra xem các tab item có được cung cấp bởi State hay không? Nếu không thì sẽ lấy từ model.
Ngoài ra, nếu có vị trí tab được selected cuối cùng được lưu, chúng ta sẽ đưa lên View.
class TabPresenter implements TabsContract.Presenter {
@Nullable private TabsContract.View view;
private TabsContract.Model model = new TabsContract.Model();
@Nullable private List tabItems;
private int lastPosition;
// Subscribe without the state.
@Override void subscribe(@NonNull TabsContract.View view) {
subscribe(view, null);
}
// Subscribe with the provided state.
@Override void subscribe(@NonNull TabsContract.View view, @Nullable TabsContract.State state) {
this.view = view;
// If there are no retrieved items, get them from the model. If there's no
// previously selected position, use 0 as a default one.
final int retrievedPosition;
if (state != null) {
tabItems = state.getLastTabItems();
retrievedPosition = state.getLastTabPosition();
} else {
tabItems = model.getTabItems();
retrievedPosition = 0;
}
// Set the values on the view.
view.setTabItems(tabItems);
view.setTabPosition(retrievedPosition);
}
// Once the state is requested, generate a new immutable state instance.
@Override TabsContract.State getState() {
return new TabsState(tabItems, tabPosition);
}
// Unsubscribe the view from the presenter.
@Override void unsubscribe() {
view = null;
// Clear state variables when unsubscribed. The view is no longer associated with this
// presenter, so the presenter shouldn't keep the track of the state.
tabItems = null;
tabPosition = null;
}
// Called by the view when the tab position changes.
@Override void onTabPositionChange(int position) {
tabPosition = position;
// For example, update the toolbar title once the selected tab changes.
if (tabItems != null && tabItems.get(position) != null) {
view.setToolbarTitle(tabItems.get(position).getTitle());
}
}
}
3.4. View Implementation
Ở tầng View, chúng ta có Activity, trong này chúng ta sẽ tương tác với Presenter trong ba hàm của vòng đời Activity:
onPostCreate()
onSaveInstanceState()
onStop()
Cách làm thông thường là chúng ta sẽ đăng ký với Presenter trong onResume() và hủy đăng ký trong onPause(). Cách làm này có đôi chút vấn đề với Presenter vì onSaveInstanceState() không phải lúc nào cũng được gọi trước onPause(). Tương tự khi hủy đăng ký cũng vậy.
Vì vậy, với cách đăng ký và hủy đăng ký trong 3 hàm của vòng đời Activity như trên là hợp lý nhất.
class TabView extends AppCompatActivity() implements TabsContract.View, ViewPager.OnPageChangeListener {
// ...
@Override public void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate();
// Pass state when subscribing; it can be null.
presenter.subscribe(this, savedInstanceState != null ? readFromBundle(savedInstanceState) : null);
}
@Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// When saving state, retrieve it from the presenter and save to Bundle.
writeToBundle(outState, presenter.getState());
}
@Override public void onStop() {
super.onStop();
// Unsubscribe from the presenter once the activity is stopped.
presenter.unsubscribe();
}
@Override public void setTabItems(List items) {
tabPagerAdapter.setTabItems(items);
}
@Override public void setTabPosition(int position) {
tabLayout.setScrollPosition(position, 0f, true);
tabPager.setCurrentItem(position);
}
// Tab listener notifying the presenter of the change.
@Override public void onPageSelected(int position) {
presenter.onTabPositionChange(position);
}
}
Hai hàm writeToBundle() và readToBundle() chỉ là hỗ trợ để ghi và đọc State từ bundle. Chúng sẽ code của chúng ta nhìn “sạch sẽ” hơn.
Bạn cũng có thể sử dụng Parcelable, tuy nhiên đây là class của Android nên cũng không được khuyến khích trong trường hợp này.
Như vậy là chúng ta đã hoàn thành xong ứng dụng demo rồi đấy. Bạn có thể build ra device và kiểm tra thành quả nhé.
4. Kết luận
Hy vọng qua bài viết này các bạn đã biết cách sử dụng mô hình MVP với Presenter độc lập với Android Class để tăng khả năng Unit test của ứng dụng.
Cảm ơn các bạn đã đọc bài viết.
Chào thân ái và quyết thắng!
Theo vntalking.com