1. 摘要
MVP(Model-View-Presenter)模式通过Presenter解决了Activity、Fragment等在MVC(Model-View-Controller)中太臃肿的问题。
Google给出了开源代码,向大家阐述了他们对于MVP模式的理解和建议:
https://github.com/googlesamples/android-architecture。其中,todo-mvp分支则是最单纯(未使用其他任何框架)的MVP实现:
https://github.com/googlesamples/android-architecture/tree/todo-mvp/。
本文基于对todo-mvp分支的理解,以更加简单的代码,阐述了作者眼中的MVP模式。
2. MVP浅析
MVP由Model、View、Presenter组成。View最容易理解,即为Android中的各种widget控件。Presenter是为了让View和Model隔离而存在的。
Model并不仅仅是简单的bean,还包括了bean的一些业务逻辑,比如从服务器获取数据等。
3. Simplest MVP
本项目基于google的
todo-mvp,加上笔者的理解,写了一个查询天气情况的Android Demo。实现的功能非常简单:单击Button后,模拟网络查询当前温度,并使用Toast显示出来。如果不使用MVP,这个功能十几行代码即可实现。采用这个简单例子,旨在让初学者可以抓住MVP的本质。
代码可在:
https://github.com/afunx/SimplestMVP下载。项目结构如图1所示:
图1 SimplestMVP项目结构
其中,BaseView和BasePresenter均为interface。但也可以用abstract class。
BasePresenter在这里仅仅是一个接口,不包含其他方法。BasePresenter.java:
package com.afunx.mvp;
public interface BasePresenter {
}
BaseView中有一个setPresenter方法,通过这种方式,把View注入到Presenter中。BaseView.java:
package com.afunx.mvp;
public interface BaseView<T> {
void setPresenter(T presetner);
}
MainContract接口定义了MainContract.View接口和MainContract.Presenter。MainContract.java:
package com.afunx.mvp.main;
import com.afunx.mvp.BasePresenter;
import com.afunx.mvp.BaseView;
import com.afunx.mvp.main.bean.TemperatureBean;
/**
* This specifies the contract between the view and the presenter(View和Presenter的协议)
*/
public interface MainContract {
interface View extends BaseView<Presenter> {
/**
* show present temperature(显示当前温度)
*
* @param temperatureBean temperature bean(temperature bean对象)
*/
void showTemperature(TemperatureBean temperatureBean);
}
interface Presenter extends BasePresenter {
/**
* query present temperature from Internet and show it(从网络上查询当前温度,并显示它)
*/
void showPresentTemperature();
}
}
TemperatureBean就是一个简单的记录温度的bean。TemperatureBean.java:
package com.afunx.mvp.main.bean;
/**
* TemperatureBean restore temperature in degree centigrade(TemperatureBean存储了气温的摄氏度)
*/
public class TemperatureBean {
private int degree;
public int getDegree() {
return degree;
}
public void setDegree(int degree) {
this.degree = degree;
}
}
MainModel就是MVP中的Model,在复杂的项目中,MainModel也可以指某个模块。MainModel.java:
package com.afunx.mvp.main.model;
import java.util.Random;
import com.afunx.mvp.main.bean.TemperatureBean;
import com.afunx.mvp.main.presenter.MainPresenter;
/**
* Main Model represents model used by the Presenter ({@link MainPresenter})
* ,simulate query present temperature from the Internet
*/
public class MainModel {
/**
* simulate query temperature from the Internet
*
* @return TemperatureBean
*/
public TemperatureBean queryTemperature() {
// simulate query from Server
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// degree is in random [20,30)
int degree = 20 + new Random().nextInt(10);
TemperatureBean temperatureBean = new TemperatureBean();
temperatureBean.setDegree(degree);
return temperatureBean;
}
}
MainPresenter实现了MainContract.Presenter接口,并在其构造器中调用mainView.setPresenter方法,把View注入到Presenter中。MainPresenter.java:
package com.afunx.mvp.main.presenter;
import android.os.Handler;
import android.support.annotation.NonNull;
import com.afunx.mvp.main.MainContract;
import com.afunx.mvp.main.bean.TemperatureBean;
import com.afunx.mvp.main.model.MainModel;
import com.afunx.mvp.main.view.MainActivity;
/**
* Listens to user actions from the UI ({@link MainActivity}), receives the data and updates the UI
* (监听用户在UI上的动作,接收温度变化,并更新UI)
*/
public class MainPresenter implements MainContract.Presenter {
@NonNull
private final MainContract.View mMainView;
private final MainModel mMainModel;
public MainPresenter(@NonNull MainContract.View mainView) {
mMainView = mainView;
mMainModel = new MainModel();
mainView.setPresenter(this);
}
@Override
public void showPresentTemperature() {
// mainHandler is created in UI thread(mainHandler在UI线程中创建)
final Handler mainHandler = new Handler();
new Thread(){
@Override
public void run() {
// query temperature in non-UI thread(在非UI线程中,查询温度)
final TemperatureBean temperatureBean = mMainModel.queryTemperature();
mainHandler.post(new Runnable() {
@Override
public void run() {
// update View in UI thread(在UI线程中更新View)
mMainView.showTemperature(temperatureBean);
}
});
}
}.start();
}
}
MainActivity实现了MainContract.View接口。MainContract.View继承自BaseView<T>接口,故MainActivity实现了setPresenter方法。MainActivity在onCreate回调中,以new MainPresenter(this)的方式实现了View和Presenter的互相注入。对于任何一个新接触MVP的人来说,都有一点绕。多看一下
加粗红色部分解释和对应代码,慢慢就能理解了。MainActivity.java:
package com.afunx.mvp.main.view;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.afunx.mvp.R;
import com.afunx.mvp.main.MainContract;
import com.afunx.mvp.main.bean.TemperatureBean;
import com.afunx.mvp.main.presenter.MainPresenter;
/**
* UI for ({@link MainPresenter})
* MainPresenter对应的UI
*/
public class MainActivity extends AppCompatActivity implements MainContract.View, View.OnClickListener {
private Button mBtnShowTemperature;
private MainContract.Presenter mMainPresenter;
@Override
public void setPresenter(MainContract.Presenter presetner) {
mMainPresenter = presetner;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MainPresenter(this);
mBtnShowTemperature = (Button) findViewById(R.id.btn_show_temperature);
mBtnShowTemperature.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v == mBtnShowTemperature) {
mMainPresenter.showPresentTemperature();
}
}
@Override
public void showTemperature(TemperatureBean temperatureBean) {
int celsius = temperatureBean.getDegree();
String format = getString(R.string.present_temperature_text);
String text = String.format(format, celsius);
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
}
MainActivity对应的布局文件activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.afunx.mvp.main.view.MainActivity">
<Button
android:id="@+id/btn_show_temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/show_temperature"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
最终效果如图2:
图2 SimplestMVP效果gif