MVP-- 一个小例子彻底搞懂MVP(玉刚说)

本文深入浅出地介绍了MVP(Model-View-Presenter)架构的概念、原理及应用场景,并通过实例对比了MVP与MVC的区别,详细阐述了MVP的实现方式、注意事项及封装技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一个小例子彻底搞懂MVP。

https://mp.weixin.qq.com/s/Jrv6f_TU-LrzKm65wyF97w

 

什么是MVP

MVP全称:Model-View-Presenter; MVP是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller / Presenter负责逻辑的处理,Model提供数据,View负责显示。

为什么要使用MVP

在讨论为什么要使用MVP架构之前,我们首先要了解传统的MVC的架构的特点及其缺点。

首先看一下MVC架构的模型图,如下

MVC

这个图很简单,当查看需要更新时,首先去找控制器,然后控制器找模型获取数据,模型获取到数据之后直接更新查看。

在MVC里,View是可以直接访问Model的。从而,View里会包含模型信息,不可避免的还要包括一些业务逻辑。在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即查看。所以,在MVC模型里,模型不依赖于View,但是查看是依依于Model的。不仅如此,因为有一些业务逻辑在查看里实现了,导致要更改查看也是比较困难的,至少那些业务逻辑是无法重用的。

这样说可能会有点抽象,下面通过一个简单的例子来说明。

假设现在有这样一个需求,活动中有一个按钮和一个TextView,点击按钮时会请求网络获取一段字符串,然后把字符串显示在TextView中,按照正常的逻辑,代码应该这么写

public class MVCActivity extends AppCompatActivity {

    private Button button;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        button = findViewById(R.id.button);
        textView = findViewById(R.id.text_view);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new HttpModel(textView).request();
            }
        });
    }
}



public class HttpModel {
    private TextView textView;

    public HttpModel(TextView textView) {
        this.textView = textView;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            textView.setText((String) msg.obj);
        }
    };

    public void request() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    Message msg = handler.obtainMessage();
                    msg.obj = "从网络获取到的数据";
                    handler.sendMessage(msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

代码很简单,当点击按钮的时候,创建一个HttpModel对象,并把TextView对象作为参数传入,然后调用它的请求方法来请求数据,当请求到数据之后,切换到主线程中更新TextView,流程完全符合上面的MVC架构图。

但是这里有个问题,首先很显然,HttpModel就是Model层,那么查看层和模型层分别干了什么事,在本例中,查看层主要做的事就是当获取到网络数据的时候,更新TextView,Controller层主要做的事就是创建HttpModel对象并调用它的请求方法,我们发现MVCActivity同时充当了View层和控制层。

这样会造成两个问题,第一,查看层和控制器层没有分离,逻辑比较混乱;第二,同样因为View和Controller层的耦合,导致活动或者Fragment很臃肿,代码量很大。由于本例比较简单,所以这两个问题都不是很明显,如果活动中的业务量很大,那么问题就会体现出来,开发和维护的成本会很高。

如何使用MVP

既然MVC有这些问题,那么应该如何改进呢,答案就是使用MVP的架构,关于MVP架构的定义前面已经说了,下面看一下它的模型图

MVP

这个图也很简单,当查看需要更新数据时,首先去找Presenter,然后Presenter去找Model请求数据,模型获取到数据之后通知Presenter,Presenter再通知查看更新数据,这样Model和View就不会直接交互了,所有的交互都由Presenter进行,Presenter充当了桥梁的角色。很显然,Presenter必须同时持有View和Model的对象的引用,才能在它们之间进行通信。

接下来用MVP的架构来改造上面的例子,代码如下

interface MVPView {
    void updateTv(String text);
}


public class MVPActivity extends AppCompatActivity implements MVPView {
    private Button button;
    private TextView textView;
    private Presenter presenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        button = findViewById(R.id.button);
        textView = findViewById(R.id.text_view);
        presenter = new Presenter(this);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.request();
            }
        });
    }

    @Override
    public void updateTv(String text) {
        textView.setText(text);
    }
}


interface Callback {
    void onResult(String text);
}


public class HttpModel {
    private Callback callback;

    public HttpModel(Callback callback) {
        this.callback = callback;
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            callback.onResult((String) msg.obj);
        }
    };

    public void request() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    Message msg = handler.obtainMessage();
                    msg.obj = "从网络获取到的数据";
                    handler.sendMessage(msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}


