问题的引出
对于一个基本的Android项目,我们在初学的时候的做法通常都是直接在xml中绘制界面,然后在对应的Activity中做一些响应操作。这样做在一些demo演示的时候倒是没什么问题,但是一旦项目稍微有点规模,就会导致Activity变得臃肿。而对于一个成百上千行的Activity,当我们需要对界面进行修改的时候,会很难快速定位到对应修改的地方,并且由于所有的操作都在activity中,耦合度很高,当修改某一处的时候,可能会涉及到多个方面,从而需要修改多处。
解决这个问题的方法就是将在activity中的操作进行分离,同时对于整个项目也进行功能模块的分离。这样的话,当我们需要修改某处的时候,只用修改对应的部分即可。因此就有了mvp模式了,虽然mvp模式也存在很多缺点,巨多的接口,但是对于一个中小型项目,这点缺点还是可以忽略的。
MVP
而MVP模式指的就是Model,View,Presenter这三者。也就是说MVP模式将集中在Activity中的操作拆分成了三个部分,由这三个部分共同完成一个操作。
其中,model是获取数据的部分,它只负责获取数据,而不进行其他操作。View则是界面显示的部分,同样的,它只负责展示界面,不进行其他操作,由Activity/Fragment担任。Presenter是连接model和View的部分,它是主要负责进行逻辑的处理。
因此,一个完整的事件流程理应是这样:在View中产生某个事件,然后由View转交给Presenter,Presenter进行逻辑的处理后通知model获取数据,在Model获取完数据后回调给Presenter,然后presenter将数据进行处理后交给View去显示。
在这个过程中,可以看出View与Model是完全没有联系的,它们之间的数据通过Presenter进行互通。另外,Model获取到的数据也是通过回调接口来提供给Presenter的。这样,就可以将Model独立出来,减少数据的耦合。
所以他们的关系也应当如上图所示,View与Presenter互相持有,可以互相通信。而Model只被Presenter持有,是单向通信的。
小结
对于MVP而言,数据的获取和界面的显示是没有关系的。也就是说,当我们需求进行变化的时候,就可以根据需求而只修改其中的一部分,而不用全部修改。
另外,由于逻辑与界面的显示是分离的,当我们测试的时候就可以不用对Activity进行注释修改添加,而是可以直接新建一个Presenter继承原Presenter,然后进行修改,极大的方便了我们测试。
至于MVP的实现,也不是一成不变的,而是根据自己的习惯或者需求进行一些变动。毕竟MVP模式本身的引出就是为了解耦,分割各个模块的。
实现MVP
对于一个应用程序,我们应当保持其界面的一致性,因此这里先定义了三个接口,分别对应MVP的三个部分的行为准则,这是三个接口中,定义的是每组MVP都应当拥有的功能,也就是说每组MVP都应当实现这些对应的方法。对于MVP,当然应该至少有三个类的实现,分别对应MVP的每个部分,称为一组MVP。
既然说到了这里,就有必要说一下分包的实现了。有些人习惯建立三个包Model、View、Presenter,然后将对应的文件分别放在这三个包中。还有些人习惯按照功能分包,然后将每个功能对应的MVP放在一起。这里我就是采用的后者,按照功能分包,同时加入一个Contract来协同每组MVP的行为。
包结构
这里,添加了一个Module,这个Module是专门用于放置MVP基础的搭建的,叫做common。然后还有一个Module,叫做app,是具体的示例程序对应的Module。
具体实现
首先是View。对于View而言,这里只添加了两个方法,进度条的显示和隐藏。当进行某项操作的时候,应当显示进度条,然后操作结束的时候隐藏,这样才能使得应用更加友好。
public interface View {
/**
* 显示进度条,用于提示正在加载
*/
void showProgress();
/**
* 隐藏进度条
*/
void hideProgress();
}
接下来是model的接口,由于这里model只是负责获取数据的部分,它是独立的。因而没有什么公共的方法,但是为了后面的model的统一,这里还是创建了一个model接口,只是其中并没有方法而已。
public interface Model {
}
最后就差Presenter了,而Presenter是model和view的连接者,它是需要同时持有model和view。但是对于View而言,它的生命周期很是复杂,我们需要当view创建的时候连接二者,而view销毁的时候解开连接。否则的话当view销毁,但是还被Presenter持有的时候,view就不会被回收,从而造成内存的泄露。那么presenter就应当拥有解除关联和判断是否还在关联的方法。
public interface Presenter {
/**
* 解除关联
*/
void detach();
/**
* 判断View是否与当前的Presenter建立连接
*
* @return true为已建立连接
*/
boolean isAttach();
/**
* 检查是否与View建立连接
*/
void checkAttach();
}
对于每个model,view,presenter,都应当实现以上的接口。而对于这种共有的方法,每个对应的类都去实现一遍显然是不合适的,最好的做法是使用一个基类来实现这些方法,其余的类只要继承这个基类即可拥有这些方法了。同时也会因为这些方法都是在基类中实现的,从而会使得每个类的行为表现得具有一致性。
要知道的是,上面只是定义了三个接口,只是代表每组MVP的行为而已。实际上还并没有实现MVP的搭建,那么下面,将会通过Base基类搭建一个MVP模式基础。
Model
public class BaseModel implements Model {
}
因为model没有共有的方法,所以它的基类也是一个空的类。可能从这里看的话会显得多余,毕竟只是一个空的类,那么就完全没必要去创建它。而实际上也的确没必要去创建这个BaseModel,毕竟接口Model本身就是为了实现model的一致性了。而之所以这里又重新创建了这个Base类,是因为后面将会基于该mvp框架搭建RxJava+Retrofit实现的MVP,而引入BaseModel的话,不仅能够使得两种MVP的使用具有一致性,同时也可以在BaseModel中添加model可能需要的共同的方法。
那么接下来看Presenter的实现。
Presenter
public abstract class BasePresenter<V extends View, M extends Model> implements Presenter {
protected V mView;
protected M mModel;
public BasePresenter(V view) {
mView = view;
mModel = createModel();
}
@Override
public void detach() {
mView = null;
}
@Override
public boolean isAttach() {
return mView != null;
}
@Override
public void checkAttach() {
if (!isAttach()){
throw new RuntimeException("当前Presenter未与View建立连接");
}
}
/**
* 创建Presenter对应的Model
*
* @return Model
*/
protected abstract M createModel();
}
从上面可以看到,这里presenter实现了presenter接口的共有方法,同时还定义了两个变量,mView 和mModel ,也就是mvp中的另外两者。并且只提供了一个构造方法,在这个构造方法中完成和View的绑定。这里就可以知道,我们没在presenter的接口中定义绑定的方法,是因为将绑定的过程放到了构造方法,也就是说当Presenter存在的时候就需要绑定View。
另外,presenter中并没有直接引用model,而是提供了一个抽象方法来生成model。因为对于一个presenter而言,model并不是必须存在的,只有当需要获取数据的时候才会需要model,因此这里将model的创建放在了一个抽象方法中,当我们不需要的时候直接返回null即可。
接下来是View的基类了,因为view是由Activity和Fragment担任的,所以这里将会分成两个BaseView。
View
public abstract class BaseActivity<P extends Presenter> extends AppCompatActivity implements View {
protected P mPresenter;
protected Context mContext;
private Dialog mProgressDialog;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getContentView());
mPresenter = createPresenter();
initView();
mContext = this;
mProgressDialog = DialogUtils.getDefaultDialog(mContext);
}
@Override
protected void onDestroy() {
mPresenter.detach();
super.onDestroy();
}
@Override
public void showProgress() {
if (mProgressDialog != null && !mProgressDialog.isShowing()) {
mProgressDialog.show();
}
}
@Override
public void hideProgress() {
if (mProgressDialog.isShowing() && mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
/**
* 返回Content布局id
*
* @return 布局文件id
*/
protected abstract @LayoutRes int getContentView();
/**
* View的初始化工作
*/
protected abstract void initView();
/**
* 创建Presenter
*
* @return 与Activity关联的Presenter
*/
protected abstract P createPresenter();
}
上面是使用Activity作为View而实现的基础View。在其中的onCreate中创建presenter和进度条,并且在onDestroy中解除了和Presenter的绑定。另外除了实现基本的显示隐藏进度条外,还抽象出了三个方法。分别是contentView的布局xml的ID和初始化的方法以及创建Presenter的方法。这样的话,当具体的View继承该基类的时候只需要实现这三个方法即可,而不用去考虑其生命周期的变化了。
此外,为了对进度条的统一管理,将生成进度条的方法放在了DialogUtils中,这样当需要修改进度条的样式的时候将会更加方便。
public class DialogUtils {
/**
* 用于生成默认的正在加载的进度条,该进度条用于普通加载时显示
*
* @return 对话框
*/
public static Dialog getDefaultDialog(Context context) {
Dialog dialog = new ProgressDialog(context);
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}
对于Fragment担任的View与Activity也是一样,唯一的区别就是他们生命周期的不同,从而在不同的方法中进行Presenter的绑定和解除。
public abstract class BaseFragment<P extends Presenter> extends Fragment implements com.pgaofeng.common.mvp.View {
protected P mPresenter;
private Dialog mDialog;
protected Context mContext;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getContentView(), container, false);
initView(view);
mPresenter = createPresenter();
mContext = getContext();
mDialog = DialogUtils.getDefaultDialog(mContext);
return view;
}
@Override
public void onDestroyView() {
mPresenter.detach();
super.onDestroyView();
}
@Override
public void showProgress() {
if (mDialog != null && !mDialog.isShowing()) {
mDialog.show();
}
}
@Override
public void hideProgress() {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
/**
* 获取content布局id
*
* @return 布局id
*/
protected abstract @LayoutRes int getContentView();
/**
* 初始化View
*
* @param view contentView
*/
protected abstract void initView(View view);
/**
* 创建View对应的Presenter
*
* @return Presenter
*/
protected abstract P createPresenter();
}
至此,对于基础MVP的封装已经完成。也就是说,MVP中的三者已经通过这几个Base类连接起来了,因此我们的封装到这里已经算是完成了。只是完成了MVP的封装又怎么能满足我们的要求呢,接下来当然是要具体的通过实际案例来体验一下封装的MVP如何使用了。
案例(使用方式)
下面将通过一个小的案例来演示如何使用MVP。这里的案例就是通过点击一个按钮实现获取数据,然后由textView展示。
先看一下xml的布局。
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".main.view.MainActivity">
<TextView
android:id="@+id/tv_myText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/btn_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="更新" />
</LinearLayout>
对于一个功能,MVP的各个部分都应当有相应的具体的行为,而行为应当是使用接口来提取的,但是若每组MVP都提出三个接口的话,会使得文件更加的多了。因此,这里在使用MVP的时候,使用了Contract来合并了每组的mvp接口。
Contract
public interface MainContract {
interface View {
/**
* 更新TextView的text
*
* @param text 内容
*/
void updateText(String text);
/**
* 显示Toast
*
* @param message 消息内容
*/
void showToast(String message);
}
interface Presenter {
/**
* 更新TextView的内容
*/
void updateTextViewText();
}
interface Model {
/**
* 获取TextView 的内容
*
* @param param 请求参数
* @param callBack 请求回调
* @param handler 用于更新主线程UI
*/
void getTextString(String param, ModelCallBack callBack, Handler handler);
}
}
从上面可以看到的是,Contract接口组合了每一组MVP的所有接口,这些接口都应当是它们的行为准则。在Modle中,获取数据的方法中传入了一个回调参数,这个参数是在Model获取到数据后回调的,由于Presenter和Model是单向通信,因此只能使用回调的方式来将结果传回Presenter。
View
public class MainActivity extends BaseActivity<MainPresenter> implements MainContract.View {
TextView mTextView;
Button mButton;
@Override
protected int getContentView() {
return R.layout.activity_main;
}
@Override
protected void initView() {
mTextView = findViewById(R.id.tv_myText);
mButton = findViewById(R.id.btn_update);
mButton.setOnClickListener(v -> {
mPresenter.updateTextViewText();
});
}
@Override
protected MainPresenter createPresenter() {
return new MainPresenter(this,new Handler());
}
@Override
public void updateText(String text) {
mTextView.setText(text);
}
@Override
public void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
MainActivity实现了View的接口,同时继承的是前面封装的BaseActivity。可以看到的是,Activity已经很简洁了,它其中的一些方法除了必要的一些外就只剩下操作界面UI的了,这与我们的期望是一致的,View只负责显示UI即可。
另外,引入的泛型是MainPresenter,也就是说该View持有的Presenter就是MainPresenter。这样的话,就可以根据用户的需求去主动地调用Presenter的方法了,这里是通过按钮的点击来触发的更新text操作。
Presenter
public class MainPresenter extends BasePresenter<MainActivity, MainModel> implements MainContract.Presenter {
public MainPresenter(MainActivity view, Handler handler) {
super(view);
this.mHandler = handler;
}
private String[] params = {"success", "fail"};
private Handler mHandler;
private Random random = new Random(System.currentTimeMillis());
@Override
protected MainModel createModel() {
return new MainModel();
}
@Override
public void updateTextViewText() {
mView.showProgress();
// 模拟获取数据的几种状态,这里通过参数来决定是否获取数据成功
int index = random.nextInt(2);
mModel.getTextString(params[index], new ModelCallBack() {
@Override
public void success(BaseData<?> baseData) {
checkAttach();
String s = (String) baseData.getData();
if (!TextUtils.isEmpty(s)) {
mView.updateText(s);
}
mView.hideProgress();
mView.showToast(baseData.getMessage());
}
@Override
public void fail(Throwable throwable) {
checkAttach();
mView.showToast("获取数据失败!");
mView.hideProgress();
}
}, mHandler);
}
}
而MainPresenter中只实现了一个方法,也就是Contract中的presenter中定义的方法。这里模拟的是获取数据,但是由于我们并没有真实的数据可以获取,这里就使用随机来模拟了两种情况,成功和失败这两种情况。
同时在构造方法中传入了一个Handler,之所以使用handler,是因为通常获取数据是一个耗时的操作,因此应该在子线程中进行。而界面的操作则必须在UI线程中进行,因此这里使用Handler来操作UI。而从上面可以看出在Presenter中还是调用了Model的获取数据的,其中由于获取数据可能是个耗时,因此Model的方法参数传入的除了参数外还有一个回调和Handler,然后在Model获取完数据后将会调用这个方法将结果传回给Presenter。
而之所以把Handler也传递给了Model,是因为这里分配职责的时候,把线程的切换交给了Model来处理。也就是说,Model如何获取数据以及在什么线程获取都是由它自己决定,而Presenter只需要它在UI线程中将结果回调回来即可。
CallBack
public interface ModelCallBack {
/**
* 请求成功的回调
*
* @param baseData 返回值的基础bean
*/
void success(BaseData<?> baseData);
/**
* 请求失败的回调
*
* @param throwable 错误异常信息
*/
void fail(Throwable throwable);
}
这里定义回调接口的时候,已经限定了获取成功的回调参数。也就是说,Model必须要将数据转换成我们所需的格式再回调回来。
BaseData
public class BaseData<T> {
/**
* 返回码
*/
private int code;
/**
* 返回提示消息
*/
private String message;
/**
* 返回的实际数据内容
*/
private T data;
...
// getter,setter
}
这里可以看到,BaseData封装了三个参数,code,Message和Data。其中code和message是对于此次获取的数据的一个描述,其中code应当是与后台协议后的返回码,而message则是对于code的一些描述消息。data使用的是泛型,是真正存储数据的地方。
那么继续看Model。
Model
public class MainModel extends BaseModel implements MainContract.Model {
@Override
public void getTextString(final String param, final ModelCallBack callBack, Handler handler) {
if (callBack == null) {
throw new RuntimeException("回调不应为空");
}
new Thread(() -> {
try {
// 模拟在子线程中获取数据
Thread.sleep(2000);
// 获取数据结束结束后切换主线程回调
handler.post(() -> {
BaseData<String> baseData = new BaseData<>();
switch (param) {
case "success":
baseData.setCode(0);
baseData.setMessage("获取成功");
baseData.setData("我是获取到的消息内容");
callBack.success(baseData);
break;
case "fail":
callBack.fail(new RuntimeException("人为异常!"));
break;
default:
break;
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
Model只有一个方法,就是获取数据的那个方法。其中开辟了一个子线程,然后在线程中sleep了2秒作为获取数据的耗时。然后通过handler在UI线程把数据传递给了Presenter,最后Presenter会通知View去展示这个数据。
总结
MVP模式主要的作用就是解耦,完全是为了降低耦合度发展而来的一种编码模式。将界面的展示和数据的获取进行分离,会使得整个项目的结构更加清晰。这样不仅能在需求修改的时候及时定位到目标,还能更加方便得进行功能模块测试。而这样的分包分模块更是方便团队协作,以及新人对项目的接手难度。
- 附: 代码上传github,点击查看代码
- 对本文的改进:基于RxJava+Retrofit的MVP模式