今天是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实现前的准备吧。因为文中包含了很多我个人主观的理解,所有有些内容可能讲的不是很正确,欢迎大家指正和给出意见。
如果看官大人们觉得这篇文章还不错或者帮助到了你,希望能给个小小的点赞和关注,你们的鼓励就是我最大的动力啦,下篇文章见~