Android MVP 实战经验

一、表现层模式架构的演变

三层架构通常是指表现层(Presentation Layer)、业务逻辑层(Business Layer)和数据访问层(Data Access Layer)。表现层是用户和系统之间交流的桥梁,它一方面提供与用户交互的界面,另一方面也提供了与数据交互的逻辑,便于协调用户与系统的操作。我们所谈论的 MVC、MVP、MVVM 等设计模式都属于表现层的设计模式。

1. MVC(Model-View-Controller)模式

MVC

如果把 MVC 模式套用在 Android 中,那么:

  • View 对应 xml 布局,实现数据的展示
  • Controller 对应 Activity / Fragment ,处理业务逻辑
  • Model 对应数据源,包括网络接口数据、数据库、缓存等

使用 MVC 模式看似分工明确,但是应用到 Android 中,会带不少的问题:

  • Activity / Fragment 实现了多重职责,即是 View,又是 Controller,导致代码复杂臃肿,难以复用
  • 把 Activity / Fragment 作为 Controller,无法对 Controller 进行单元测试
    所以,在 Android 中,MVC 模式不太适用。

2. MVP(Model-View-Presenter) 模式

MVP 模式是 MVC 的进化版,它把 Controller 的职责从 Activity/Fragment 中拆分出来,作为 Presenter,这样就实现了 Activity/Fragment 和业务逻辑的解耦,更好地解决了数据与界面的关系。

二、细说 MVP

1. 职责划分

MVP 各自的职责分别是:

  • View

    1. 对应 Activity / Fragment / Custom View
    2. 与用户交互,响应用户操作,分派事件行为给 Presenter 处理
    3. 响应 Presenter 回调,对数据进行显示
  • Presenter

    1. 是连接 VIew 和其它代码的胶水
    2. 用于转换 Model 的数据以便于 VIew 显示
    3. Presenter 不做 UI 相关处理,也不包含上下文对象(Context)
  • Model

    1. 与数据进行交互,对数据进行加工处理
    2. 通常是与 Android 无关的,不会用到 Android SDK
    3. 关注从哪里拿数据(Retrofit、Sqlite etc.)

2. 架构实现

MVP

谷歌官方已经给出了一个 MVP 架构的实践示例[googlesamples/android-architecture](上面图中的 REPOSITORIES 就是指 Model 层)。

接下来,我们以它为例来看一下具体的实现。我们简化一下代码,更直观地看一下它们之间的关系。

首先官方的例子定义了一个契约(Contract)类:

public interface TasksContract {
    interface View extends BaseView<Presenter> {
        void setLoadingIndicator(boolean active);
        void showTasks(List<Task> tasks);'' 
        void showLoadingTasksError();
        // ....
    }

    interface Presenter extends BasePresenter {
       void loadTasks();
       void addNewTask();
        // ....
    }
}

契约类就是把 MVP 所需要定义的几个接口都写在一个类里面。在项目实现中,我会把 Model 接口也写到契约类里,定义契约类的好处除了可以少写几个 Java 文件外,也比较直观。比如先在 Presenter 定义一个 loadTasks 方法,那么相应地,Model 就接口需要定义一个 getTasks 方法来为 Presenter 提供数据,View 接口则需要定义一个 showTasks 来显示获取到 Tasks 数据,以及 setLoadingIndicator、showLoadingTasksError 等方法来更新 UI 状态。

  • Model 层的实现代码:
public class TasksRepository implements TasksDataSource  {
    @Inject 
    TasksRepository(@Remote TasksDataSource tasksRemoteDataSource, 
                    @Local TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = tasksRemoteDataSource;
        mTasksLocalDataSource = tasksLocalDataSource;
    }

    @Override
    public Observable<List<Task>> getTasks() {
        // 从缓存或者网络接口等数据源获取数据
    }
}

