MVP框架的演化

MVP这种架构在Android界已经基本成为标配,MVP本身也有很多写法和变种,当然,没有最好的架构,只有最合适的架构,具体架构要怎么写,还是要看实际项目的需要。
我们在这里简单梳理一下MVP的一些演化版本,希望为具体的项目实现提供一点参考。
MVP本身的概念,就是把Model、View和Presenter相互解耦,大概可以这样理解:


MVP的基本概念

各自分工如下:

  • View负责外部界面交互,不直接处理业务逻辑
  • Presenter负责内部业务逻辑,不直接处理业务数据
  • Model负责核心业务数据,与数据库和网络进行数据交互

原始MVP

如果仅从分工的角度实现MVP,只需要发生两次引用:

  • 向View里引用Presenter,(因为View都是Fragment或Activity,有特定的构造函数,所以一般采用set方式引用),以处理具体的内部业务逻辑,代码形如:
public class TasksFragment extends Fragment{
  ...
  private Presenter mPresenter;
  public void setPresenter(Presenter presenter) {
        mPresenter = presenter;
    }
  ...
}
  • 向Presenter里引用Model和View,其中View需要通过接口封装一下再引用(一般在构造函数中引用),引用Model为业务逻辑提供核心的业务数据,引用View操作与界面相关的业务逻辑,代码形如:
public class Presenter{
  ...
  private Repository mRepository;//model的实现这里不再展开
  private TasksFragment mView;
  public Presenter(Repository tasksRepository, TasksFragment tasksView) {//presenter里引用model
        mRepository = tasksRepository;
        mTView = tasksView;
        mTasksView.setPresenter(this);//view里引用presenter
    }
  ...
}

这就是一个最原始的MVP的实现,这个版本有一个严重的问题,就是可维护性太差!
这版MVP虽然实现了各司其职,但其实质只不过是把代码拆到了不同的文件里,在实现中,M、V和P都引用了实体类的实例,形成了非常紧密的耦合,它其实只是实现了这样的效果:


实际效果

很显然,难以复用,难以扩展,未来的维护简直是个灾难。
为了解耦合,很自然地,要使用接口去解耦合。

演化1-Google Architecture

Google在github上开源的architecture是个教科书般的MVP框架,它是这样做的:

  • V和P的接口化和注入
    View和Presenter不再引用实体类,而是引用抽象接口,View里引用的Presenter的接口,Presenter里引用的也是View的接口,这样的View和Presenter的代码形如:
public class TasksFragment extends Fragment implements IView{//实现接口以便注入到Presenter
  ...
  private IPresenter mPresenter;
  public void setPresenter(IPresenter presenter) {//view已有特定的构造函数,以set方式注入为宜
        mPresenter = presenter;
    }
  ...
}

public class Presenter implements IPresenter{//实现接口以便注入到view
  ...
  private Repository mRepository;//model的实现这里不再展开
  private IView mView;
  public Presenter(Repository tasksRepository, IView tasksView) {//presenter里注入model和view
        mRepository = tasksRepository;
        mTView = tasksView;
        mTasksView.setPresenter(this);//view里注入presenter
    }
  ...
}

如果愿意的话,model也可以采用接口注入的形式(google architecture并没有做model的接口注入,是为了确保引用的实例是一个全局唯一的数据层单例,这样容易避免污染数据),这样就能实现一个完好解耦的MVP:


可维护性良好MVP
  • 集中管理V和P的接口
    其实就是把V和P的接口放在同一个接口文件下了,代码形如:
public interface ITasksContract {
    interface IView{...}
    interface IPresenter{...}
}

这样做有两个好处:
1.从一组业务来讲,业务逻辑和界面逻辑在同一个文件中定义,极具连贯性,极大地方便了阅读、理解和维护(这也会引导你先从接口开始写代码)
2.从多组业务来讲(App一般有多组业务),便于管理好多个V和P的接口,这些接口天然按照业务分别写在不同文件里,扩展和引用更加清晰,不易出错

google architecture还做了一项改进,为V和P的接口定义了更基础的接口,在基础接口中统一定义了View注入Presenter的行为和Presenter开启业务工作的行为,代码形如:

