Một trong những lý do ra đời của mô hình MVP đó là tăng khả năng Unit test ứng dụng. Để làm được điều này thì lớp Presenter trong MVP phải độc lập với các Android Class.
Android class là những thành phần hệ thống như Context, Broadcast, Bundle… Trong lớp Presenter chỉ nên chứa thuần code Java/Kotlin là tốt nhất.
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? (Tham khảo ngay Context là gì)
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 trạng thái (thuật ngữ gọi là state) như xoay device, thay đổi ngôn ngữ điện thoại…
Với mô hình MVP, 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.
Nội dung chính của bài viết
#Khởi tạo cấu trúc dự án ban đầu theo MVP
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ó lưu trữ giá trị 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<V extends BaseView> { 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<V extends BaseView, S extends BaseState> extends BasePresenter<V> { 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é.
💦 Triển khai mô hình MVP: Tạo dự án theo mô hình MVP trong Android với Dagger2
#Tạo ứng dụng minh họa
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 đó.
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<TabItem> items); void setTabPosition(int position); } interface State extends BaseState { List<TabItem> getLastTabItems(); int getLastTabPosition(); } interface Model extends BaseModel { List<TabItem> provideTabItems(); } interface Presenter extends BaseStatefulPresenter<View, State> { void onTabPositionChange(int position); } }
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<TabItem> tabItems; private final int tabPosition; public TabsState(List<TabItem> tabItems, int tabPosition) { this.tabItems = tabItems; this.tabPosition = tabPosition; } @Override public List<TabItem> getTabItems() { return tabItems; } @Override public int getTabPosition() { return tabPosition; } }
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<TabItem> 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()); } } }
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<TabItem> 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é.
💦 Đọc thêm về lập trình Android:
Tks anh bài viết rất hay. Cho em hỏi ngu: Như vậy là presenter-view giống với stateful-stateless component bên react phải không ạ
Hi Đạt,
Mình thấy hai cái này nó không tương đồng lắm bạn à. Khó so sánh lắm