这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
MVP简介
在传统的模式中,本该作为View层的Activity或者Fragment非常臃肿,既要对数据进行加载和绑定,又要处理网络请求和UI更新。
MVP模式减少了Activity的职责,将复杂的逻辑代码提取到了Presenter中进行处理,模块职责划分明显,层次清晰:
- View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)。
- Model:负责存储、检索、操纵数据。
- Presenter:作为View与Model交互的桥梁,并处理一些复杂的逻辑。
与之对应的好处就是,耦合度更低,更方便的进行测试。
MVP结构图
与MVC的区别
MVC中的M(Model)与MVP中的一致,但是V是指XML文件,C是Activity;MVP中的V则是指Activity,P是presenter类。
此外,MVP相对于MVC的区别是:
- View与Model并不直接交互,而是通过与Presenter交互来与Model间接交互。
- 通常View与Presenter是一对一的关系,不过复杂的View可以绑定多个Presenter。
- Presenter与View的交互是通过接口来进行的,更有利于添加单元测试。
MVP在实现代码逻辑简洁的同时,也额外增加了大量的接口、类,不方便进行管理。我们可以增加一个Contract接口,然后把与M、V、P三层的相关接口列入其中。类似于这种:
public interface Contract {
interface IModel {
}
interface IPresenter {
}
interface IView {
}
}
复制代码
MVP小例子
这里写一个小Demo,就是我们平常很熟悉的登录界面,简化了业务逻辑,更便于理解MVP。
首先是需要确定我们的APP需要什么功能:要有账号密码的输入框,输入之后点击登录按键,点击之后会有进度条的显示,登录完毕之后进度条隐藏,并且会根据登录结果执行相应的操作。
那么就先根据这些需求定义好Contract接口吧(注意:先写Contract接口是为了对后面的编写进行引导,后续编码过程中会不断对该接口进行完善,并不是一开始就完全确定)。
Contract接口
public interface Contract {
interface Model {
void login(String username, String password,
Contract.CallBack callBack);
}
interface View {
String getUsername();
String getPassword();
void showLoading();
void hideLoading();
}
interface Presenter {
void login(String username, String password);
}
interface CallBack {
void error(Throwable throwable);
void success(String string);
void failed(String string);
void completed();
}
}
复制代码
View
从看得见的出发,先来看看View层吧。
在XML文件中定义账号密码输入框、登录按键以及加载进度条:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<EditText
android:id="@+id/et_username"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginTop="168dp"
android:hint="Account:"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_password"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:hint="Password:"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_username" />
<Button
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_password" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
复制代码
Activity中提供的账号密码的get方法、进度条的显示隐藏、以及登录按键的监听:
public class LoginActivity extends Activity
implements Contract.View, View.OnClickListener {
private Contract.Presenter mPresenter;
private ProgressBar mProgressBar;
private Button btnLogin;
private EditText etUsername;
private EditText etPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
public void initView() {
etUsername = findViewById(R.id.et_username);
etPassword = findViewById(R.id.et_password);
btnLogin = findViewById(R.id.btn_login);
mProgressBar = findViewById(R.id.progressBar);
btnLogin.setOnClickListener(this);
mPresenter = new LoginPresenter(this);
}
@Override
public String getUsername() {
return etUsername.getText().toString().trim();
}
@Override
public String getPassword() {
return etPassword.getText().toString().trim();
}
@Override
public void showLoading() {
mProgressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
mProgressBar.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_login) {
mPresenter.login(getUsername(), getPassword());
}
}
}
复制代码
Presenter
点击登录后,Presenter会从View层拿到账号密码,进行基本的逻辑判断。满足基本条件就调用Model的登录方法,并在方法参数中传入回调接口:
public class LoginPresenter implements Contract.Presenter {
private static final String TAG = LoginPresenter.class.getSimpleName();
private final Contract.View view;
private final Contract.Model model;
public LoginPresenter(Contract.View view) {
this.view = view;
model = new LoginModel();
}
@Override
public void login(String username, String password) {
Log.d(TAG, "login: ");
//进行一些异常状态判断,比如格式出错或者处于连续输错的等待时间...
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText((LoginActivity) view,
"帐号密码不能为空", Toast.LENGTH_SHORT).show();
return;
}
//如果没问题则将数据传给Model进行登录
view.showLoading();//显示加载动画,等待登录完成的回调
model.login(username, password, new Contract.CallBack() {
@Override
public void error(Throwable throwable) {
Toast.makeText((Activity) view,
throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void success(String string) {
Toast.makeText((Activity) view, string, Toast.LENGTH_SHORT).show();
}
@Override
public void failed(String string) {
Toast.makeText((Activity) view, string, Toast.LENGTH_SHORT).show();
}
@Override
public void completed() {
//登录完成(不管成功与失败,都要隐藏加载动画)
view.hideLoading();
}
});
}
}
复制代码
Model
Model会进行网络申请来验证密码,并根据结果调用登录成功、密码错误、网络错误、登录异常等回调接口:
public class LoginModel implements Contract.Model{
public LoginModel(){
}
@Override
public void login(String username, String password, Contract.CallBack callBack) {
//一般来讲Model会进行网络请求,在服务器端验证账户密码的正确性
//此demo使用一秒延时来模拟该操作
new Handler().postDelayed(() -> {
//网络异常时调用error回调方法(demo未涉及到网络,仅供参考)
//callBack.error(new Throwable("出了一点状况..."));
//网络连接正常,只有账号密码都为123456时通过验证
if (username.equals("123456") && password.equals("123456")){
callBack.success("登录成功");
}else {
callBack.failed("账号或密码错误");
}
callBack.completed();
}, 2000);
}
}
复制代码
简陋的 界面
点击登录,进度条加载后显示结果:
总结
关于MVP的理解就暂时到这里了,有机会让我们继续深入探索。一个良好的框架能大幅度地提高我们的开发效率。如此同时,我们也不经想到:在Android开发中,还有没有更优的架构等着我们发掘或使用。