public interface BaseView<T> {//用泛型定义Presenter
    void setPresenter(T presenter);//用set注入Presenter
}

public interface BasePresenter {
    void start();//开启业务工作
}

你自己实现的V和P的接口,只要继承基础接口,就能保证MVP基础行为的一致性,这样你的V和P就可以更加专注于业务
(除了教科书般的MVP,google architecture还提供了教科书般的数据Model层实现,不过这里就不做展开了)

演化2-泄露的问题

上面的这种做法,有一个潜在的问题,就是内存泄露
我们知道,Presenter为了实现业务逻辑,一手持有数据Model,一手持有View,这里面有一个隐含的bug:
数据Model在处理数据时,无论是处理本地数据还是网络数据,都是耗时操作,是不能在主线程运行的;而View,是必须在主线程运行的。这就容易产生一个问题,当View关闭退出时,Presenter可能还在异步线程里工作,而且Presenter还持有着View的实例——标准的内存泄露场景
要避免持有型的内存泄露,一个很有效的办法就是把强引用的持有变成弱引用,就是说,在Presenter里,要用WeakReference的方式去持有View,实现代码形如:

    protected WeakReference<T> mViewRef; // view 的弱引用
    public void attachView(T view){//持有View
        mViewRef = new WeakReference<T>(view);
    }
    public void detachView(){
        if (mViewRef != null){
            mViewRef.clear();
            mViewRef = null;
        }
    }
    public T getView() {//获取view的实例
        return mViewRef.get();
    }

这段代码其实是通用代码,根据聚焦业务的原则,应该抽象为基础行为,而接口是不能实现任何方法的,所以,这段代码只能通过抽象类实现通用化,整个类的代码形如:

public abstract class MVPBasePresenter<T> {
    protected WeakReference<T> mViewRef; // view 的弱引用
    public void attachView(T view){//持有View
        mViewRef = new WeakReference<T>(view);
    }
    public void detachView(){
        if (mViewRef != null){
            mViewRef.clear();
            mViewRef = null;
        }
    }
    public T getView() {//获取view的实例
        return mViewRef.get();
    }
}

其中,attachView和detachView要在View的相应的生命周期中调用,这样的话,我们又需要为View实现相关的抽象类,Fragment和Activity都需要

//需要两个泛型类型,一个用来继承Presenter的抽象类,而这个Presenter抽象类又需要一个View的泛型
public abstract class MVPBaseFragment<V,T extends MVPBasePresenter<V>> extends Fragment {
    protected T mPresenter;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        mPresenter.attachView((V)this);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        if(mPresenter!=null)
        mPresenter.detachView();
    }
    protected abstract T createPresenter();
}

这样一个业务Fragment在实例化时,代码形如:

//ICategoryContract.View是IView接口
//我们还定义了ICategoryContract.Presenter作为IPresenter接口
//CategoryPresenter继承了MVPBasePresenter抽象了和IPresenter接口
public class MainFragment 
extends MVPBaseFragment<ICategoryContract.View,CategoryPresenter> 
implements ICategoryContract.View {...}

我们实际上把Presenter抽象类和IPresenter业务接口做了分离,把View抽象类和IView业务接口做了分离,基础行为和业务逻辑互不干扰。
Activity的代码内容类似,这里不再重复。
到了实际项目中,V和P分别继承对应的抽象类,因为抽象类里已经实现了弱引用和相关的管理,所以我们可以专注于业务逻辑的实现。
不过,这样做带来两个问题:

  • 如果在View的构造函数中自动处理Presenter的实例化,实际上会束缚了我们自己的写作方式,比如我们的Presenter需要注入Model,就不能用构造方式注入;更严重的是,如果我们在Presenter初始化时需要设置某些UI控件,因为抽象类的oncreate需要先于业务类的oncreate去执行(业务类里需要先执行super.oncreate),会遇到UI控件不能及时初始化的问题。
  • Android的View其实是在不断扩张的,以Activity为例,常见的就包括Activity、AppCompatActivity、FragmentActivity、RxAppCompatActivity等,如果使用这种抽象类的模式,每遇到一种Activity,就得去做一个对应的抽象类,可扩展性很差。