public class Presenter {
    private MVPView view;
    private HttpModel model;

    public Presenter(MVPView view) {
        this.view = view;
        model = new HttpModel(new Callback() {
            @Override
            public void onResult(String text) {
                Presenter.this.view.updateTv(text);
            }
        });
    }

    public void request() {
        model.request();
    }
}

简单解释一下上面的代码,首先创建一个MVPView的接口,它即时查看层,里面有一个更新TextView的方法,然后让Activity实现这个接口,并复写更新TextView的方法.Model层不再传入TextView了,而是传入一个回调接口Callback,因为网络请求获取数据是异步的,在获取到数据之后需要通过Callback来通知Presenter.Presenter也很简单,首先在它的构造方法中,同时持有View和Model的引用,再对外提供一个请求方法。

分析一下上面代码执行的流程,当点击按钮的时候,Presenter调用请求方法,在它的内部,通过模型调用请求方法来请求数据,请求到数据之后,切换到主线程,调用callback的onResult方法来通知Presenter,这时候Presenter就会调用查看的更新方法来更新TextView,完成了整个流程,可以发现,在整个过程从,View和模型并没有直接交互,所有的交互都是在Presenter中进行的。

注意事项

接口的必要性
可能有的同学会问,为什么要写一个MVPView的接口,直接把活动本身传入到Presenter不行吗?这当然是可行的,这里使用接口主要是为了代码的复用,试想一下,如果直接传入活动,那么这个Presenter就只能为这一个活动服务。举个例子,假设有个App已经开发完成了,可以在手机上正常使用,现在要求做平板上的适配,在平板上的界面显示效果有所变化,TextView并不是直接在Activity中的,而是在Fragment里面,如果没有使用View的接口的话,那就需要再写一个针对Fragment的Presenter,然后把整个过程再来一遍。但是使用View的接口就很简单了,直接让Fragment实现这个接口,然后复写接口里面的方法,Presenter和Model层都不需要做任何改动。同理,Model层也可以采用接口的方式来写。

防止内存泄漏
其实上面的代码存在内存泄漏的风险。试想一下,如果在点击按钮之后,模型获取到数据之前,退出了活动,此时由于活动被演示者引用,而Presenter正在进行耗时操作,会导致活动的对象无法被回收,造成了内存泄漏,解决的方式很简单,在活动退出的时候,把Presenter对中查看的引用置为空即可。

// Presenter.java
public void detachView() {
    view = null;
}

// MVPActivity.java
@Override
protected void onDestroy() {
    super.onDestroy();
    presenter.detachView();
}

另外还有一个问题,虽然这里活动不会内存泄漏了,但是当活动退出之后,模型中请求数据就没有意义了,所以还应该在detachView方法中,把Handler的任务取消,避免造成资源浪费,这个比较简单,就不贴代码了。

MVP的封装

很显然,MVP的实现套路是大致相同的,如果在一个应用中,存在大量的Activity和Fragment,并且都使用MVP的架构,那么难免会有很多重复工作,所以封装就很有必要性了。

在说MVP的封装之前,需要强调一点,MVP更多的是一种思想,而不是一种模式,每个开发者都可以按照自己的思路来实现具有个性化的MVP,所以不同的人写出的MVP可能会有一些差别,笔者在此仅提供一种实现思路,供读者参考。

首先模型,View和Presenter都可能会有一些通用性的操作,所以可以分别定义三个对应的底层接口。

interface BaseModel {
}

interface BaseView {
    void showError(String msg);
}

public abstract class BasePresenter<V extends BaseViewM extends BaseModel{
    protected V view;
    protected M model;

    public BasePresenter() {
        model = createModel();
    }

    void attachView(V view) {
        this.view = view;
    }

    void detachView() {
        this.view = null;
    }

    abstract M createModel();
}

这里的View层添加了一个通用的方法,显示错误信息,写在接口层,可以在实现处按照需求来显示,比如有的地方可能会是弹出一个Toast,或者有的地方需要将错误信息显示在TextView中,模型层也可以根据需要添加通用的方法,重点来看一下Presenter层。

