以Activity/Fragment作为Presenter的MVP写法

2,200 阅读5分钟

首先说明下,文章中我要说的是MVP模式的另一种"异类"的实现方式,将"Activity/Fragment"作为Presenter,V层单独独立出来。同时下文中的P层即代表Presenter,V层代表View。

起初接触这个新型的写法,来源于何红辉的这篇文章。纵观下来,这样的写法,无外乎是作者对于传统MVP:"P层对生命周期无感知;启动Service,Activity等非视图层的功能" 之类的问题的解决之道,先看生命周期问题,诚然,我们页面的请求时机,一般是在onCreate(),onResume()或者setUserVisibleHint()等方法中,因为,我们需要在用户打开页面时才进行下拉数据,使用什么方法无非在于是否需要delay,但是,传统写法中,Presenter常常是一个单独的类,其实例由V层提供,V层在每个生命周期方法中,手动通知Presenter,从而让Presenter根据不同时机来进行业务处理,那么问题在于,P对生命周期的感知,是主动还是被动,例如:

被动形式

V层在onResume()中,通过getP().getData()得到数据,Presenter调用M层,返回数据到Presenter,然后调用Presenter通过调用getV().showData()将数据返回给View,整个过程用图粗略的表示:

传统MVP写法
主动形式
将Activity/Fragment作为P的流程
区别在于:

  1. 所有事件流向的开始从 V->P->M 变成了 P->M->V,V层不用再主动通知P,来进行对应生命周期的业务处理。
  2. 新型MVP写法,Presenter无法复用,但是View可以。
  3. 新型MVP写法,View对Presenter完全无感知。

重点说下第2,3点,P层无法复用,是这个写法的痛点,因为需要在P层实例化一个具体的View层实例,同时View层是无法得到Presenter的,这样一来,V层就完全独立了,此外View只需写自己怎么展示数据,例如RecycleView只需要设置Adapter,设置LayoutManager,至于Adapter的创建,Item的点击这些业务操作(之所以说是业务操作,是因为在我眼里,视图层只是负责展示/交互),总结来说:写View层的只需要写自己需要什么数据,怎么展示,至于数据由哪个Presenter通过什么途径获取我不关心。你可能会说,传统MVP也可以做到啊,但是传统MVP中,View层会到处散落着各种生命周期方法,以及在对应生命周期方法内的操作,会显得View层不干净,原本写View层只需要根据数据去写展示,而现在,我需要额外的关心生命周期,让Presenter在生命周期内获取我需要的数据!

举个栗子:我们两个页面的区别在于尾部不同,于是尾部我使用一个ViewGroup容器,通过判断来源,使用不同的尾部布局,接下来看两种MVP怎么做的。
以Activity/Fragment作为Presenter的MVP
  View层写法:  让Presenter传递一个布局id或者View,以及相对应布局中的View的事件监听器。

public class TestNewMvpView extends BaseView implements NewsContract.INewsView{
    @Override
    public void initView(Bundle saveInstanceState) {
        //初始化View
    }

    @Override
    public int getLayoutId() {
        return R.layout.activiity_news;
    }


    @Override
    public void setNewsData(@LayoutRes int layoutId, View.OnClickListener clickListener) {
        View view = LayoutInflater.from(getContext()).inflate(layoutId,footViewGroup);
        TextView tv = view.findViewById(R.id.xxx);
        tv.setOnClickListener(clickListener);
    }
}

  Presenter层写法:  根据来源,使用不同的尾部。

public class TestNewFragmentPresenter extends BaseFragmentPresenter<NewsContract.INewsView> implements NewsContract.INewsPresenter {
    @Override
    public Class<NewsContract.INewsView> getV() {
        return TestNewMvpView.class;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        int fromType = getArguments().getInt("fromType");
        //根据来源,或者说,根据从哪个页面跳过来的,使用不同的尾部布局
        switch (fromType){
            case 1:
                mView.setNewsData(xx,xx);
                break;
            case 2:
                mView.setNewsData(xx,xx);
                break;
        }
    }
}

以Activity/Fragment作为View层的MVP

使用的是官方MVP Demo的写法

  View层写法:   手动创建Presenter,并且通知Presenter进行初始化,然后通知View该显示哪个布局

public class TestNewFragment extends Fragment implements NewsContract.INewsView {

    private NewsContract.INewsPresenter mPresenter;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.activiity_news, container, true);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        int fromType = getArguments().getInt("fromType");
        //手动创建Presenter,并且通知Presenter初始化
        mPresenter = new TestNewPresenter(this, fromType);
        mPresenter.init();
    }


    @Override
    public void setNewsData(int layoutId, View.OnClickListener clickListener) {
        View view = LayoutInflater.from(getContext()).inflate(layoutId, footViewGroup);
        TextView tv = view.findViewById(R.id.xxx);
        tv.setOnClickListener(clickListener);
    }
}

  Presenter层写法:  创建初始化方法,根据fromType,返回不同的布局id

public class TestNewPresenter implements NewsContract.INewsPresenter {
    //View层对象最好是一个接口,解耦
    private NewsContract.INewsView mView;
    private int mFromType;

    public TestNewPresenter(NewsContract.INewsView view, int fromType) {
        this.mView = view;
        this.mFromType = fromType;
    }

    @Override
    public void init() {
        //根据不同的type,展示不同的尾部
        switch (mFromType) {
            case 1:
                mView.setNewsData(xx, xx);
                break;
            case 2:
                mView.setNewsData(xx, xx);
                break;
        }
    }
}

那么现在这两种写法,你看出区别了吗?以Fragment/Activity作为View层的写法,会让View层变的不干净,写View层的同学,不仅一边想着该在哪个生命周期中初始化,而且还需要做一些,例如创建Presenter,调用Presenter这些压根跟View层没关系的事。而以Fragment/Activity作为Presenter的写法,View层就是可以外包出去,我只需在Presenter得到数据处理后,交给View层,而View层同学,压根不用关心其他事,只需照着UI画图,然后根据数据去进行展示。
但是,在我个人看来,业务复用>页面复用,业务逻辑相比页面本身来说,前者的逻辑复杂度是大于后者,尤其是一些复杂的业务,例如笔者目前在参与的直播房间这种业务,会经常衍生出不同类型的房间,例如,单人房间,多人房间等,这个时候,Presenter的代码重合度会很高,纵使再怎么抽象,封装,该实现的实现,该调用的还是调用,此时如果将Presenter进行抽象,封装,例如,将房间内的消息模块封装为一个Presenter,礼物封装为一个Presenter等,就不用在下次出新房间需求的时候,再次创建并复制之前的业务代码。

事实上,我已经用这两种写法开发了几个公司项目,总结起来,每种MVP写法都有弊有利,像Google官方MVP DEMO中的写法,和目前这种新式MVP写法,至少在阅读代码层面,我觉得新式MVP写法的View层面会比较易读,同时View层面写起来也比较舒服,上面也用一个小demo进行了说明,但是目前也是一个非常非常小的功能,当业务足够复杂的时候,我推荐还是用传统MVP写法,去复用Presenter而不是View。