参照Google的做法,我们应该再多做一点接口的文章

演化3-View的剥离

我们回头再看一遍Presenter抽象类

//Presenter抽象类
public abstract class MVPBasePresenter<T>{...}

其实在Presenter抽象类里,用来处理View的泛型是与业务无关的,我们此前是做了一个View的抽象类来配合Presenter做弱引用处理,其实细想起来,这个View的角色没必要使用抽象类,我们用一个IView基础接口就可以满足需要:

//基础接口,不需要定义任何方法
public interface IMVPBaseView {}

我们的业务接口里,IView业务接口继承这个基础接口:

public interface CategoryContract {
    ...
    interface View implements IMVPBaseView{
    ...
    }
}

我们的Fragement可以恢复Google教科书那样的简洁:

public class TasksFragment extends Fragment implements IView{//实现的接口中包含基础行为和业务逻辑
  ...
}

最终,我们的MVP结构是这样的:
Model:接口注入(更灵活)或引用一个全局单例(更干净)
View:IMVPBaseView(基础行为)-> IContract.View(业务逻辑)-> XXFragment(V的具体实现)
Presenter:(MVPBasePresenter(基础行为) + IContract.Presenter)-> XXPresenter(P的具体实现)
当然,在这种方式下,Presenter的创建、初始化、销毁等行为,也还给了最终的业务Fragment(或Activity)。

演化4-Dagger

MVP里面其实有大量的依赖关系和注入行为,代码会显得比较复杂,而Dagger是一个专门处理依赖注入的框架,可以用配置的方式实现复杂的依赖关系,所以我们完全可以用Dagger来实现MVP
在Dagger(Dagger2)里,核心要素就是Module、Inject和Component,它们分别起这样的作用:

  • Module:提供依赖,其实就是把我们此前用set或构造参数注入的依赖实例,改用module配置出来,由Dagger负责传给要注入的类,比如把IView和数据Model注入到Presenter里,代码形如:
//为presenter提供IView参数实例
@Module
public class TasksPresenterModule {
    private final TasksContract.View mView;
    public TasksPresenterModule(TasksContract.View view) {
        mView = view;
    }
    @Provides//提供参数的函数方法
    TasksContract.View provideTasksContractView() {
        return mView;
    }
}

//为presenter提供数据Model参数实例(google官方示例里又嵌套了几层component)
@Singleton//要求dagger实现单例
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {
    TasksRepository getTasksRepository();
}
  • Inject:指定依赖,就是说明某个属性对象是需要用Module注入进来的,比如在Presenter里说明某个modle对象和某个view对象是需要dagger注入进来的,代码形如:
//presenter类的参数改用Dagger注入
class TasksPresenter implements TasksContract.Presenter {
    private final TasksRepository mTasksRepository;
    private final TasksContract.View mTasksView;
    /**
     * Dagger strictly enforces that arguments not marked with {@code @Nullable} are not injected
     * with {@code @Nullable} values.
     */
    @Inject   //参数是需要注入的
    TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
        mTasksRepository = tasksRepository;
        mTasksView = tasksView;
    }
    ...
}

同样,在Activity里也要用dagger注入persenter,代码形如:

public class TasksActivity extends AppCompatActivity {
    @Inject TasksPresenter mTasksPresenter;//内部对象是需要注入的
    ...
}
  • Component:组装器,做两件事:1-把做好的Module对象作为参数提供给要注入的类,比如把Modle对象和IView对象实例化,作为Presenter的参数,完成Presenter的实例化;2-把完成注入和实例化的类,注入到当前类里,比如把完成实例化的Presenter注入到Activity里,代码形如:
public class TasksActivity extends AppCompatActivity {
    @Inject TasksPresenter mTasksPresenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Create the presenter
        DaggerTasksComponent.builder()
                //component会做出presenter需要的两个参数
                .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                .tasksPresenterModule(new TasksPresenterModule(tasksFragment))
                .build()//构造出Presenter的实例
                .inject(this);//把Presenter注入到当前Activity中
        ...
    }
    ...
}

