最近项目有 MVP 架构的需求。于是简单写了 MVP 的一种实现模板。开始吧。

MVP

mvp 架构是一种设计模式,他没有所谓的标准。只是一种思维上的架构。所以本文介绍的只是其中一种实现模板。

MVP 全称 Model - View - Presenter,就是把代码分成了三层。具体可以参考下图:

(%2BP`X@0WB1Z}W88OCDG9.png

然后来分别讲一下吧我对这三个逻辑层的理解吧。

Model

Model 层一般都是全局单例的。用于数据的获取。主要是本地的持久化数据与网络的数据。

Presenter

Presenter 在我看来也应该是单例的。它拥有三个生命周期:

  • onInit - 当对象实例化之后立刻调用,一般为第一个 View 层开始绑定之前调用。
  • onBind(IView v) - 当 View 层启动后绑定 Presenter 时候调用。
  • onUnbind(IView v) - 当 View 层结束后解绑的时候调用。

一个 Presenter 就对应着一个 View 层。当程序启动后第一个 View 启动的时候会实例化,然后一直到程序结束都只有一个对象。只是根据 View 层的不同情况调用 onBindonUnbind

下图是我理解的 Presenter 生命周期,假设该 Presenter 层对应的 View 层是 Activity

553PPVSB~B1$GK2`D63~JVB.png

可以看到,在该 Activity 第一次启动的时候,会实例化 Presneter 对象并调用 onInit ,然后绑定。对该 Activity 第二三次启动,此时 Presenter 对象还在,直接绑定。

这里我对 Presenter 的理解主要是将数据和界面分离。在我的 Demo 中,我加入了一个按钮可以重启当前 Activity。当你按下之后,Activity 重启之后,你会发现你当前的数字不变,这是因为我们数据都保存在 Presenter 对象中,而这个对象由契约类持有,契约类是单例模式。所以 Presenter 一旦被实例化,就会一直持续到程序彻底关掉。而 契约类里我在 bind 方法中加了判断,如果当前 Presenternull 才会实例化并调用 onInit

这也是我觉得 MVP 架构的主要目的。Activity 只负责显示和交互,数据处理统统交给 Presenter。甚至当 Activity 销毁之后,Presenter 依然保存着数据等待下一次 bind

View

这一个就很简单了,一般是 ActivityFragment。主要用于处理 UI 的显示。

Demo

这里还是直接上 Demo,比较好理解。以下是需求:

  • 主页面显示一个当前数字和历史记录数字
  • 主页面有一个按钮可以使当前数字 + 1
  • 主页面有一个按钮可以使当前数字归零
  • 主页面有一个按钮可以将当前数字加入历史数字列表
  • 历史记录的数字是持久化的,程序重启后依然存在

预览

这里给一下 动态图预览效果:

GIF 2020-8-8 星期六 21-08-40.gif

项目地址

https://github.com/heyanLE/MVPDemo

程序分包

NBHATR}1NYT{R8H2YCNFL2.png

  • adapter : 适配器,没啥特别的
  • contract :契约包,用于将 View 层和 Presenter 层连接
  • model : Model 层
  • presenter:Presenter 层
  • view :view 层

Model

Model 是按照业务划分的,比如一个游戏的 App 就拥有 用户Model游戏房间Model 等,这一层是无状态独立运行的。具体到这个 Demo 我们只需处理历史数字的持久化即可,这里采用 SharedPreferences 进行保存。

分包:

QO%0(2`()C8)S01K{4~J.png

其中外面的 StringListModel 规定了这个 Model 的接口,具体实现在 impl 里,而 Model 持有其他的 Model 对象。

/**
 * Created by HeYanLe on 2020/8/8 0008 19:22.
 * https://github.com/heyanLE
 */
public class Model {

    // StringList 对象
    public static StringListModel STRING_LIST = new StringListModelImpl();

}

StringListModel 接口

/**
 * Created by HeYanLe on 2020/8/8 0008 19:17.
 * https://github.com/heyanLE
 */
public interface StringListModel {

    List<String> load();

    void save(List<String> data);
}

具体实现:

/**
 * Created by HeYanLe on 2020/8/8 0008 19:19.
 * https://github.com/heyanLE
 */
public class StringListModelImpl implements StringListModel {

    static final String FILE_NAME = "file_name";
    static final String KEY_LIST = "key_list";

    @Override
    public List<String> load() {
        String s = DemoApplication.getInstance().getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
                .getString(KEY_LIST,"[]");
        return new Gson().fromJson(s,
                new TypeToken<List<String>>() {}.getType());
    }

    @Override
    public void save(List<String> data) {
        String s = new Gson().toJson(data);
        SharedPreferences.Editor editor = DemoApplication.getInstance()
                .getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE).edit();
        editor.putString(KEY_LIST, s);
        editor.apply();
    }
}

然后我们就可以采用 Model.STRING_LIST 的形式来进行数据保存和读取了。

Contract

分包:

XAG5QHUN_J$$B975G10MSF0.png

这个包主要是规定了 ViewPresenter 的对应关系。首先是一个基类:

/**
 * 契约类 - 指定一个 V 层和 P 层
 * Created by HeYanLe on 2020/8/8 0008 17:36.
 * https://github.com/heyanLE
 */
public abstract class Contract<V extends IView, P extends IPresenter<V>> {

    private V iView;
    private P iPresenter;


    public V getView() {
        return iView;
    }

    public P getPresenter() {
        return iPresenter;
    }

    abstract P newPresenter();

    public P bind(V iView){
        this.iView  = iView;
        if (null == iPresenter){
            iPresenter = newPresenter();
            iPresenter.onInit();
        }else{
            iPresenter.onUnbind();
        }
        iPresenter.onBind(iView);
        return iPresenter;
    }

    public void unbind(IView iView1){
        if (iView1 == iView) {
            if (null != iPresenter) {
                iPresenter.onUnbind();
                iView = null;
            }
        }
    }


}

首先通过泛型制定具体的 PresenterView ,当然这两个泛型都需要继承于基类 IpresenterIView

然后因为 Java 的泛型是无法实化的,所以我们需要一个抽象方法 newPresenter 来实例化一个指定的 Presenter。然后就是 bindunbind 了,这俩比较简单。

具体到这个项目,因为只有一个界面,所以我们直接定义一个单例模式的主页面契约:

/**
 * Created by HeYanLe on 2020/8/8 0008 19:37.
 * https://github.com/heyanLE
 */
public class MainContract extends Contract<MainActivity, MainPresenter> {

    @Override
    MainPresenter newPresenter() {
        return new MainPresenterImpl();
    }

    private static MainContract INSTANCE = new MainContract();
    public static MainContract getInstance(){
        return INSTANCE;
    }
    private MainContract(){}
}

首先泛型指定了 , MainActivityMainPresenter (当然这两个也是接口),然后重写 newPresenter 返回一个 MainPresenter 的具体实现,然后就是单例模式的实现了。如果你有其他界面,就可以新建新的契约。

Presenter

(@VB4GZ0`V6}_(G59(1E{KB.png

首先规定 Presenter 基类:

/**
 * Presenter 接口, 所有 P 层都要实现
 * Created by HeYanLe on 2020/8/8 0008 17:30.
 * https://github.com/heyanLE
 */
public interface IPresenter<T extends IView> {

    /**
     * 当 V 层绑定时调用 - 如果是 Activity 对应 onStart 生命周期
     * @param view  View 层接口
     */
    void onBind(T view);

    /**
     * 当 V 层取消绑定时调用 - 如果是 Activity 对应 onStop 生命周期
     */
    void onUnbind();

    /*
    当 初始化时候调用
     */
    void onInit();

}

这里通过泛型指定 View 层的具体类。然后三个生命周期方法。

具体到本项目,只有一个界面 所以以下是 MainPresenter 接口

/**
 * Created by HeYanLe on 2020/8/8 0008 19:37.
 * https://github.com/heyanLE
 */
public interface MainPresenter extends IPresenter<MainActivity> {

    void onAddOneClick();

    void onZeroClick();

    void onSignClick();

}

通过泛型指定对应的是 MainActivity ,然后三个方法分别是 加一按钮,归零按钮,记录按钮的对应方法。

具体实现也是放在 impl 里:

/**
 * Created by HeYanLe on 2020/8/8 0008 19:39.
 * https://github.com/heyanLE
 */
public class MainPresenterImpl implements MainPresenter {

    private MainActivity view;
    private int num;
    private List<String> list;

    @Override
    public void onAddOneClick() {
        num ++;
        view.setNumText(num+"");
        Model.STRING_LIST.save(list);
    }

    @Override
    public void onZeroClick() {
        num = 0;
        view.setNumText(num+"");
        Model.STRING_LIST.save(list);
    }

    @Override
    public void onSignClick() {
        list.add(num+"");
        Model.STRING_LIST.save(list);
        view.setNumList(list);
    }

    @Override
    public void onBind(MainActivity view) {
        this.view = view;
        view.setNumText(num+"");
        view.setNumList(list);
    }

    @Override
    public void onUnbind() {

    }

    @Override
    public void onInit() {
        num = 0;
        list = Model.STRING_LIST.load();
        if (list == null){
            list = new ArrayList<>();
        }

    }
}

其中 MainActivity 是 这个界面数据刷新的接口,具体下面可以看到。
MainPresenter 里,我们维护一个档期数字和列表,并在 onInit 的时候从 Model 读取数据,在onBind 的时候更新数据。然后在相应按钮按下的时候更新数据并保持。

View

}WFXMZ20R93~34_QTU%N.png

首先还是看基类:

/**
 * Created by HeYanLe on 2020/8/8 0008 19:23.
 * https://github.com/heyanLE
 */
public interface IView {

    Context getContext();

    <T extends View> T findViewById(int id);

}

我的想法是,作为 View 层,肯定是能管理 View 并持有 Context 的对象,所以我们 View 层的基类就可以用以上模板。这样可以使用 ActivityFragment 作为 View 层,当然你也可以自定义。

具体到本项目,只有一个界面,以下是 MainActivity 的接口:

/**
 * Created by HeYanLe on 2020/8/8 0008 19:24.
 * https://github.com/heyanLE
 */
public interface MainActivity extends IView {

    void setNumText(String s);

    void setNumList(List<String> list);

}

主要就两个方法,更改当前显示的数字和刷新历史数字列表;

然后具体实现还是在 impl

/**
 * Created by HeYanLe on 2020/8/8 0008 19:34.
 * https://github.com/heyanLE
 */
public class MainActivityImpl extends AppCompatActivity implements MainActivity {

    private MainPresenter mPresenter;
    private ActivityMainBinding mBinding;
    private ListAdapter listAdapter;
    private List<String> list = new ArrayList<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityMainBinding.inflate(LayoutInflater.from(this));
        setContentView(mBinding.getRoot());


    }

    @Override
    protected void onStart() {
        super.onStart();
        initView();
        mPresenter = MainContract.getInstance().bind(this);
    }

    private void initView(){
        listAdapter = new ListAdapter(list);
        mBinding.recycler.setLayoutManager(new LinearLayoutManager(this));
        mBinding.recycler.setAdapter(listAdapter);

        mBinding.sign.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mPresenter.onSignClick();
            }
        });
        mBinding.add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mPresenter.onAddOneClick();
            }
        });
        mBinding.zero.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mPresenter.onZeroClick();
            }
        });
        mBinding.restart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivityImpl.this, MainActivityImpl.class);
                startActivity(intent);
                finish();
            }
        });
    }

    @Override
    protected void onStop() {
        super.onStop();
        MainContract.getInstance().unbind(this);
    }

    @Override
    public void setNumText(String s) {
        mBinding.num.setText(s);
    }

    @Override
    public void setNumList(List<String> list) {
        this.list.clear();
        this.list.addAll(list);
        listAdapter.notifyDataSetChanged();
    }

    @Override
    public Context getContext() {
        return this;
    }
}

