这是一篇面向Android初学者抛砖引玉的文章,正如以前的我——写代码只考虑如何实现功能,对于设计模式完全没有想法和认知。在这篇文章中,我会通过一个常用的登录场景,从几十行代码的直接实现,一步步构建出入门级的MVP架构,向你们分享我所理解的代码的流畅性。但限于文章长度,本篇先对实现MVP前我认为需要了解的一些代码优化内容做介绍,比如为什么要用到接口,以及代码的流畅性等。
当然,书读千遍不如行万里路,真正地理解,一定是在自己不断敲代码的过程中获得的。这是我切身感受到的,也推荐如果是刚入门的你这样去做:先按照网上的示例去“模仿”实现,在做过多次后,那些理念性的优缺点自然就能感受并理解了。
这次使用一个常用的手机号+验证码的登录场景作为示例,看一下效果图吧:
image
首先在不使用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行。这么一看,还算的上优化代码吗? 其实虽然代码量增加了,但类中的许多方法变得更加精简,每个方法负责的任务变少了,这也是编程思想中重要的”单一职责原则“的体现:每一个方法只执行它相应的职责,如果有超出它职责范围的内容,交由其他方法去做就好了。
作者:广州芦苇科技App
链接:https://www.jianshu.com/p/1b3f519f13ac