前言
作为菜鸟一只,学习的新知识都要记下来,以便日后复习。
本文侧重点在于介绍 Android MVP 的优劣,通过 Google 官方的to-do-mvp 系列项目了解官方是如何使用 MVP 的,并通过自己动手写一个小小的 MVP-demo 来加深对该模式的理解。
不废话了,下面进入正文。
MVC
谈到 MVP,就不能不提它的“前身”- MVC,但为了更好的了解,我们还需要向上追溯到 三层架构 :
- 界面层:与用户交互的界面
- 业务逻辑层:界面层和数据访问层的桥梁,实现业务逻辑。
- 数据访问层:和数据库打交道,类似DAO。
而 MVC 实际上更多只涉及前两层,目的在于解除业务逻辑和视图之间的耦合。可以说不只是 Android ,甚至在整个软件开发中都是使用最广的系统架构之一。
MVC 将整个结构分为三个组件--Model、View、Controller
- Model(模型):Model 是应用程序的数据源,同时包括对业务逻辑的封装。它接受 Controller 的请求并完成相应的业务处理,并将处理后的数据通过 View 显示给用户。数据源可以是Web、本地数据库(sqlite)等。
- View(视图):该组件直接与用户交互,并负责用户如何查看我们的应用程序。View 可以直接与 Model 进行交互,在MVC中,XML(也可以说是 Activity )被视为视图。
- Controller(控制器):这是MVC模式的重要部分,Controller是操作、编辑、使用 Model 并通过 View 显示给用户的组件。Controller 负责收集所有数据,在Model 和 View 之间充当中间人。Activity/Fragment 被认为是Android 的 Controller 。
一句话概括 MVC 的工作机理就是:当 User 触发事件时,View 发送指令到Controller,之后 Controller 通知 Model 更新数据,之后将结果显示到 View 中。
过程很理想是吧?但是在 Android 却并不怎么令人满意,我们来看看在 Android 中是个什么情况。
首先 布局.xml 毫无疑问是 View 吧,然后一些 java bean 之类的就是 Model,而 Controller 则是 Activity/Fragment 咯,但是理想很丰满,现实很骨感,作为 View 而言,xml 显然是不能胜任的,它只能展示最基础的静态界面,比如当我们动态隐藏显示一个界面时候,我们必须在 Activity/Fragment 中去实现,这也就导致了 Activity/Fragment 既是 Controller 但是又承担了一部分 View 的工作。
可以说 Android 中的 MVC 只做到了 M-V,因为所有一切都和 Activity 紧密相连。
MVC 在 Android 中的表现大致如下:
上述结果就是,Activity 中的代码轻轻松松上千行。如果只是我们自己写,自己维护的话,上千行似乎并非不能接受。但是一但需要你去看别人的上千行代码,想想就很难受。(更别说需要研读 Android 破万行的源码了。。)
为了解决这个重大问题,我们需要将 Activity 承担的工作拆分,Activity 只控制 View,另外新建一个 Controller ,以此避免 Activity 越来越大,难以维护。
于是就衍生出了 MVP。
MVP
MVP 作为 MVC 的衍生,将 Controller 和 View 从 Activity 中分割开开。对于 Android 来说,MVP 的 Model 和 MVC 中的 Model 是一样的,而 Activity/Fragment 不再是 Controller,而是纯粹的 View,所有关于用户事件的处理都通过 Presenter。
我们可以看到,最明显的差别就是 Model 和 View 不再相连,取而代之的是 Presenter 在二者之间充当桥梁,分别与 Model 和 View 双向通信。
工作流程大致为:
- View 接受用户的交互请求
- View 将事件传递到 Presenter
- Presenter 操作 Model 进行数据处理
- Model 处理完成后,通知 Presenter 处理已完成
- Presenter 根据处理后的数据更新 View 的显示
至于 Presenter 如何与 Model、View 交互的,还记得设计模式中提到的 面向接口编程 的思想么?没错,这里我们也是采取接口的形式,比如 Activity/Fragment 实现已经定义好的接口,在对应的 Presenter 中通过接口调用方法。
下面我们就看一看 Google 为我们提供的 MVP 示例中是如何编程的。
如果读者对 面向接口编程 的思想不了解,那么建议先去Google 一下,有基本的了解之后,再继续阅读,不然只能是徒增痛苦。。。。
Google todo-mvp 项目介绍
Google 在 GayHub 上推出了一个项目 Android Architecture Blueprints,用来展示 Android 使用各种各样的 MVP 架构,虽然Google 表示其中的示例只是用来做参考,并不是要做标准,但是作为 Google 脑残粉,相信 Google 出品,必属精品 。
项目中设计多种架构,但作为菜鸡一枚,还是只从最基础的 todo-mvp 入手,分析如何实现 MVP 架构。
总体结构
以 StatisticsContract 为例(其他类似):
基类
//在 Fragment 的 onResume()中调用方法,作用是 presenter 开始获取数据并调用 view 中方法改变界面显示
public interface BasePresenter {
void start();
}
//在 presenter 实现类中调用方法,作用是将 presenter 实例传入 view 中
public interface BaseView<T> {
void setPresenter(T presenter);
}
“契约类”-XXXContract
public interface StatisticsContract {
interface View extends BaseView<Presenter> {
void setProgressIndicator(boolean active);
void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks);
void showLoadingStatisticsError();
boolean isActive();
}
interface Presenter extends BasePresenter {
}
}
Google 似乎很喜欢这种写法,编写一个契约类来管理 View 和 Presenter 的所有接口,这种方式使得我们很清楚的知道这二者有哪些功能,方便维护。
Activity 的作用
在子模块中扮演着 模块管理者 的角色,负责创建 Presenter 实例以及 创建 View(Fragment),并将二者联系起来:
@Override
protected void onCreate(Bundle savedInstanceState) {
//。。。。
StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager()
.findFragmentById(R.id.contentFrame);
if (statisticsFragment == null) {
statisticsFragment = StatisticsFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),statisticsFragment, R.id.contentFrame);
}
new StatisticsPresenter(
Injection.provideTasksRepository(getApplicationContext()), statisticsFragment);
//。。。。
}
在创建 StatisticsPresenter 时,我们传入了 statisticsFragment,再看看 StatisticsPresenter 的构造函数:
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "StatisticsView cannot be null!");
mStatisticsView.setPresenter(this);
}
也就是说 这时 StatisticsPresenter 获取到了 statisticsFragment 的引用,且其实现了 view 接口,那么就可以调用 view 的方法了。
View <--> Presenter
分析一下 View 如何与 Presenter 双向通信,上源码:
public class StatisticsFragment extends Fragment implements StatisticsContract.View {
private TextView mStatisticsTV;
private StatisticsContract.Presenter mPresenter;
public static StatisticsFragment newInstance() {
return new StatisticsFragment();
}
@Override
public void setPresenter(@NonNull StatisticsContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
//。。。。
}
可以看到,Fragment 作为 View ,同时在 setPresenter 方法中得到 Presenter 实例(结合 Presenter 的构造方法),从而可以调用 Presenter 中的方法。
而上面我们也提到过在 presenter 的构造方法中获取到了 fragment 也就是view 的引用。于是 二者就可以互相通信了。
Model <--> Presenter
分析完 View-Presenter,再来看看 Model 如何与 Presenter 双向交互,源码:
public class StatisticsPresenter implements StatisticsContract.Presenter {
private final TasksRepository mTasksRepository;
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "StatisticsView cannot be null!");
mStatisticsView.setPresenter(this);
}
@Override
public void start() {
loadStatistics();
}
private void loadStatistics() {
mStatisticsView.setProgressIndicator(true);
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
int activeTasks = 0;
int completedTasks = 0;
//.....
mStatisticsView.setProgressIndicator(false);
mStatisticsView.showStatistics(activeTasks, completedTasks);
}
@Override
public void onDataNotAvailable() {
// The view may not be able to handle UI updates anymore
if (!mStatisticsView.isActive()) {
return;
}
mStatisticsView.showLoadingStatisticsError();
}
});
}
}
上述代码中的 TasksRepository 即为 Model,仍然是在构造函数中获取到其引用,同时在 loadStatistics() 方法中调用 mTasksRepository.getTasks()方法,体现的 presenter 调用 model 的方法;同时 getTasks() 方法中传入了 TasksDataSource.LoadTasksCallback() 参数,该接口定义如下:
public interface TasksDataSource {
interface LoadTasksCallback {
void onTasksLoaded(List<Task> tasks);
void onDataNotAvailable();
}
//。。。。
而在 TasksRepository(Model)中 getTasks() 方法定义如下:
public class TasksRepository implements TasksDataSource {
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
});
}
}
//。。。。。
}
也就是说,在getTasks()方法中,TasksRepository 处理完数据之后,回调了 StatisticsPresenter 中实现的方法,也就体现了 Model -> Presenter。
小结
到这里,关于 todo-mvp 的架构就分析的差不多了,总体上看,MVP 的使用使得整个结构十分的清晰,毕竟我这样的菜鸟都能去分析源码了,虽然代码量略微增多,但是每个模块的界限很清晰,责任单一,高度的解耦,使得维护起来很轻松。
动手撸一个 Demo
上面我们分析了Google的源码,但看懂毕竟只是看懂,距离我们深入理解还差得远,下面就动手实践一下,撸一个 MVP 的简易 Demo。
Demo 地址见文章结尾。
先放上最终的效果:
接下来是 Demo 的结构图:
下面我们就来一点点实现。
Model
首先肯定需要有一个实体类 User,然后对于这个 Demo,业务逻辑只有一个:登录,那么我们也就至少有一个 login() 方法来实现登录业务。同时,我还需要能告知 presenter 我是否登陆成功了,那么我就需要写一个 回调接口 供 presenter 来实现。
User.java
package com.bit.whdalive.demomvp.bean;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
IUserModel.java
package com.bit.whdalive.demomvp.mvp;
public interface IUserModel {
void login(String username,String password,OnLoginListener listener);
//回调接口,我放到IUserModel中,实际上也可以单独抽离出来,或者放到 presenter 的接口中都是可以的,毕竟这个demo功能太单一了
public interface OnLoginListener{
void loginSuccess();
void loginFailed();
}
}
IUserModelImpl.java
package com.bit.whdalive.demomvp.mvp;
public class UserModelImpl implements IUserModel{
private IUserLoginPresenter mIUserLoginPresenter;
public UserModelImpl(IUserLoginPresenter IUserLoginPresenter) {
mIUserLoginPresenter = IUserLoginPresenter;
}
@Override
public void login(final String username, final String password,final IUserModel.OnLoginListener listener) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if("whdalive".equals(username)&&"123...".equals(password)){
User user = new User();
user.setUsername(username);
user.setPassword(password);
listener.loginSuccess();
}else{
listener.loginFailed();
}
}
}.start();
}
}
依旧是 面向接口编程 的思想,将 Model 层的方法抽离成一个接口,日后彼此交互也都是通过传递接口类型的引用,避免强耦合。
View
我们考虑一下 View 中应该有哪些功能,首先效果图中有两个按钮,login 和 clear。
想要实现 login ,就需要能够提供我们输入的文本,对应如下方法:
String getUserName();
String getPassword();
实现 clear,那么意味着 View 需要有清除 输入文本 的功能,也就是需要如下两个方法:
void clearUserName();
void clearPassword();
同时,我们看到登录时有个 Progressbar 来提示登录过程(毕竟实际上这是个耗时的过程),那么就需要能够显示和隐藏它:
void showLoading();
void hideLoading();
最后,无论我们登录成功与否,都需要有个提示显示我们是否登录成功了:
void toMainActivity();
void showFailedError();
综上,完整的接口定义为:
IUserLoginView
package com.bit.whdalive.demomvp.mvp;
public interface IUserLoginView {
String getUserName();
String getPassword();
void clearUserName();
void clearPassword();
void showLoading();
void hideLoading();
void toMainActivity();
void showFailedError();
}
接下来就是写它的实现类了(实际上就是个纯碎的Activity)
UserLoginActivity
package com.bit.whdalive.demomvp.mvp;
public class UserLoginActivity extends AppCompatActivity implements IUserLoginView {
private EditText mEdtUsername,mEdtPwd;
private Button mBtnLogin,mBtnClear;
private ProgressBar mPbLoading;
private IUserLoginPresenter mIUserLoginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews(){
mIUserLoginPresenter = new UserLoginPresenterImpl(this);
mEdtUsername = findViewById(R.id.input_account);
mEdtPwd = findViewById(R.id.input_password);
mBtnClear = findViewById(R.id.btn_clear);
mBtnLogin = findViewById(R.id.btn_login);
mPbLoading = findViewById(R.id.pb_loading);
mBtnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mIUserLoginPresenter.doLogin();
}
});
mBtnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mIUserLoginPresenter.clear();
}
});
}
@Override
public String getUserName() {
return mEdtUsername.getText().toString();
}
@Override
public String getPassword() {
return mEdtPwd.getText().toString();
}
@Override
public void clearUserName() {
mEdtUsername.setText("");
}
@Override
public void clearPassword() {
mEdtPwd.setText("");
}
@Override
public void showLoading() {
mPbLoading.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
mPbLoading.setVisibility(View.GONE);
}
@Override
public void toMainActivity() {
Toast.makeText(this,"login success, to MainActivity",Toast.LENGTH_SHORT).show();
}
@Override
public void showFailedError() {
Toast.makeText(this,"Login failed",Toast.LENGTH_SHORT).show();
}
}
对于 View 而言,因为其只和 用户的交互 打交道,因此我们只需要考虑好 哪些操作需要改动界面显示?哪些操作需要什么反馈? 并以此来编写对应方法并抽象成接口,而一旦写好了接口,那么实现类就是手到擒来的事情了。
Presenter
Presenter 作为 Model 和 View 的桥梁,需要能够调用 Model 的方法,来执行具体的业务方法;同时需要调用 View 的方法,来更新界面。
在这个 Demo 中,就只有两个功能可言:doLogin 和 clear。
对于 doLogin 实际就是调用了 Model 中的 login方法,clear 则是调用了 View 中的 clearUserName() 和 clearPassword() 来清除文本。
IUserLoginPresenter.java
package com.bit.whdalive.demomvp.mvp;
public interface IUserLoginPresenter {
void doLogin();
void clear();
}
UserLoginPresenterImpl.java
package com.bit.whdalive.demomvp.mvp;
import android.os.Handler;
public class UserLoginPresenterImpl implements IUserLoginPresenter,IUserModel.OnLoginListener {
private IUserLoginView mIUserLoginView;
private IUserModel mIUserModel;
private Handler mHandler = new Handler();
public UserLoginPresenterImpl(IUserLoginView IUserLoginView) {
mIUserLoginView = IUserLoginView;
mIUserModel = new UserModelImpl(this);
}
@Override
public void doLogin() {
String username = mIUserLoginView.getUserName();
String password = mIUserLoginView.getPassword();
mIUserLoginView.showLoading();
mIUserModel.login(username,password,this);
}
@Override
public void loginSuccess() {
mHandler.post(new Runnable() {
@Override
public void run() {
mIUserLoginView.hideLoading();
mIUserLoginView.toMainActivity();
}
});
}
@Override
public void loginFailed() {
mHandler.post(new Runnable() {
@Override
public void run() {
mIUserLoginView.hideLoading();
mIUserLoginView.showFailedError();
}
});
}
@Override
public void clear() {
mIUserLoginView.clearUserName();
mIUserLoginView.clearPassword();
}
}
上述代码中,由于 Model 需要通知 Presenter 是否登陆成功,因此 presenter 实现了 IUserModel.OnLoginListener 接口。
同时由于Presenter 分别和 Model、View 双向通信,因此 Presenter 持有后两者的引用,而 Model和View彼此不持有对方的引用,都只有 Presenter 的引用。
契约类写法
当然如果读者偏爱于 契约类Contracts 的写法,问题也不大:
LoginContract.java
package com.bit.whdalive.demomvp.mvp_contracts;
import com.bit.whdalive.demomvp.bean.User;
public interface LoginContract {
public interface View {
String getUserName();
String getPassword();
void clearUserName();
void clearPassword();
void showLoading();
void hideLoading();
void toMainActivity();
void showFailedError();
}
public interface Presenter {
void login();
void clear();
}
}
之后,在实现类中对实现接口的名字从 IUserLoginPresenter/IUserLoginView 改为 LoginContract.Presenter/LoginContract.View 即可。
小结
最后以 Login 登录功能捋顺一下该demo的执行流程:
- 用户输入账号密码
- 点击Login按钮,View 将该事件传递给 Presenter
- Presenter 接收到 login 请求,从 View 中提取 账号密码文本,并一并交给 Model 执行具体的 login 操作(也就是调用 Model 的login()方法)
- Model 执行 login 操作后,通知 Presenter 是否登录成功
- Presenter 接收到 Model 的反馈,通知 View 更新页面
- View 根据 Presenter 的指令,更改当前页面。
上面我们就通过一步步拆解,逐步讲解如何手撸一个简易的Demo,读者也可以自己找一些简单的场景加以练习。
附上 Demo 地址:
最后,愿本文对大家有所帮助。