有点长,这里我只讲重点,首先这个界面使用了 ViewBinding 进行View 绑定,具体就不多说了。

我们在 onStartonStop 中分别调用契约类的绑定和取消绑定的方法:


@Override
protected void onStart() {
    super.onStart();
    initView();
    mPresenter = MainContract.getInstance().bind(this);
}

@Override
protected void onStop() {
    super.onStop();
    MainContract.getInstance().unbind(this);
}

注意:绑定 Presenter 要在 initView 之后。因为一旦开始绑定了,Presenter 层就有可能会更新数据,如果此时 View 还没初始化,就有可能出现异常。

然后就是接收 Presenter 的更新调用并更新界面了。

@Override
public void setNumText(String s) {
    mBinding.num.setText(s);
}

@Override
public void setNumList(List<String> list) {
    this.list.clear();
    this.list.addAll(list);
    listAdapter.notifyDataSetChanged();
}

同时在按钮点击之后调用 Presenter 层的方法:

 private void initView(){
    listAdapter = new ListAdapter(list);
    mBinding.recycler.setLayoutManager(new LinearLayoutManager(this));
    mBinding.recycler.setAdapter(listAdapter);

    mBinding.sign.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mPresenter.onSignClick();
        }
    });
    mBinding.add.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mPresenter.onAddOneClick();
        }
    });
    mBinding.zero.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mPresenter.onZeroClick();
        }
    });
    mBinding.restart.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent(MainActivityImpl.this, MainActivityImpl.class);
            startActivity(intent);
            finish();
        }
    });
}

至此,一个 MVP 架构的项目就完工了。