Dagger只是用注解来配置依赖关系,编译时还是用工厂类和传参等形式实现的依赖注入,例如,针对上述代码,Dagger的apt插件会在编译时把它转成形如这样的代码:

...
//生成的Component类里,Module工厂类实现Module的实例化
this.provideTasksViewProvider = MainModule_ProvideTasksViewFactory.create(builder.mainModule);
...
//生成的Component类里,Presenter工厂类实现Presenter的实例化
mainPresenterProvider = MainPresenter_Factory.create(provideTasksRepositoryProvider,provideTasksViewProvider);
...
//生成的Activity的Injector类里,用构造参数实现依赖注入
this.mainPresenterProvider = mainPresenterProvider;

这样用Dagger实现的MVP,最开始会有点别扭,因为类之间的注入关系好像不像直接代码实现那样熟悉,但习惯之后,你会发现这么几个好处:
1.基于JSR330的稳定和标准的依赖注入方法
2.依赖关系是配置化的,代码可读性更强,也容易聚焦业务
3.可以通过注解实现全局单例

演化5-Kotlin的引入

作为基础通用框架,我们必须有一个Kotlin的版本,当然,不同的演化版本,会有不同的写法,如果参照演化3的版本,对应的Kotlin版本形如:

//基础IMVPView接口
interface IMvpView {
}
//基础MvpPresenter抽象类
abstract class MvpPresenter<T:IMvpView> {
    protected var mViewRef:WeakReIference<T>?=null

    fun attachView(view:T){
        mViewRef= WeakReference(view)
    }
    fun detachView(){
        if(mViewRef!= null){
            mViewRef!!.clear()
            mViewRef=null
        }
    }
    val view:T? get() = mViewRef!!.get()
}
//业务逻辑接口
interface ICatContract {
    interface View<Presenter>{
        fun refreshUI()
    }
    interface Presenter{
        fun doInitPage()
    }
}
//业务Presenter
class CatPresenter : MvpPresenter<CatActivity>(),ICatContract.Presenter {
    override fun doInitPage() {
    }
}
//业务Activity
class CatActivity : AppCompatActivity(),MvpView,ICatContract.View<CatPresenter> {
    val TAG: String = "CatActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
    }
    override fun refreshUI() {
    }
}

总结

MVP作为一个基础型的结构,核心作用在于辅助我们实行良好的可读性和可维护性,我们可以为一个Presenter提供多种View的实现(例如,一个业务可以同时有全屏Activity和对话框Activity两种形式,分别提供给不同的业务环节,背后却使用同一个Presenter),也可以为一个Presenter提供不同的数据Model(例如,在两个根据后台数据动态绘制界面的Activity实例中,业务逻辑一致,可以使用同一种Presenter,但数据内容不同,就可以使用两个分别注入了不同Model的Presenter实例)
MVP里有Passive View(Presenter通过View的接口操作View,并作为主要的业务驱动方)和Supervisor Controller(Presenter负责大量复杂的View逻辑)两种衍生,
MVP还是一个开放性的结构,你可以根据自己的需要,去规避某些缺陷,或取得某些优势,如何去演化一个适合自己需求的MVP框架,一方面满足需求,一方面保持灵活,完全看自己的发挥了

关于MVVM

MVP的结构比较通透明了,不过其中的View总是要写一些业务逻辑相关的代码,比如操纵Presenter,处理生命周期,实例化Model对象等,如果需要更进一步,把View的角色限定为纯粹的UI,不做任何业务逻辑,不涉及任何数据,就需要用到MVVM模式了。
在MVVM模式里,不再有Presenter,用ViewModel来处理业务逻辑,ViewModel不处理UI,而View只负责UI,与ViewModel建立数据绑定关系,通过databinding自动实现UI和Model之间的数据操作。
技术细节推荐阅读如何构建Android MVVM应用程序

引用

Github Google android-architecture
Android App的设计架构:MVC,MVP,MVVM与架构经验谈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容