Android MVP模式从入门到进门(一)

今天是2018年最后一个工作日了,提前祝大家新年快乐啦~~~

这是一篇面向Android初学者抛砖引玉的文章,正如以前的我——写代码只考虑如何实现功能,对于设计模式完全没有想法和认知。在这篇文章中,我会通过一个常用的登录场景,从几十行代码的直接实现,一步步构建出入门级的MVP架构,向你们分享我所理解的代码的流畅性。但限于文章长度,本篇先对实现MVP前我认为需要了解的一些代码优化内容做介绍,比如为什么要用到接口,以及代码的流畅性等。

当然,书读千遍不如行万里路,真正地理解,一定是在自己不断敲代码的过程中获得的。这是我切身感受到的,也推荐如果是刚入门的你这样去做:先按照网上的示例去“模仿”实现,在做过多次后,那些理念性的优缺点自然就能感受并理解了。

这次使用一个常用的手机号+验证码的登录场景作为示例,看一下效果图吧:

首先在不使用MVC或者MVP等设计模式的情况下,看下如何手撸出上面的效果:

public class LoginAcitvity extends AppCompatActivity {

    @BindView(R.id.et_phone)
    EditText mEtPhone;
    @BindView(R.id.et_code)
    EditText mEtCode;
    @BindView(R.id.pb_loading)
    ProgressBar mPbLoading;
    private String mRandomCode;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ButterKnife.bind(this);
    }

    /**
     * 点击获取验证码 生成6位随机数并显示
     */
    private void showCode() {
        //创建随机验证码
        Random random = new Random();
        StringBuilder rCode = new StringBuilder();
        int codeMaxLength = 6;
        for (int i = 0; i < codeMaxLength; i++) {
            rCode.append(random.nextInt(10));
        }
        mRandomCode = rCode.toString();
        //将创建的验证码显示出来
        Toast.makeText(this, "验证码:" + mRandomCode, Toast.LENGTH_SHORT).show();
    }

    /**
     * 验证登录
     */
    private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        //用ProgressBar作为Loading控件,在验证登录前显示
        mPbLoading.setVisibility(View.VISIBLE);
        //用handler的延迟操作模拟网络效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //无论登录成功与否,都关掉loading控件的显示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录失败" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
    }

    @OnClick({R.id.btn_code, R.id.btn_login})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_code:
                showCode();
                break;
            case R.id.btn_login:
                login();
                break;
            default:
        }
    }
}
复制代码

代码中使用了 Butterknife 代替 findViewById 实现对 View 的绑定和 Click 的事件处理。 其中主要包含两个方法:

  1. void showCode()

    点击获取验证码按钮时调用,因为是测试环境,所以直接生成6位随机数作为验证码并显示出来,同时传入全局变量mRandomCode中以作登录校验用。

  2. void login()

    点击登录按钮时调用,校验输入的手机号和验证码,通过handler的delay操作延迟1秒模拟网络环境。在校验前显示loading控件,返回结果后隐藏。

哒哒~只用了几十行代码就完整实现了图中的功能,并且还没出现bug呢。不过代码作为新时代的艺术,我们自然是不能就此满足了,还有很多优化之路要走。

可能有同学就会问了:“ 这样写不是挺好的吗,一个Activity里就写完所有逻辑了,很方便直接啊。”

确实是,在处理一些简单任务的时候,一行行堆砌代码的确来的快捷简便。但如果代码堆叠得多了,Activity就会变得特别臃肿,我们看一下在上面这个简单的例子中,Activity负责了哪些行为:

  • 对各种控件进行绑定和控制
  • 获取用户的输入、点击事件
  • 向服务器发送获取验证码的请求(因为是模拟登录,所以只是创建随机验证码并显示给用户以模拟这一步骤)
  • 向服务器发送手机号和验证码,获取验证结果(也是模拟验证)
  • 将结果在页面上显示出来告知用户
  • 管理自身相关生命周期的事务、例如在退出时关闭网络连接等(因为是模拟没有实际网络连接,所以代码中没有体现)

将这些行为按照如下规则分类:

  • 跟界面相关,负责处理各种界面操作
    • 控制控件
    • 获取事件
    • 生命周期
    • 显示结果
  • 跟界面无关,负责处理业务的逻辑
    • 向服务器获取验证码
    • 向服务器验证登录