这里的BasePresenter采用了泛型,为什么要这么做呢?主要是因为Presenter必须同时持有View和Model的引用,但是在底层接口中无法确定他们的类型,只能确定他们是BaseView和BaseModel的子类,所以采用泛型的方式来引用,就巧妙的解决了这个问题,在BasePresenter的子类中只要定义好查看和模型的类型,就会自动引用他们的对象了.Presenter中的通用的方法主要就是attachView和detachView,分别用于创建查看对象和把View的对象置位空,前面已经说过,置空是为了防止内存泄漏,Model的对象可以在Presenter的构造方法中创建。另外,这里的Presenter也可以写成接口的形式,读者可以按照自己的喜好来选择。

然后看一下在业务代码中该如何使用MVP的封装,代码如下

interface TestContract {

    interface Model extends BaseModel {
        void getData1(Callback1 callback1);
        void getData2(Callback2 callback2);
        void getData3(Callback3 callback3);
    }

    interface View extends BaseView {
        void updateUI1();
        void updateUI2();
        void updateUI3();
    }

    abstract class Presenter extends BasePresenter<ViewModel{
        abstract void request1();
        abstract void request2();
        void request3() {
            model.getData3(new Callback3() {
                @Override
                public void onResult(String text) {
                    view.updateUI3();
                }
            });
        }
    }
}

首先定义一个契契约接口,然后把Model,View,和Presenter的子类分别放入Contract的内部,这里的一个Contract就对应一个页面(一个活动或者一个片段),放在Contract内部是为了让同一个页面的逻辑方法都放在一起,方便查看和修改.Presenter中的request3方法演示了如何通过Presenter来进行View和Model的交互。

接下来要做的就是实现这三个模块的逻辑方法了,在Activity或Fragment中实现TextContract.View的接口,再分别创建两个类用来实现TextContract.Model和TextContract.Presenter,复写里面的抽象方法就好了。

扩展:用RxJava简化代码

上面的代码中,Model层中的每个方法都传入了一个回调接口,这是因为获取数据往往是异步的,在获取的数据时需要用回调接口通知Presenter来更新查看。

如果想要避免回调接口,可以采用RxJava的方式来模型获取的数据直接返回一个Observable,接下来用RxJava的方式来改造前面的例子

public class HttpModel {
    public Observable<String> request() {
        return Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> emitter) throws Exception {
                Thread.sleep(2000);
                emitter.onNext("从网络获取到的数据");
                emitter.onComplete();
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}


public class Presenter {
    private MVPView view;
    private HttpModel model;

    public Presenter(MVPView view) {
        this.view = view;
        model = new HttpModel();
    }

    private Disposable disposable;

    public void request() {
        disposable = model.request()
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String s) throws Exception {
                        view.updateTv(s);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {

                    }
                });
    }

    public void detachView() {
        view = null;
        if (disposable != null && !disposable.isDisposed()) {
            disposable.dispose();
        }
    }
}

Model的请求方法直接返回一个Observable,然后在Presenter中调用subscribe方法来通知查看更新,这样就避免了使用回调接口。

开源库推荐

一个MVP架构的开源库,正如笔者所说要比较简单的集成MVP的架构,笔者推荐这个库:
https ://github.com/sockeqwe/mosby 
它的使用方法比较简单,可以直接参考官方的demo,接下来简单的分析一下作者的封装思想。

首先查看层和Presenter层分别有一个基础的接口

public interface MvpView {
}

public interface MvpPresenter<V extends MvpView{

  /**
   * Set or attach the view to this presenter
   */
  @UiThread
  void attachView(V view);

  /**
   * Will be called if the view has been destroyed. Typically this method will be invoked from
   * <code>Activity.detachView()</code> or <code>Fragment.onDestroyView()</code>
   */
  @UiThread
  void detachView(boolean retainInstance);
}

这里加@UIThread注解是为了确保attachView和detachView都运行在主线程中。
然后业务代码的活动需要继承MvpActivity

