--- title: 'MVP' disqus: kyleAlien --- MVP === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: [TOC] ## MVP 概述 * 全名為 Model、View、Presenter,**其主要功能是為了減少 `Activity` & `fragment` 的職責,讓其成為單純的 View**,更方便單元測試 * MVP 能有效的降低 View 的複雜性,同時獲得更好的拓展性、測試,保證了系統的整潔性、靈活性,**其重點實現是依賴接口** :::warning 以往會將邏輯 & View 寫在一塊,當業務邏輯發生改變 or UI 重新設計時就必須一同改變,這會導致修改維護要付出的代價過高,最終還是會分離 ::: ### MVP 角色 * MVP 可以讓 View & Model 完全分離(可以從 MVC 改進版看出來 MVP 的身影)。**MVP 並不是一個標準化的格式,只要==保證是透過 Presenter 來分離 View & Model 就是一個正確的方向==,降低負責度並可配合測試** * Activity、Fragment 可以作為 View 來看(因為它們兩個負責 View 的加載)也可以作為 Presenter 來看 | 名稱 | 代表 | 處理 | | -------- | -------- | -------- | | Model 層 | 單純的數據處理、網路操作、以及某些耗時任務的邏輯 | **需要一個 Model 接口讓 Controller 層實做,並讓 Controller 進行搜索、操作** | | View 層 | Android 的 xml、自定義布局、對 View 的操作 | **實作 view,可以新增一個接口給 Presenter 使用** | | **Presenter** | 完成 View & Model 的互交,將資料包裝給 View(處理邏輯後統整給 View) | 接資料封整理完畢後傳給 View、並監聽 Model 接口反應到 View 上 | > ![](https://i.imgur.com/kBbEeXc.png) ## NavigationView Google 團隊所發表的 Android Design Support Library 其中有很多控件,而 NavigationView(側滑菜單) 就是使用 MVP 模式搭建的 > ![](https://i.imgur.com/GIpPO4R.png) 以下為 xml View 布局,一般來說會用 DrawerLayout 包裹住 NavigationView,而 NavigationView 又分為 headerLayout、menu 兩個部份 ```xml= <?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:openDrawer="start"> <!-- 先不關心 include --> <include layout="@layout/app_bar_main" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/nav_header_main" app:menu="@menu/activity_main_drawer" /> </androidx.drawerlayout.widget.DrawerLayout> <!-- 以下是 nav_header_main--> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="@dimen/nav_header_height" android:background="@drawable/side_nav_bar" android:gravity="bottom" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin" android:theme="@style/ThemeOverlay.AppCompat.Dark"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/nav_header_desc" android:paddingTop="@dimen/nav_header_vertical_spacing" app:srcCompat="@mipmap/ic_launcher_round" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/nav_header_vertical_spacing" android:text="@string/nav_header_title" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/nav_header_subtitle" /> </LinearLayout> <!--以下是 activity_main_drawer --> <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:showIn="navigation_view"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_home" android:icon="@drawable/ic_menu_camera" android:title="@string/menu_home" /> <item android:id="@+id/nav_gallery" android:icon="@drawable/ic_menu_gallery" android:title="@string/menu_gallery" /> <item android:id="@+id/nav_slideshow" android:icon="@drawable/ic_menu_slideshow" android:title="@string/menu_slideshow" /> </group> </menu> ``` ### NavigationView 源碼分析 * 分析 NavigationView 的源碼 ```java= public class NavigationView extends ScrimInsetsFrameLayout { ... // Menu 菜單 private final NavigationMenu mMenu; // 菜單 Presenter 層 private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter(); // 菜單加載 private MenuInflater mMenuInflater; ... // 下面的 construct 只列出重點部份 public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // Create the menu mMenu = new NavigationMenu(context); ... mMenu.setCallback(new MenuBuilder.Callback() { @Override public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { return mListener != null && mListener.onNavigationItemSelected(item); } @Override public void onMenuModeChange(MenuBuilder menu) {} }); mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID); // 1. 初始化資源 mPresenter.initForMenu(context, mMenu); mPresenter.setItemIconTintList(itemIconTint); if (textAppearanceSet) { mPresenter.setItemTextAppearance(textAppearance); } mPresenter.setItemTextColor(itemTextColor); mPresenter.setItemBackground(itemBackground); mMenu.addMenuPresenter(mPresenter); // 2. 建構整個菜單視圖,並添加到當前的布局中,其實就是將 RecyclerView 加入布局 addView((View) mPresenter.getMenuView(this)); // 3. 初始化菜單資源 menu 目錄 if (a.hasValue(R.styleable.NavigationView_menu)) { inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0)); } // 4. 初始化 header layout if (a.hasValue(R.styleable.NavigationView_headerLayout)) { inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0)); } a.recycle(); } ... public void inflateMenu(int resId) { // Presenter 暫停加載 mPresenter.setUpdateSuspended(true); // 先加載使用者自定義的 Menu 布局 getMenuInflater().inflate(resId, mMenu); // 開始並更新 mPresenter.setUpdateSuspended(false); mPresenter.updateMenuView(false); } public View inflateHeaderView(@LayoutRes int res) { return mPresenter.inflateHeaderView(res); } } ``` 以下是 NavigationView 建構函數時呼叫 Presenter 的重點函數,其實也就是 MenuPresenter 接口 (列出部份) | Presenter 函數 | 功能 | | -------- | -------- | | initForMenu | 初始化 LayoutInflater、MenuBuilder | | getMenuView | 手動加載布局 NavigationMenuView (Header 視圖) 並設定 NavigationMenuAdapter、也同時加載 HeaderLayout | | updateMenuView | 更新使用者自定義的 Menu 菜單 | | inflateHeaderView | 加載 Header 布局 | * 以下是 NavigationMenuPresenter 的重點程式 ```java= // NavigationMenuPresenter.java public class NavigationMenuPresenter implements MenuPresenter { @Override public void initForMenu(Context context, MenuBuilder menu) { mLayoutInflater = LayoutInflater.from(context); mMenu = menu; Resources res = context.getResources(); mPaddingSeparator = res.getDimensionPixelOffset( R.dimen.design_navigation_separator_vertical_padding); } @Override public MenuView getMenuView(ViewGroup root) { if (mMenuView == null) { // 其實 NavigationMenuView 就是 RecyclerView mMenuView = (NavigationMenuView) mLayoutInflater.inflate( R.layout.design_navigation_menu, root, false); // mAdapter 是 RecyclerView.Adapter<ViewHolder> if (mAdapter == null) { mAdapter = new NavigationMenuAdapter(); } // Header 布局 mHeaderLayout = (LinearLayout) mLayoutInflater .inflate(R.layout.design_navigation_item_header, mMenuView, false); mMenuView.setAdapter(mAdapter); } // 返回 Menu return mMenuView; } @Override public void updateMenuView(boolean cleared) { if (mAdapter != null) { mAdapter.update(); } } public View inflateHeaderView(@LayoutRes int res) { View view = mLayoutInflater.inflate(res, mHeaderLayout, false); addHeaderView(view); return view; } public void addHeaderView(@NonNull View view) { mHeaderLayout.addView(view); // The padding on top should be cleared. 設定 Padding mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom()); } } ``` 配合源碼所畫的 布局-概念圖 如下 > ![](https://i.imgur.com/XMHodVf.png) 下圖是 NavigationView MVP 概念圖,可以發現一個 Presenter 對應多個 View | 層級 | 代表 | | -------- | -------- | | Present | NavigationMenuPresenter | | Model | NavigationMenuAdapter | | View | NavigationMenuView、NavigationView | > ![](https://i.imgur.com/dUg1QIS.png) :::success 可以看到這裡的 MVP 與平常做的 MVP 不同,這裡的 Presenter 並沒有持有 View 的對象,而 View 卻持有 Presenter,但是仍然是正確的,透過 Presenter 層來解偶 View & Model 之間的關係 > **學習模式不是為了生搬硬套,當掌握知識的核心後就應該去靈活應用** ::: * 接下來看看 NavigationMenuPresenter 中的 NavigationMenuAdapter 所加載的布局,onCreateViewHolder 有四種不同的布局(不同的菜單會有不同的 ViewHolder,就像是 HeaderViewHolder 所載入的 View 就不同) ```java= private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> { ... @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_NORMAL: return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener); case VIEW_TYPE_SUBHEADER: return new SubheaderViewHolder(mLayoutInflater, parent); case VIEW_TYPE_SEPARATOR: return new SeparatorViewHolder(mLayoutInflater, parent); case VIEW_TYPE_HEADER: // 與其他 3 個 View 不同 return new HeaderViewHolder(mHeaderLayout); } return null; } private void prepareMenuItems() { if (mUpdateSuspended) { return; } mUpdateSuspended = true; mItems.clear(); mItems.add(new NavigationMenuHeaderItem()); int currentGroupId = -1; int currentGroupStart = 0; boolean currentGroupHasIcon = false; for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) { MenuItemImpl item = mMenu.getVisibleItems().get(i); if (item.isChecked()) { setCheckedItem(item); } if (item.isCheckable()) { item.setExclusiveCheckable(false); } if (item.hasSubMenu()) { SubMenu subMenu = item.getSubMenu(); // 省略... } else { // 省略... mItems.add(textItem); currentGroupId = groupId; } } mUpdateSuspended = false; } } ``` ## Android MVP 該 MVP 與 MVC 最後改進的類型很相似,不過在該範例下 Activity 做為 View 層 ( 我在 MVC 範例時 Activity 作為 Presenter) > ![](https://i.imgur.com/TnE93d3.png) ### 讀取圖片 ```java= public class Model { private Bitmap mBitmap; private Handler mHandler; private Context mContext; private ModelListener mModelListener; public interface ModelListener { void onChange(Bitmap bitmap); } public Model(Context context) { mContext = context; mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.target); mHandler = new Handler(); } public void setModelListener(ModelListener modelListener) { mModelListener = modelListener; } public void LoadPic() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); // 模擬耗時操作 mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.target2); if(mModelListener != null) { // 子線程透過 mHandler 丟訊息至主線程 mHandler.post(new Runnable() { @Override public void run() { mModelListener.onChange(mBitmap); } }); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } public void defaultPic() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); // 模擬耗時操作 mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.target); if(mModelListener != null) { // 子線程透過 mHandler 丟訊息至主線程 mHandler.post(new Runnable() { @Override public void run() { mModelListener.onChange(mBitmap); } }); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } //======================================================================== public class Presenter { private ViewEvent mViewEvent; private Model mModel; public Presenter(ViewEvent viewEvent) { mViewEvent = viewEvent; mModel = new Model(viewEvent.getContext()); mModel.setModelListener(new Model.ModelListener() { @Override public void onChange(Bitmap bitmap) { mViewEvent.showPic(bitmap); } }); } public void loadPic() { mModel.LoadPic(); } public void defaultPic() { mModel.defaultPic(); } } //======================================================================== interface ViewEvent { void showPic(Bitmap bitmap); Context getContext(); } public class MainActivity extends AppCompatActivity implements ViewEvent, View.OnClickListener { private ImageView imageView; private Presenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); presenter = new Presenter(this); findViewById(R.id.load_btn).setOnClickListener(this); findViewById(R.id.clear_btn).setOnClickListener(this); imageView = findViewById(R.id.imageView); } @Override public void showPic(Bitmap bitmap) { imageView.setImageBitmap(bitmap); } @Override public Context getContext() { return this; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.load_btn: presenter.loadPic(); break; case R.id.clear_btn: presenter.defaultPic(); break; } } } ``` **--實作結果--** > ![](https://i.imgur.com/XOM9gWe.png) ## Presenter 內存洩漏 由於 Presenter 會持有 View or 其他的耗時操作(網路邏輯操作),若是 Activity 提早結束則會造成,在 Presenter 持有的內存未即時釋放,導致內存洩漏 ### 生命週期管理 - 泛型 * **透過 Presenter 層與外部使用的 Activity 生命週期同步,當 Activity 結束時同時關閉對 View 的引用,達到 Presenter 可以被正常的 GC 回收** 主要分為以下幾個類 | 層級 | 類 | 介紹 | | -------- | -------- | - | | View | MVPBaseActivity<V, T extends BasePresenter<V\>> | 泛型、抽象類,作為 View 的基類,**==透過泛型直接綁定 Presenter==** | | View | ArticlesActivity | **繼承 View 基類 限制泛型**,並且實做 Presenter 類,並實做 View 的接口 | | Presenter | BasePresenter<T\> | 泛型、抽象類,為了防止內存洩漏,**有關 View 的引用 ==使用弱引用==** | | Presenter | Presenter | 負責接收 Model 的響應事件 (實做 Model 接口),也把 View 的事件傳給 Model | | Model | Model | 會有一個 Model 的響應接口,其他則是單純的資料、耗時操作、網路請求等等 | > ![](https://i.imgur.com/S6SLons.png) :::success 主要是使用 ^1^透過泛型限制 Presenter 引用的生命(跟隨外部 View 的生命週期),^2^透過泛型的關係讓 Presenter & View 產生關係 ::: * 以下把 Activity 作為 View 層 ```java= /** * View 的基類 * @param <V> View 的接口類 * @param <T> Presenter 具體類(非抽象 or 接口 */ public abstract class MVPBaseActivity<V, T extends BasePresenter<V>> extends Activity { protected T mPresenter; @SuppressWarnings("unchecked") @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPresenter = createPresenter(); // 創建 Presenter mPresenter.attachView((V) this); // 讓 Presenter 持有 View } @Override protected void onDestroy() { super.onDestroy(); mPresenter.detachView(); // 讓 Presenter 失去對 View 的持有 } public abstract T createPresenter(); // 獲得具體對象 } //======================================================================== /** * View 接口 */ interface ViewEvent { void OnShow(String str); void OnLoading(); } /** * View 層 */ public class ArticlesActivity extends MVPBaseActivity<ViewEvent, Presenter> implements ViewEvent{ private Presenter presenter = new Presenter(); @Override public void OnShow(String str) { mTxt.setText(str); } @Override public void OnLoading() { mTxt.setText("Loading"); } private TextView mTxt; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.my_mvp); mTxt = findViewById(R.id.txt); Button mBtn = findViewById(R.id.btn); mBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { presenter.requestData(); } }); } @Override public Presenter createPresenter() { return presenter; } } //======================================================================== /** * Presenter 的基類,與 MVPBaseActivity 有關細 * @param <T> 該泛型使用 弱引用,不要產生內存洩漏 */ public abstract class BasePresenter<T> { protected Reference<T> mReference; public void attachView(T view) { mReference = new WeakReference<>(view); } protected T getView() { return mReference.get(); } public boolean isViewAttached() { return mReference != null && mReference.get() != null; } public void detachView() { if(mReference != null) { mReference.clear(); mReference = null; } } } //======================================================================== /** * Presenter 層 */ public class Presenter extends BasePresenter<ViewEvent> implements Model.ModelEvent { private Model mModel = new Model(); public Presenter() { mModel.setModelEvent(this); } public void requestData() { if(isViewAttached()) { getView().OnLoading(); } mModel.load(); } // Model 接口響應 @Override public void onComplete(String str) { if(isViewAttached()) { getView().OnShow(str); } } } //======================================================================== /** * Model 層 */ public class Model { private ModelEvent mModelEvent; public interface ModelEvent { void onComplete(String str); } public void setModelEvent(ModelEvent modelEvent) { mModelEvent = modelEvent; } public void load() { try { Thread.sleep(1000); // 模擬耗時操作 if(mModelEvent != null) { mModelEvent.onComplete("Hello MVP"); } } catch (InterruptedException e) { e.printStackTrace(); } } } ``` **--實做結果--** > ![](https://i.imgur.com/svYoJM2.png) ## Appendix & FAQ :::info ::: ###### tags: `框架模式`