Android架构设计——MVP | 8月更文挑战

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

MVP简介

在传统的模式中,本该作为View层的Activity或者Fragment非常臃肿,既要对数据进行加载和绑定,又要处理网络请求和UI更新。

MVP模式减少了Activity的职责,将复杂的逻辑代码提取到了Presenter中进行处理,模块职责划分明显,层次清晰:

  • View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)。
  • Model:负责存储、检索、操纵数据。
  • Presenter:作为View与Model交互的桥梁,并处理一些复杂的逻辑。

与之对应的好处就是,耦合度更低,更方便的进行测试。

MVP结构图

MVP结构.png

与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);
    }
}
复制代码

简陋的 界面

Snipaste_2021-08-12_19-55-18.png

点击登录,进度条加载后显示结果:

Snipaste_2021-08-12_19-53-42.png

总结

关于MVP的理解就暂时到这里了,有机会让我们继续深入探索。一个良好的框架能大幅度地提高我们的开发效率。如此同时,我们也不经想到:在Android开发中,还有没有更优的架构等着我们发掘或使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享