可以发现,如果按照责任划分,出现了以界面处理和业务处理两种类型的代码行为。那么这是否可以作为我们优化代码流畅性的一个参考标准呢?如果将代码按照上面的分类进行改写,会有怎样的效果?

我们回看上面void login()部分的代码:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
    	//用ProgressBar作为Loading控件,在验证登录前显示
        mPbLoading.setVisibility(View.VISIBLE);
        //用handler的延迟操作模拟网络效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //无论登录成功与否,都关掉loading控件的显示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录失败" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
    }
复制代码

可以发现其中包含“获取输入”、“控制Loading控件”、”验证登录“以及”显示结果”四个任务,也就是既有对界面的操控,又对服务器进行数据处理。我们试着把这两者分开看一下:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

public void verifyLogin(String phone, String code){
    	//用ProgressBar作为Loading控件,在验证登录前显示
        mPbLoading.setVisibility(View.VISIBLE);
    	//用handler的延迟操作模拟网络效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                //无论登录成功与否,都关掉loading控件的显示
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录成功" , Toast.LENGTH_SHORT).show();
            } else {
                mPbLoading.setVisibility(View.INVISIBLE);
                Toast.makeText(this, "登录失败" , Toast.LENGTH_SHORT).show();
            }
        }, 1000);
}
复制代码

将验证登录这一部分独立出来后,发现其方法里还是有很多代码,我们再将其按照责任分离一下,达到下面这种效果:

private void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

public void verifyLogin(String phone, String code){
    	showLoading();
    	//用handler的延迟操作模拟网络效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                onLoginSuccess();
            } else {
                onLoginFail();
            }
        }, 1000);
}

