【从 0 开始开发一款直播 APP】5.3 MVC 完全解析 -
本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
视频地址:http://www.cniao5.com/course/10121
【从 0 开始开发一款直播 APP】5.1 MVP 完全解析 -- 实现直播登录
【从 0 开始开发一款直播 APP】5.2 MVP 之 Fragment 交互实现滑动导航
【从 0 开始开发一款直播 APP】5.3 MVC 完全解析 -- 实现直播登录
【从 0 开始开发一款直播 APP】5.4 MVC 之 Fragment 交互实现滑动导航
MVC(Model-View-Controller,模型-视图-控制器)模式是 80 年代 Smalltalk-80 出现的一种软件设计模式,后来得到了广泛的应用,其主要目的在于促进应用中模型,视图,控制器间的关注的清晰分离。
Model — 业务逻辑和数据模型。
Model 层主要负责
1、从网络,数据库,文件,传感器,第三方等数据源读写数据。
2、对外部的数据类型进行解析转换为 APP 内部数据交由上层处理。
3、对数据的临时存储,管理,协调上层数据请求。
View — 显示数据
View 层主要负责
1、提供 UI 交
2、在 Controller 的控制下修改 UI
3、将业务事件交由 Controller 处理
4、与 Model 进行交互,将 Model 中的数据和数据状呈现给用户
Controller — View 和 Model 间的交互
Controller 层主要负责
1、接收 View 的操作事件,调用 Model 的接口进行数据操作
2、Model 数据更新直接通知 View 更改,不用经过 Controller
MVC 优点
1、耦合性低。利用 MVC 框架使得View(视图)层和Model(模型)层可以很好的分离,这样就达到了解耦的目的,所以耦合性低,减少模块代码之间的相互影响
2、便于进行单元开发,单元测试
3、可扩展性好。由于耦合性低,添加需求,扩展代码易于修改。
4、模块职责划分明确。主要划分层 M,V,C 三个模块,利于代码的维护
MVC 缺点
1、Activity 中有很多关于视图 UI 的显示代码,因此 View 视图和 Activity 控制器并不是完全分离的,当activity 类业务过多的时候,会变得难以管理和维护。
2、当 UI 的状态数据,跟持久化的数据混杂在一起,变得极为混乱。
MVC 的使用
MVC 的逻辑实现类图,根据 MVC 模型,定义 C 层 和 V 层接口以及基本方法以及 M 层相关方法。
使用 MVC 大致要做以下步骤
1、创建 IController 接口,把所有业务逻辑接口都定义在内,并创建它的实现类 ControllerImpl。IController 持有 IView 的引用,调用 IView 中的方法,IController 持有 Model 的引用,可对数据进行处理。
2、创建 IView接口,把所有视图逻辑的接口都定义在内,创建其实现类 Activity / Fragment。
3、在上图中可以看到,Activity 关联了 IController,而 ControllerImpl 里包含了一个 IView 并依赖于 Model。Activity 又关联了 Model,通过 Activity 更新 Model 数据,IController 中的 Model 数据应 同步更新。
MVC 实现登录
Controller — MVCLoginController
1、定义 MVCILoginController 抽象类,定义登录所需逻辑处理方法,与 UserInfo 关联。
2、定义 MVCLoginController 继承 MVCILoginController 抽象类,实现抽象方法以及处理逻辑,与 MVCLoginView 关联。这就满足了 MVC 架构中 Controller 同时与 View 和 Model 交互的原则。
View — MVCLoginActivity
1、定义 MVCBaseView 接口,将通用方法封装到里面。
2、定义 MVCILoginView 接口,定义登录需要的通用方法。
3、创建 MVCLoginView 实现 MVCILoginView 接口,并与 UserInfo 和 MVCLoginController 关联。这里就满足了 MVC 中的 View 同时与 Controller 和 UserInfo 之间的交互。
Model — UserInfo
根据登录请求数据定义 Model 所需字段以及 set() 和 get() 方法,注意:Model 要实现序列化接口。
根据相关类图创建包和类。
对登录不了解的请查看 【从 0 开始开发一款直播 APP】4.4 网络封装之 OkHttp -- 网络请求实现直播登录
在后面的代码中会涉及到网络的状态判断,倒计时器的加载,弱引用,ACache 缓存等,有兴趣的可以看看。
【从 0 开始开发一款直播 APP】6 缓存 ACache 源码解析
【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析
【从 0 开始开发一款直播 APP】8 弱引用 WeakReference
【从 0 开始开发一款直播 APP】9 网络连接状态详解
Controller 所有类实现
1、MVCILoginController
public abstract class MVCILoginController {
protected UserInfo mUserInfo;
public MVCILoginController(UserInfo userInfo) {
mUserInfo = userInfo;
}
/**
* 检查手机号验证码是否合法
* @param phone
* @param verifyCode
* @return
*/
public abstract boolean checkPhoneLogin(String phone,String verifyCode);
/**
* 检查用户名密码是否合法
* @param userName
* @param password
* @return
*/
public abstract boolean checkUserNameLogin(String userName,String password);
/**
* 手机号登录
* @param phone
* @param verifyCode
*/
public abstract void phoneLogin(String phone,String verifyCode);
/**
* 用户名登录
* @param userName
* @param password
*/
public abstract void userNameLogin(String userName,String password);
/**
* 发送验证码
* @param phoneNum
*/
public abstract void sendVerifyCode(String phoneNum);
2、MVCLoginController
MVCLoginController 继承 MVCILoginController 抽象类,实现其方法和构造函数。
public class MVCLoginController extends MVCILoginController {
private MVCLoginView mLoginView;
public MVCLoginController(UserInfo userInfo,MVCLoginView loginView) {
super(userInfo);
mLoginView = loginView;
}
@Override
public boolean checkPhoneLogin(String phone, String verifyCode) {
if (OtherUtils.isPhoneNumValid(phone)) {
if (OtherUtils.isVerifyCodeValid(verifyCode)) {
if (OtherUtils.checkNetWorkState(mLoginView.getContext())) {
return true;
} else {
mLoginView.showMsg("网络未连接");
}
} else {
mLoginView.verifyCodeError("验证码错误!");
}
} else {
mLoginView.phoneError("手机格式错误!");
}
mLoginView.dismissLoading();
return false;
}
@Override
public boolean checkUserNameLogin(String userName, String password) {
if (OtherUtils.isUsernameVaild(userName)) {
if (OtherUtils.isPasswordValid(password)) {
if (OtherUtils.checkNetWorkState(mLoginView.getContext())) {
return true;
} else {
mLoginView.showMsg("当前无网络连接!");
}
} else {
mLoginView.passwordError("密码过短!");
}
} else {
mLoginView.usernameError("用户名不符合规范!");
}
mLoginView.dismissLoading();
return false;
}
@Override
public void phoneLogin(final String phone, String verifyCode) {
if (checkPhoneLogin(phone, verifyCode)) {
final PhoneLoginRequest request = new PhoneLoginRequest(RequestComm.loginPhone, phone, verifyCode);
AsyncHttp.instance().postJson(request, new AsyncHttp.IHttpListener() {
@Override
public void onStart(int requestId) {
mLoginView.showLoading();
}
@Override
public void onSuccess(int requestId, Response response) {
if (response.getStatus() == RequestComm.SUCCESS) {
ACache.get(mLoginView.getContext()).put(CacheConstants.LOGIN_PHONE, phone);
mLoginView.loginSuccess();
} else {
mLoginView.loginFailed(response.getStatus(), response.getMsg());
}
mLoginView.dismissLoading();
}
@Override
public void onFailure(int requestId, int httpStatus, Throwable error) {
mLoginView.verifyCodeFailed("网络异常");
mLoginView.dismissLoading();
}
});
}
}
@Override
public void userNameLogin(final String userName, final String password) {
if (checkUserNameLogin(userName, password)) {
LoginRequest request = new LoginRequest(RequestComm.loginUsername, userName, password);
AsyncHttp.instance().postJson(request, new AsyncHttp.IHttpListener() {
@Override
public void onStart(int requestId) {
mLoginView.showLoading();
}
@Override
public void onSuccess(int requestId, Response response) {
if (response.getStatus() == RequestComm.SUCCESS) {
UserInfo info = (UserInfo) response.getData();
MVCUSerInfoCache.saveCache(mLoginView.getContext(), info);
ACache.get(mLoginView.getContext()).getAsString(CacheConstants.LOGIN_USERNAME);
ACache.get(mLoginView.getContext()).put(CacheConstants.LOGIN_USERNAME, userName);
ACache.get(mLoginView.getContext()).put(CacheConstants.LOGIN_PASSWORD, password);
mLoginView.loginSuccess();
} else {
mLoginView.loginFailed(response.getStatus(), response.getMsg());
mLoginView.dismissLoading();
}
}
@Override
public void onFailure(int requestId, int httpStatus, Throwable error) {
mLoginView.loginFailed(httpStatus, error.getMessage());
mLoginView.dismissLoading();
}
});
}
}
@Override
public void sendVerifyCode(String phoneNum) {
if (OtherUtils.isPhoneNumValid(phoneNum)) {
if (OtherUtils.checkNetWorkState(mLoginView.getContext())) {
VerifyCodeRequest request = new VerifyCodeRequest(RequestComm.verifyCodeRequestId, phoneNum);
AsyncHttp.instance().postJson(request, new AsyncHttp.IHttpListener() {
@Override
public void onStart(int requestId) {
mLoginView.showLoading();
}
@Override
public void onSuccess(int requestId, Response response) {
if (response.getStatus() == RequestComm.SUCCESS) {
UserInfo info = (UserInfo) response.getData();
if (null != mLoginView){
mLoginView.verifyCodeSuccess(60,60);
}
}else {
mLoginView.verifyCodeFailed("获取后台验证码失败!");
}
mLoginView.dismissLoading();
}
@Override
public void onFailure(int requestId, int httpStatus, Throwable error) {
if (null != mLoginView){
mLoginView.verifyCodeFailed("获取后台验证码失败!");
}
mLoginView.dismissLoading();
}
});
} else {
mLoginView.showMsg("当前无网络连接!");
}
} else {
mLoginView.phoneError("手机号码不符合规范!");
}
}
}
View 所有类实现
1、MVCBaseView
View 的通用方法封装。
public interface MVCBaseView {
/**
* 数据加载或耗时加载时界面显示
*/
void showLoading();
/**
* 数据加载或耗时加载完成时界面显示
*/
void dismissLoading();
/**
* 消息提示,如 Toast,Dialog等
*/
void showMsg(String msg);
void showMsg(int msgId);
/**
* 获取Context
* @return
*/
Context getContext();
}
2、MVCILoginView
MVCILoginView 登录逻辑方法封装
public interface MVCILoginView extends MVCBaseView {
/**
* 登录成功
*/
void loginSuccess();
/**
* 登录失败
* @param status
* @param msg
*/
void loginFailed(int status, String msg);
/**
* 用户名错误
* @param errorMsg
*/
void usernameError(String errorMsg);
/**
*手机号错误
* @param errorMsg
*/
void phoneError(String errorMsg);
/**
* 密码错误
* @param errorMsg
*/
void passwordError(String errorMsg);
/**
* 验证码错误
* @param errorMsg
*/
void verifyCodeError(String errorMsg);
/**
* 验证失败
* @param errorMsg
*/
void verifyCodeFailed(String errorMsg);
/**
* 验证成功
* @param reaskDuration
* @param expireDuration
*/
void verifyCodeSuccess(int reaskDuration, int expireDuration);
}
3、MVCLoginView
绑定 Model 和 Controller,实现 M,V,C 三层交互
public abstract class MVCLoginView extends BaseActivity implements MVCILoginView {
protected UserInfo mUserInfo;
protected MVCLoginController mController;
public void setModel(UserInfo info){
if (mUserInfo == null){
throw new NullPointerException("no model");
}
this.mUserInfo = info;
}
}
4、MVCLoginActivity
MVCLoginActivity 继承 MVCLoginView,实现登录 UI 界面加载相关逻辑。
public class MVCLoginActivity extends MVCLoginView implements View.OnClickListener {
//共用控件
private ProgressBar progressBar;
private EditText etPassword;
private EditText etLogin;
private Button btnLogin;
private Button btnPhoneLogin;
private TextInputLayout tilLogin, tilPassword;
private Button btnRegister;
//手机验证登陆控件
private TextView tvVerifyCode;
private boolean isPhoneLogin = false;
@Override
public void loginSuccess() {
dismissLoading();
invoke(this, MainActivity.class);
finish();
}
@Override
public void loginFailed(int status, String msg) {
dismissLoading();
showMsg(msg);
}
@Override
public void usernameError(String errorMsg) {
etLogin.setError(errorMsg);
}
@Override
public void phoneError(String errorMsg) {
etLogin.setError(errorMsg);
}
@Override
public void passwordError(String errorMsg) {
etPassword.setError(errorMsg);
}
@Override
public void verifyCodeError(String errorMsg) {
showMsg(errorMsg);
}
@Override
public void verifyCodeFailed(String errorMsg) {
showMsg(errorMsg);
}
@Override
public void verifyCodeSuccess(int reaskDuration, int expireDuration) {
showMsg("注册短信下发,验证码 " + expireDuration / 60 + " 分钟内有效!");
OtherUtils.startTimer(new WeakReference<TextView>(tvVerifyCode), "验证码", reaskDuration, 1);
}
@Override
public void showLoading() {
showOnLoading(true);
}
@Override
public void dismissLoading() {
showOnLoading(false);
}
@Override
public void showMsg(String msg) {
showToast(msg);
}
@Override
public void showMsg(int msgId) {
showToast(msgId);
}
@Override
public Context getContext() {
return this;
}
@Override
protected void setActionBar() {
}
@Override
protected void setListener() {
}
/**
* 手机登录和用户名登录界面显示或隐藏
* @param active
*/
public void showOnLoading(boolean active) {
if (active) {
progressBar.setVisibility(View.VISIBLE);
btnLogin.setVisibility(View.INVISIBLE);
etLogin.setEnabled(false);
etPassword.setEnabled(false);
btnPhoneLogin.setClickable(false);
btnRegister.setTextColor(getResources().getColor(R.color.colorTransparentGray));
btnPhoneLogin.setTextColor(getResources().getColor(R.color.colorTransparentGray));
btnRegister.setClickable(false);
} else {
progressBar.setVisibility(View.GONE);
btnLogin.setVisibility(View.VISIBLE);
etLogin.setEnabled(true);
etPassword.setEnabled(true);
btnPhoneLogin.setClickable(true);
btnRegister.setClickable(true);
btnRegister.setTextColor(getResources().getColor(R.color.white));
btnPhoneLogin.setTextColor(getResources().getColor(R.color.white));
}
}
@Override
protected void initData() {
//缓存中读取用户名和密码
etLogin.setText(ACache.get(this).getAsString(CacheConstants.LOGIN_USERNAME));
etPassword.setText(ACache.get(this).getAsString(CacheConstants.LOGIN_PASSWORD));
}
@Override
protected void initView() {
//MVCLoginController 初始化
mController = new MVCLoginController(mUserInfo,this);
etLogin = obtainView(R.id.et_username);
etPassword = obtainView(R.id.et_password);
btnRegister = obtainView(R.id.btn_register);
btnPhoneLogin = obtainView(R.id.btn_phone_login);
btnLogin = obtainView(R.id.btn_login);
progressBar = obtainView(R.id.progressbar);
tilLogin = obtainView(til_login);
tilPassword = obtainView(R.id.til_password);
tvVerifyCode = obtainView(R.id.btn_verify_code);
userNameLoginViewInit();
}
/**
* 用户名密码登录界面init
*/
public void userNameLoginViewInit() {
//用户名登录切换
userLoginTrans();
tvVerifyCode.setOnClickListener(this);
//注册
btnRegister.setOnClickListener(this);
//手机号登录
btnPhoneLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//手机号登录
phoneLoginViewinit();
}
});
//用户名登录
btnLogin.setOnClickListener(this);
}
public void phoneLoginViewinit() {
phoneLoginTrans();
btnPhoneLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//转换为用户名登录界面
userNameLoginViewInit();
}
});
btnLogin.setOnClickListener(this);
btnRegister.setOnClickListener(this);
tvVerifyCode.setOnClickListener(this);
}
private void phoneLoginTrans() {
isPhoneLogin = true;
tvVerifyCode.setVisibility(View.VISIBLE);
AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
alphaAnimation.setDuration(250);
tvVerifyCode.setAnimation(alphaAnimation);
//设定点击优先级于最前(避免被EditText遮挡的情况)
tvVerifyCode.bringToFront();
//设置输入框输入类型为 手机号
etLogin.setInputType(EditorInfo.TYPE_CLASS_PHONE);
etLogin.setText("");
etPassword.setText("");
//手机号登录按钮文字改为 用户名登录
btnPhoneLogin.setText("用户名登录");
tilLogin.setHint("手机号");
tilPassword.setHint("密码");
}
private void userLoginTrans() {
isPhoneLogin = false;
tvVerifyCode.setVisibility(View.GONE);
AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
alphaAnimation.setDuration(250);
tvVerifyCode.setAnimation(alphaAnimation);
etLogin.setInputType(EditorInfo.TYPE_CLASS_TEXT);
etLogin.setText("");
etPassword.setText("");
btnPhoneLogin.setText("手机号登录");
tilLogin.setHint("用户名");
tilPassword.setHint("密码");
}
@Override
protected int getLayoutId() {
return R.layout.activity_mvclogin;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_login:
if (isPhoneLogin) {
mController.phoneLogin(etLogin.getText().toString(), etPassword.getText().toString());
} else {
System.out.println("-----------------"+mController);
mController.userNameLogin(etLogin.getText().toString(), etPassword.getText().toString());
}
break;
case R.id.btn_verify_code:
mController.sendVerifyCode(etLogin.getText().toString());
break;
case R.id.btn_register:
break;
}
}
}
Model 类
Model 数据和 MVP 章节数据完全一样,包括 UserInfo 和 MVCUSerInfoCache 两个类,这里不再贴出。
登录实现运行效果
参考:
http://hammercui.github.io/post/android%E5%9F%BA%E7%A1%80%EF%BC%9A%E6%B5%85%E6%9E%90mvc%E4%B8%8Emvp/
140套Android优秀开源项目源码,领取地址:http://mp.weixin.qq.com/s/afPGHqfdiApALZqHsXbw-A