public abstract class MvpActivity<V extends MvpViewP extends MvpPresenter<V>>
    extends AppCompatActivity implements MvpView,
    com.hannesdorfmann.mosby3.mvp.delegate.MvpDelegateCallback<V,P{

  protected ActivityMvpDelegate mvpDelegate;
  protected P presenter;
  protected boolean retainInstance;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getMvpDelegate().onCreate(savedInstanceState);
  }

  @Override protected void onDestroy() {
    super.onDestroy();
    getMvpDelegate().onDestroy();
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    getMvpDelegate().onSaveInstanceState(outState);
  }

  @Override protected void onPause() {
    super.onPause();
    getMvpDelegate().onPause();
  }

  @Override protected void onResume() {
    super.onResume();
    getMvpDelegate().onResume();
  }

  @Override protected void onStart() {
    super.onStart();
    getMvpDelegate().onStart();
  }

  @Override protected void onStop() {
    super.onStop();
    getMvpDelegate().onStop();
  }

  @Override protected void onRestart() {
    super.onRestart();
    getMvpDelegate().onRestart();
  }

  @Override public void onContentChanged() {
    super.onContentChanged();
    getMvpDelegate().onContentChanged();
  }

  @Override protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    getMvpDelegate().onPostCreate(savedInstanceState);
  }

  /**
   * Instantiate a presenter instance
   *
   * @return The {@link MvpPresenter} for this view
   */
  @NonNull public abstract P createPresenter();

  /**
   * Get the mvp delegate. This is internally used for creating presenter, attaching and detaching
   * view from presenter.
   *
   * <p><b>Please note that only one instance of mvp delegate should be used per Activity
   * instance</b>.
   * </p>
   *
   * <p>
   * Only override this method if you really know what you are doing.
   * </p>
   *
   * @return {@link ActivityMvpDelegateImpl}
   */
  @NonNull protected ActivityMvpDelegate<V, P> getMvpDelegate() {
    if (mvpDelegate == null) {
      mvpDelegate = new ActivityMvpDelegateImpl(thisthistrue);
    }

    return mvpDelegate;
  }

  @NonNull @Override public P getPresenter() {
    return presenter;
  }

  @Override public void setPresenter(@NonNull P presenter) {
    this.presenter = presenter;
  }

  @NonNull @Override public V getMvpView() {
    return (V) this;
  }
}

MvpActivity中持有一个ActivityMvpDelegate对象,它的实现类是ActivityMvpDelegateImpl,并且需要传入MvpDelegateCallback接口,ActivityMvpDelegateImpl的代码如下