Model 层的代码维护了两个数据源(mTasksRemoteDataSource 和 mTasksLocalDataSource),用来为 Presenter 提供数据,Presenter 无需关心从哪里拿数据。代码中的注解 @Inject 标记了该构造方法可以被注入,如果你使用了 Google 的 Dagger 框架,Dagger 可以提供 TasksRepository 所需的依赖,并创建一个 TasksRepository 对象。

  • Presenter 层的实现代码:
public class TasksPresenter implements TasksContract.Presenter {
    @Inject
    TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
        mTasksRepository = tasksRepository;
        mTasksView = tasksView;
    }

    @Override
    public void loadTasks() {
        mTasksRepository.getTasks()
                .observeOn(mSchedulerProvider.ui())
                .subscribe(new Observer<List<Task>>() {
                    @Override
                    public void onCompleted() {
                        mTasksView.setLoadingIndicator(false);
                    }
                    @Override
                    public void onError(Throwable e) {
                        mTasksView.showLoadingTasksError();
                    }
                    @Override
                    public void onNext(List<Task> tasks) {
                        mTasksView.showTasks(tasks);
                    }
                });
    }
}

这里我们使用了 Dagger 和 RxJava(官方例子是分开两个独立的分支),Dagger 注入所需的依赖,并创建 TasksPresenter 对象。View 通过调用 Presenter 的 loadTasks 方法来获取便于 VIew 展示的数据。Presenter 就是用于响应 View 分派的事件,校验数据并提交给 Model 处理,最后把 Model 处理的结果转交给 View。

另外,Presenter 也承担了一部分 Activity / Fragment 的业务逻辑,这样也减轻了 Activity / Fragment 作为 View 的负担。

  • View 层的实现代码
public class TasksFragment extends Fragment implements TasksContract.View {
   private TasksContract.Presenter mPresenter;
   
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
       swipeRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
           @Override
           public void onRefresh() {
               mPresenter.loadTasks();
           }
       });
   } 

   @Override
   public void showLoadingTasksError() {
       showMessage(getString(R.string.loading_tasks_error));
   }

   @Override
   public void showTasks(List<Task> tasks) {
       mListAdapter.replaceData(tasks);
       mTasksView.setVisibility(View.VISIBLE);
       mNoTasksView.setVisibility(View.GONE);
    }
}

View 就比较简单了,纯粹做 UI 相关的处理,不关注业务处理。它将用户的行为传递给 Presenter,同时接收 Presenter 的调用来更新界面。在上面的代码中,当 TasksFragment 初始化之后,会调用 swipeRefreshLayout.setRefreshing 来触发 Presenter 的 loadTasks() 方法。之后,当 Presenter 处理完后,调用 View 的 showTasks 或者 showLoadingTasksError 方法,TasksFragment 只需要关注界面更新的具体实现。

从上面的例子来看,Model-View-Presenter 三种角色之间分工明确,使得数据与界面之间的耦合更低,代码复用性更高,也更方便于测试。

三、总结

使用 MVP 模式来开发 Android 应用给我们带来了很多好处,只是需要多定义 M-V-P 这三个接口(可以写在一个 Contract 类中),正是因为这些接口,才使得类的组织结构更加清晰,每一层实现对应的接口,只关注其本身单一的职责。这样层与层的耦合底非常低,可维护性提高。

一路走来,我们也在不断地尝试,持续地改进现有的架构。我们结合了目前流行的框架来提高生产力;比如,使用 Dagger 来为 Presenter 注入依赖(不需要再用new关键字创建各种对象),还使用了 Retrofit、RxJava、Realm 等框架更好地去组织数据层的接口,以及使用 DataBinding 来简化 UI 界面与数据实体的绑定。

架构的作用是为解决痛点,适合自己的才是最好的。希望本文对你找到适合自己项目的架构组织方式有所帮助。

// 能力一般,水平有限。文中有不妥或谬误之处在所难免,请大家批评指正。

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

推荐阅读更多精彩内容