今天是 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 的事件处理. 其中主要包含两个方法:
void showCode()
点击获取验证码按钮时调用, 因为是测试环境, 所以直接生成 6 位随机数作为验证码并显示出来, 同时传入全局变量 mRandomCode 中以作登录校验用.
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 实现前的准备吧. 因为文中包含了很多我个人主观的理解, 所有有些内容可能讲的不是很正确, 欢迎大家指正和给出意见.
如果看官大人们觉得这篇文章还不错或者帮助到了你, 希望能给个小小的点赞和关注, 你们的鼓励就是我最大的动力啦, 下篇文章见~
来源: https://juejin.im/post/5c2772536fb9a049be5d92dd