【从 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容