public void showMessage(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

public void hideLoading() {
        mPbLoading.setVisibility(View.INVISIBLE);
    }

public void onLoginSuccess() {
        hideLoading();
        showMessage("登录成功");
    }

public void onLoginFail() {
        hideLoading();
        showMessage("登录失败");
    }
复制代码

怎样,是不是感觉代码变得“好看”了许多。虽然从一个方法,分而变成了很多个,但我们主要的目的还是按照“界面 - 业务”进行分类,其他void showLoading()void hideLoading()等方法都是为了更方便在代码中复用而创建的。

说到代码复用,我想到了今年下半年··emm 不开花先。

我们可以思考一下什么方法是比较通用的,在我看来有以下三个:

  • void showMessage(String msg) 很多地方需要显示消息,文中使用了常用的Toast。
  • void showLoading() 需要耗时操作的业务一般都会有Loading控件
  • void hideLoading() 有显示自然就有隐藏

那么对于这些通用的方法,自然而然我们引入到了接口(Interface)的概念,既然每个Activity都有很大可能用到这些方法,那我们可以声明一个接口,让需要用到Activity实现这个接口吧:

public interface IBaseActivity {
    /**
     * 显示Loading
     */
    void showLoading();

    /**
     * 关闭Loading
     */
    void hideLoading();

    /**
     * 显示消息
     * @param msg
     */
    void showMessage(String msg);

}
复制代码

其实我看过很多介绍MVP的文章,里面都有继承和实验接口的操作,但往往不会介绍太清楚。如果你像我一样对JAVA基础不牢固,在还不甚了解接口这部分知识的时候去阅读这些文章,很容易会不明其所以然。所以我推荐你如果不太了解接口和继承的知识,可以先去阅读一下相关概念。

此处运用接口的意义在于将通用的方法独立出来,以供需要它的类直接实现和重写该方法,我将这种接口叫做通用接口。

但是通用的接口,往往实现的方法不多,如果我想再多实现一些方法呢?我们做个极端一点的例子,将上面LoginActivity中所有方法都写成接口的形式,代码的效果是这样的(接下来的代码都去掉了注释以缩短文章长度):

public interface InterfaceBase {
    
    void showLoading();

    void hideLoading();

    void showMessage(String msg);

    void sentCode();

    void login();

    void verifyLogin();

    void onLoginSuccess();

    void onLoginFail();
}
复制代码

这样一来,我们直接实现这个接口,就可以省得再去Activity中创建这些方法了。而实际开发中也确实是这样,因为能直观地在接口中看到所有的方法,所以我们会在创建Activity前先创建接口,声明需要实现的一些方法,然后在Activity中实现接口就可以了。

对于这种专司其职的接口,我将其称为专用接口。

那既然已经有了专用接口,前面提到的通用接口还有什么用处呢?一个类只能继承一个接口,我们肯定选择继承功能强大的专用接口,而不是方法少、功能单一的通用接口啊。可以看到,上面提供的专用接口中,仍然包含了void showLoading();void hideLoading();void showMessage();这三个通用方法,如果每次创建专用接口都添加这三个方法,肯定不是聪明的选择。于是接口的继承就派上用场了——每次创建专用接口时继承通用接口,这样就可以更方便地实现所有方法了。

但是,前面说到将所有方法都在接口中声明出来,是比较极端的方式,一般是不会这样去写接口的。其实接口的定义很多,我只是按我理解的方式去设计它而已。

在这里我只保留了其中关于登录结果回调的方法,至于原因会在接下来MVP相关内容时讲到。下面是继承了通用接口只保留登录结果回调的接口代码:

public interface InterfaceLogin extends InterfaceBase {
    
    void onLoginSuccess();

    void onLoginFail();
}
复制代码

于是对各种方法进行细分、实现继承完的接口后,我们的Activity就变成了这样:

public class LoginActivity extends AppCompatActivity implements ILoginActivity {

    @BindView(R.id.et_phone)
    EditText mEtPhone;
    @BindView(R.id.et_code)
    EditText mEtCode;
    @BindView(R.id.pb_loading)
    ProgressBar mPbLoading;
    //生成的随机6位数验证码
    private String mRandomCode;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ButterKnife.bind(this);
    }

    @Override
    public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        mPbLoading.setVisibility(View.INVISIBLE);
    }

    @Override
    public void showMessage(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }


    public void sentCode() {
        //生成验证码
        mRandomCode = generateCode();
        //将创建的验证码显示出来
        showCode();
    }


    private String generateCode() {
        Random random = new Random();
        StringBuilder rCode = new StringBuilder();
        int codeMaxLength = 6;
        for (int i = 0; i < codeMaxLength; i++) {
            rCode.append(random.nextInt(10));
        }
        return rCode.toString();
    }

    private void showCode() {
        Toast.makeText(this, "验证码:" + mRandomCode, Toast.LENGTH_SHORT).show();

    }

    public void login() {
        String phone = mEtPhone.getText().toString();
        String code = mEtCode.getText().toString();
        verifyLogin(phone, code);
    }

    public void verifyLogin(String phone, String code) {
        showLoading();
        //用handler的延迟操作模拟网络效果
        new Handler().postDelayed(() -> {
            if (!TextUtils.isEmpty(phone) && code.equals(mRandomCode)) {
                onLoginSuccess();
            } else {
                onLoginFail();
            }
        }, 1000);
    }

    @Override
    public void onLoginSuccess() {
        hideLoading();
        showMessage("登录成功");
    }

    @Override
    public void onLoginFail() {
        hideLoading();
        showMessage("登录失败");
    }

    @OnClick({R.id.btn_code, R.id.btn_login})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.btn_code:
                sentCode();
                break;
            case R.id.btn_login:
                login();
                break;
            default:
        }
    }
}
复制代码

到这里我们就将一个很简单几十行代码的Activity,变成了拥有接口的Acitivity,并且代码量翻倍到了100行。这么一看,还算的上优化代码吗? 其实虽然代码量增加了,但类中的许多方法变得更加精简,每个方法负责的任务变少了,这也是编程思想中重要的”单一职责原则“的体现:每一个方法只执行它相应的职责,如果有超出它职责范围的内容,交由其他方法去做就好了。

这样对代码一番优化下来,整体的阅读性增加了,在需求变动的时候也更方便改动代码了。但是到此我们的优化之路只走了一半,还剩下的内容,便是MVP了。

限于文章长度,MVP的内容放到下一篇文章再去详细阐述,这一篇文章就当作MVP实现前的准备吧。因为文中包含了很多我个人主观的理解,所有有些内容可能讲的不是很正确,欢迎大家指正和给出意见。

如果看官大人们觉得这篇文章还不错或者帮助到了你,希望能给个小小的点赞和关注,你们的鼓励就是我最大的动力啦,下篇文章见~

猜你喜欢

转载自juejin.im/post/5c2772536fb9a049be5d92dd