public class ActivityMvpDelegateImpl<V extends MvpViewP extends MvpPresenter<V>>
    implements ActivityMvpDelegate {

  protected static final String KEY_MOSBY_VIEW_ID = "com.hannesdorfmann.mosby3.activity.mvp.id";

  public static boolean DEBUG = false;
  private static final String DEBUG_TAG = "ActivityMvpDelegateImpl";

  private MvpDelegateCallback<V, P> delegateCallback;
  protected boolean keepPresenterInstance;
  protected Activity activity;
  protected String mosbyViewId = null;

  /**
   * @param activity The Activity
   * @param delegateCallback The callback
   * @param keepPresenterInstance true, if the presenter instance should be kept across screen
   * orientation changes. Otherwise false.
   */
  public ActivityMvpDelegateImpl(@NonNull Activity activity,
      @NonNull MvpDelegateCallback<V, P> delegateCallback, boolean keepPresenterInstance) {

    if (activity == null) {
      throw new NullPointerException("Activity is null!");
    }

    if (delegateCallback == null) {
      throw new NullPointerException("MvpDelegateCallback is null!");
    }
    this.delegateCallback = delegateCallback;
    this.activity = activity;
    this.keepPresenterInstance = keepPresenterInstance;
  }

  /**
   * Determines whether or not a Presenter Instance should be kept
   *
   * @param keepPresenterInstance true, if the delegate has enabled keep
   */
  static boolean retainPresenterInstance(boolean keepPresenterInstance, Activity activity) {
    return keepPresenterInstance && (activity.isChangingConfigurations()
        || !activity.isFinishing());
  }

  /**
   * Generates the unique (mosby internal) view id and calls {@link
   * MvpDelegateCallback#createPresenter()}
   * to create a new presenter instance
   *
   * @return The new created presenter instance
   */
  private P createViewIdAndCreatePresenter() {

    P presenter = delegateCallback.createPresenter();
    if (presenter == null) {
      throw new NullPointerException(
          "Presenter returned from createPresenter() is null. Activity is " + activity);
    }
    if (keepPresenterInstance) {
      mosbyViewId = UUID.randomUUID().toString();
      PresenterManager.putPresenter(activity, mosbyViewId, presenter);
    }
    return presenter;
  }

  @Override public void onCreate(Bundle bundle) {

    P presenter = null;

    if (bundle != null && keepPresenterInstance) {

      mosbyViewId = bundle.getString(KEY_MOSBY_VIEW_ID);

      if (DEBUG) {
        Log.d(DEBUG_TAG,
            "MosbyView ID = " + mosbyViewId + " for MvpView: " + delegateCallback.getMvpView());
      }

      if (mosbyViewId != null
          && (presenter = PresenterManager.getPresenter(activity, mosbyViewId)) != null) {
        //
        // Presenter restored from cache
        //
        if (DEBUG) {
          Log.d(DEBUG_TAG,
              "Reused presenter " + presenter + " for view " + delegateCallback.getMvpView());
        }
      } else {
        //
        // No presenter found in cache, most likely caused by process death
        //
        presenter = createViewIdAndCreatePresenter();
        if (DEBUG) {
          Log.d(DEBUG_TAG, "No presenter found although view Id was here: "
              + mosbyViewId
              + ". Most likely this was caused by a process death. New Presenter created"
              + presenter
              + " for view "
              + getMvpView());
        }
      }
    } else {
      //
      // Activity starting first time, so create a new presenter
      //
      presenter = createViewIdAndCreatePresenter();
      if (DEBUG) {
        Log.d(DEBUG_TAG, "New presenter " + presenter + " for view " + getMvpView());
      }
    }

    if (presenter == null) {
      throw new IllegalStateException(
          "Oops, Presenter is null. This seems to be a Mosby internal bug. Please report this issue here: https://github.com/sockeqwe/mosby/issues");
    }

    delegateCallback.setPresenter(presenter);
    getPresenter().attachView(getMvpView());

    if (DEBUG) {
      Log.d(DEBUG_TAG, "View" + getMvpView() + " attached to Presenter " + presenter);
    }
  }

  private P getPresenter() {
    P presenter = delegateCallback.getPresenter();
    if (presenter == null) {
      throw new NullPointerException("Presenter returned from getPresenter() is null");
    }
    return presenter;
  }

  private V getMvpView() {
    V view = delegateCallback.getMvpView();
    if (view == null) {
      throw new NullPointerException("View returned from getMvpView() is null");
    }
    return view;
  }

  @Override public void onDestroy() {
    boolean retainPresenterInstance = retainPresenterInstance(keepPresenterInstance, activity);
    getPresenter().detachView(retainPresenterInstance);
    if (!retainPresenterInstance && mosbyViewId != null) {
      PresenterManager.remove(activity, mosbyViewId);
    }

    if (DEBUG) {
      if (retainPresenterInstance) {
        Log.d(DEBUG_TAG, "View"
            + getMvpView()
            + " destroyed temporarily. View detached from presenter "
            + getPresenter());
      } else {
        Log.d(DEBUG_TAG, "View"
            + getMvpView()
            + " destroyed permanently. View detached permanently from presenter "
            + getPresenter());
      }
    }
  }

  @Override public void onPause() {

  }

  @Override public void onResume() {

  }

  @Override public void onStart() {

  }

  @Override public void onStop() {

  }

  @Override public void onRestart() {

  }

  @Override public void onContentChanged() {

  }

  @Override public void onSaveInstanceState(Bundle outState) {
    if (keepPresenterInstance && outState != null) {
      outState.putString(KEY_MOSBY_VIEW_ID, mosbyViewId);
      if (DEBUG) {
        Log.d(DEBUG_TAG,
            "Saving MosbyViewId into Bundle. ViewId: " + mosbyViewId + " for view " + getMvpView());
      }
    }
  }

  @Override public void onPostCreate(Bundle savedInstanceState) {
  }
}

代码有点长,但是逻辑还是比较清晰的,它其实就是在onCreate方法中根据不同的情况来创建Presenter对象,并通过MvpDelegateCallback的setPresenter方法把它保存在MvpDelegateCallback中,这里的MvpDelegateCallback就是MvpActivity本身。另外可以在活动的各个生命周期方法中加入需要实现的逻辑。

可能有的同学会问,为什么没有Model呢?其实这里的代码主要是对Presenter的封装,从作者给出的官方演示中可以发现,Model和View都是需要自己创建的。

这里只做一个简单的分析,有兴趣的同学可以自己查看它的源码,再强调一遍,MVP更多的是一种思想,不用局限于某一种套路,可以在领悟了它的思想之后,写出自己的MVP。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值