MVP是从MVC架构演变而来的,目的是为了使得程序开发高内聚低耦合,易于扩展,方便维护。
MVC中的M指的是model模型, V 指的是View视图,C指的是Controller。 MVC 大体的思想是,控制器控制着模型与视图的同步。当模型发生了变动,通知控制器然后控制器通知视图同步。比如我们浏览一个动态网页,以JSP为例: 用户请求路径--> 得到一个视图V,在视图上修改某个javaBean,即修改模型,此时它会交给servlet控制器C,然后控制器通过后台的业务逻辑代码完成模型M的同步。 修改完模型后,需要回到servlet控制器C中,判断是否需要将模型封装到视图解析器中,通过jstl标签编译成网页视图V返回给用户。 总之,它的特点就是单向通信。 V->C->M; M->C->V。
MVP中,M指的是model模型,V指的是View视图,P指的是Presenter(中介者)。 它的面向对象的思想更强。 具体的理解我将结合自己的练习来进行学习理解。
还有一种16年左右红起来的MVVM的架构,现在主流的三大前端框架,angular,react,vue就是基于这个。 其中VM指的是ViewModel,视图模型。 它的最强大的一个地方是将同步的任务交给代码完成,删减了很多的冗余代码,提高开发效率。 貌似叫做响应式开发。 angular的双向绑定,确是一种非常强大理想的特性。
我的想法出发点是这样的:
第一,每个Activity中需要写的内容分为三部分, 一部分是布局文件xml,一部分是针对布局文件的业务逻辑代码,另一部分是二者的同步! 要完成这三个基本的逻辑功能,通常我们需要写很多的代码在activity中。 这样会带来一些问题,如,逻辑不清晰,维护困难,看到代码颓丧消极。 因为,稍微一个有些功能的界面,都至少是一百好几行代码。 当这些代码乘以写的页面数,改起来要老命了!
第二,每一个同步的代码需要写好几步: 第一要确定每个视图id与所对应的成员变量名,那么这里就存在一个命名的问题(而,实际上这个命名除了起个标志位的作用,没其他任何作用,我们要还要尽量做到见名知意)。 第二,要写很多无聊的findById进行绑定。第三,为了完成单向取值,通常需要一个一个从视图组件调用相应的方法取得自己想要的属性。 冗余的代码量很多。 体验很差。 通过开源注入框架ButterKnife可以很大程度上解决这个问题。
第三,虽说使用了注入框架,代码看起来清爽了很多。 可是为了完成视图与模型的同步,我们还是不得不定义相关的成员变量进行绑定。 有没有一种方法,可以完成类似于angular的双向绑定呢?
基于MVP的抽象层设计:
View顶层抽象:
package com.automannn.meimeijiong.activity.view.api.baseApi;
public interface IView {
}
Model 顶层抽象:
package com.automannn.meimeijiong.model.api.base;
public interface IModel {
}
Presenter 顶层抽象:
package com.automannn.meimeijiong.presenter.api;
import com.automannn.meimeijiong.model.api.base.IModel;
import com.automannn.meimeijiong.activity.view.api.baseApi.IView;
public abstract class BasePresenter<M extends IModel,V extends IView>{
protected M currModel;
protected V currView;
public BasePresenter(V view){
this.currView = view;
}
public abstract void init();
}
在这里可以看到,每个中介者持有模型与视图的引用。 它的目的是为了更好的发挥他协调者的作用。
View的第二层抽象:
package com.automannn.meimeijiong.activity.api;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.automannn.meimeijiong.presenter.api.BasePresenter;
public abstract class BaseActivity<T extends BasePresenter> extends AppCompatActivity {
protected T currentPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayout());
initView();
currentPresenter = initPresent();
onPrepare();
}
protected abstract void onPrepare();
protected abstract T initPresent();
protected abstract int getLayout();
protected abstract void initView();
}
Presenter的第二层抽象:
使用示例:
package com.automannn.meimeijiong.presenter;
import com.automannn.meimeijiong.model.ChatModel;
import com.automannn.meimeijiong.presenter.api.BasePresenter;
public class ChatPresenter extends BasePresenter<ChatModel,ChatActivity> {
public ChatPresenter(ChatActivity view) {
super(view);
currModel = new ChatModel();
}
@Override
public void init() {
//todo,do something
}
}
从这里可以看到,V与P二者是双向持有对方的关联的。 这就难免导致一些逻辑上的问题(比如到底以presenter为主还是以activity为主,方法的调用的逻辑应该怎么布局?)。我是这样解决的:
整个app入口在manifest文件,而我们直接交互的界面在activity中。 所以说activity可以近似看成人机接口。 一个activity发生变化应该有三种方式: 第一,activity本身具有自身的生命周期方法; 第二,activity作为视图的载体,具有反馈事件回调的功能;第三,通过looper机制,结合hander可以完成一些其它的功能。hander的方式由于出现了很多的开源框架,因此不推荐使用,但是理解它是很有必要的。 因此,在整个布局的时候,将只存在两部分,以生命周期线索组织起来的代码逻辑;和 以事件回调线索组织起来的代码逻辑。
也就是说,每一个功能性方法的入口都在activity,(视图)中,但是功能的实现逻辑则分布在presenter,P中。 因为它是资源的持有者,即持有V,也持有M。
以上讨论的是解决V与P的关系。 但是我们的根本目的是为了解决M与V的关系。 为了完成二者同步,我们可以很自然的想到基于视图适配器的ListView,ListAdapter。 因为它可以不用频繁切换场景,直接定义好视图绑定规则,即可完成很多的操作。 但是针对单页的适配(或者说普通的视图适配)并没有相关解决方案,但是通过类比适配器的源码,我们可以抽象出一个视图持有者,基于它来完成类似适配器的功能。
ViewHoler抽象:
package com.automannn.meimeijiong.adapter;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class ViewHolder {
//代表视图 dto的根部局
private View currView;
public ViewHolder(View view){
this.currView = view;
currView.setTag(this);
}
public <T extends View> T getView(int id){
T t = (T) currView.findViewById(id);
return t;
}
public View getRootView() {
return currView;
}
/**
* 设置文字
*/
public ViewHolder setText(int id, CharSequence text) {
View view = getView(id);
if (view instanceof TextView) {
((TextView) view).setText(text);
}
return this;
}
/**
* 设置图片
*/
public ViewHolder setImageResource(int id, int drawableRes) {
View view = getView(id);
if (view instanceof ImageView) {
((ImageView) view).setImageResource(drawableRes);
} else {
view.setBackgroundResource(drawableRes);
}
return this;
}
public ViewHolder setImageResourceFromBitmap(int id, Bitmap bitmap) {
View view = getView(id);
if (view instanceof ImageView) {
((ImageView) view).setImageBitmap(bitmap);
} else {
Drawable drawable = new BitmapDrawable(bitmap);
view.setBackground(drawable);
}
return this;
}
/**
* 设置可见
*/
public ViewHolder setVisibility(int id, int visible) {
getView(id).setVisibility(visible);
return this;
}
/**
* 设置标签
*/
public ViewHolder setTag(int id, Object obj) {
getView(id).setTag(obj);
return this;
}
public void invalidate(){
this.currView.invalidate();
}
}
它的来源是,将当前Activity的整体充当一个View的根部局,然后建立一个引用,与之相关的所有的同步操作均由此类完成。通过该抽象我们可以完成类似双向绑定的功能,只不是不是响应式的,需要我们手动控制。 这样就会减少很多因视图同步所需要的写的冗余代码。
下面举个完整例子展示:
布局:
<?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=".MainActivity">
<EditText
android:id="@+id/user_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="昵称"/>
<EditText
android:id="@+id/user_realname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="真实姓名"/>
<EditText
android:id="@+id/user_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="年龄"/>
<EditText
android:id="@+id/user_sex"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="性别"/>
<EditText
android:id="@+id/user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="邮箱"/>
<Button
android:id="@+id/user_submit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="提交"/>
</LinearLayout>
activity视图:
package automannn.com.testmvp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import automannn.com.testmvp.presenter.UserPresenter;
import automannn.com.testmvp.view.api.ISingleModelView;
import automannn.com.testmvp.view.api.IView;
public class MainActivity extends BaseActivity<UserPresenter> implements ISingleModelView {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onPrepare() {
currentPresenter.init();
}
@Override
protected UserPresenter initPresent() {
return new UserPresenter(this);
}
@Override
protected int getLayout() {
return R.layout.activity_main;
}
@Override
protected void initView() {
button =findViewById(R.id.user_submit);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
currentPresenter.submit();
}
});
}
@Override
public View getRootView() {
return getWindow().getDecorView();
}
public void showToast(String msg){
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
}
}
model:
package automannn.com.testmvp.model;
import android.view.View;
import android.widget.Adapter;
import android.widget.TextView;
import automannn.com.testmvp.R;
import automannn.com.testmvp.adapter.ViewHolder;
import automannn.com.testmvp.entity.User;
import automannn.com.testmvp.model.api.ISingleModel;
public class UserModel implements ISingleModel<User> {
private User data;
private ModelViewHolder modelViewHolder;
@Override
public User getdata() {
return data;
}
@Override
public Adapter getAdapter() {
return null;
}
@Override
public void init() {
}
@Override
public void setCurrentData(User o) {
data =o;
}
@Override
public void myNotifyDataSetChanged() {
modelViewHolder.getViewHolder().invalidate();
}
@Override
public void initModelViewHolder(View rootView) {
modelViewHolder = new ModelViewHolder(rootView);
}
@Override
public void M2VbindModelViewHolder() {
ViewHolder viewHolder = modelViewHolder.getViewHolder();
viewHolder.setText(R.id.user_nickname,data.getNickName());
viewHolder.setText(R.id.user_realname,data.getRealName());
viewHolder.setText(R.id.user_sex,data.getSex());
viewHolder.setText(R.id.user_age,data.getAge()==null?"":data.getAge()+"");
viewHolder.setText(R.id.user_email,data.getEmail());
}
@Override
public void V2MbindModelViewHolder() {
ViewHolder viewHolder = modelViewHolder.getViewHolder();
data.setNickName(((TextView)viewHolder.getView(R.id.user_nickname)).getText().toString());
data.setRealName(((TextView)viewHolder.getView(R.id.user_realname)).getText().toString());
data.setSex(((TextView)viewHolder.getView(R.id.user_sex)).getText().toString());
data.setAge(Integer.valueOf(((TextView)viewHolder.getView(R.id.user_age)).getText().toString()));
data.setEmail(((TextView)viewHolder.getView(R.id.user_email)).getText().toString());
}
}
presenter:
package automannn.com.testmvp.presenter;
import automannn.com.testmvp.MainActivity;
import automannn.com.testmvp.entity.User;
import automannn.com.testmvp.model.UserModel;
public class UserPresenter extends BasePresenter<UserModel,MainActivity> {
public UserPresenter(MainActivity view) {
super(view);
currModel= new UserModel();
}
//生命周期的核心业务逻辑在此处书完成
@Override
public void init() {
//设置数据源(注意,该数据源有可能来自任意的位置)
currModel.setCurrentData(new User());
//初始化
currModel.initModelViewHolder(currView.getRootView());
}
public void submit() {
currModel.V2MbindModelViewHolder();
currView.showToast(currModel.getdata().toString());
//新建一个模型,模拟用于同步视图
currModel.setCurrentData(new User());
currModel.M2VbindModelViewHolder();
}
}
------------------------------------------------
在单页的状态下,model充当了视图模型的作用,可以类似完成双向绑定的效果,所有的操作可以面向数据对象,而不是面向视图组件。 在一定程度上减少了相当的冗余代码。 同时,对于后期的修改升级与维护,逻辑还算清晰。
对于具有集合数据的页面,大体上差不多。 只是相应的数据源会发生一些改变。 集合数据的页面最终还是会以跳转到单页页面为结果,此时单页页面的数据源就是从intent中接收,而非重新实例化。
demo在github的位置:这里 (在androidManifest.xml中将入口改成User对应的activity就可以了。)
为了防止由于时间的流逝而使得自己曾经的思考付诸流水,特此记录!