---
# System prepended metadata

title: MVP
tags: [框架模式]

---

---
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: `框架模式`
