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