MVVM + RxAndroid + RxView + DataBinding + LiveData + LiveEventBus + Retrofit

前言

本来想记录一下最近相机相关的知识点的,但发现需要时间整理一下,那这里就介绍一下最近写的直播app中使用的整体架构吧。

由于之前项目大多是用MVC,MVP的整体架构,所以这次一个人写直播项目时就干脆用MVVM进行开发(sunflower的架构让我很馋)

简介

最后现阶段是 基于 MVVM

  • UI: AndroidX + DataBinding + RxView + Bravh
  • 数据传递: LiveData + LiveEventBus
  • 网络请求: Retrofit + RxAndroid + OkHttp3
 // 分包工具
    implementation deps.support.multidex
    // androidX
    implementation deps.androidX.appcompat
    implementation deps.androidX.recyclerview
    implementation deps.androidX.constraintLayout
    implementation deps.androidX.lifecycle
    implementation deps.androidX.palette
    // material
    implementation deps.material.runtime
//    implementation deps.support.design
//    implementation deps.support.recyclerview
    // 腾讯直播SDK
    implementation deps.liteavSdk.liteavsdk_smart
    // 自定义采集控件
    implementation deps.liveKit.runtime
    // OkHttp3 + OkHttp3拦截器 腾讯云需要
    implementation deps.okHttp3.runtime
    implementation deps.okHttp3.interceptor
    // gson
    implementation deps.gson.runtime
    // 腾讯IM
    implementation deps.imsdk.runtime
    // Glide
    implementation deps.glide.runtime
    // 腾讯存储服务
    implementation deps.cosxml.runtime
    // B站弹幕
    implementation deps.DanmakuFlameMaster.runtime
    // rxAndroid + rxJava
    implementation deps.rxAndroid.runtime
    implementation deps.rxAndroid.rxjava
    // rxBinding
    implementation deps.rxBinding.runtime
    // autoDispose
    implementation deps.autoDispose.android
    implementation deps.autoDispose.lifecycle
    // retrofit
    implementation deps.retrofit.runtime
    implementation deps.retrofit.adapter
    implementation deps.retrofit.converter
    // xxpermissions
    implementation deps.xxpermissions.runtime
    // liveEventBus
    implementation deps.liveEventBus.runtime
    // banner
    implementation deps.banner.runtime
    // bravh
    implementation deps.bravh.runtime
    // hilt
//    implementation deps.hilt.runtime
//    implementation deps.hilt.lifecycle
//    kapt deps.hilt.kapt
//    kapt deps.hilt.compiler
    // leakCanary
    debugImplementation deps.leakCanary.runtime

以上就是大致引入的包,然后接下来就是针对业务场景的一整套流程演示了:

登录场景
View
/**
 * 登录页面
 */
public class LoginActivity extends MVVMActivity {
    private static final String TAG = "LoginActivity";

    private LoadingDialog.Builder mLoading; // 加载页面
    private ActivityLoginBinding mDataBinding;// DataBinding
    private LoginViewModel mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void initViewModel() {
        mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
        ViewModelProvider.Factory factory = new LoginViewModelFactory(getApplication(), this);
        mViewModel = ViewModelProviders.of(this, factory).get(LoginViewModel.class);
    }

    @Override
    public void init(){
        mLoading = new LoadingDialog.Builder(LoginActivity.this);
        mLoading.setMessage(getString(R.string.login_loading_text));
        mLoading.create();
    }

    @Override
    public void bindUi(){
        // 登录请求
        RxView.clicks(mDataBinding.loginBtn)
                .subscribeOn(AndroidSchedulers.mainThread())
                .to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
                .subscribe(unit ->
                        PermissionTools.requestPermission(this, () ->             //                       校验读写权限
                                        mViewModel.Login(mDataBinding.userNameEdt.getText().toString().trim()  //  登录请求
                                                , mDataBinding.passwordEdt.getText().toString().trim())
                                , Permission.READ_PHONE_STATE));
        // 注册按钮
        RxView.clicks(mDataBinding.registerImg)
                .to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
                .subscribe(unit -> startActivity(new Intent(LoginActivity.this, RegisterActivity.class)));  //  跳转注册页面
    }

    /**
     * 不带粘性消息
     */
    @Override
    public void subscribeUi() {
        // 页面状态变化通知  带粘性消息
        mViewModel.getLoginState().observe(this, state -> {
            switch (state) {
                case ERROR_CUSTOMER_SUCCESS_PASS:   // 通过校验
                    mLoading.getObj().show();
                    break;
                case ERROR_CUSTOMER_PASSWORD_ERROR: // 账号错误
                case ERROR_CUSTOMER_USERNAME_ERROR: // 密码错误
                    mDataBinding.passwordEdt.setText("");  // 清空密码输入框
                    ToastUtil.showToast(this, TCErrorConstants.getErrorInfo(state));
                    break;
            }
        });

        // 登录信息返回通知
        LiveEventBus.get(RequestTags.LOGIN_REQ, BaseResponBean.class)
                .observe(this, bean -> {
                    Optional.ofNullable(mLoading).ifPresent(builder -> mLoading.getObj().dismiss());  // 取消 Loading
                    if (bean.getCode() == 200) { // 登录成功
                        ToastUtil.showToast(LoginActivity.this, "登录成功!");
                        startActivity(new Intent(LoginActivity.this, MainActivity.class));
                        finish();
                    } else {                     // 登录失败
                        ToastUtil.showToast(LoginActivity.this, "登录失败:" + TCErrorConstants.getErrorInfo(bean.getCode()));
                        mDataBinding.passwordEdt.setText("");  // 清空密码输入框
                    }
                });
    }

    @Override
    public void initRequest() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Optional.ofNullable(mLoading).ifPresent(builder -> mLoading.getObj().dismiss()); // 取消 Loading
    }

}

以上的登录View中包含几个模块

  1. initViewModel() 是为了保证MVVM的完整性,进行的VIewModel初始化
  2. init() 用于处理一些View中控件的初始化
  3. bindUi() 是通过RxView,将页面的事件转换成Observable,然后在于ViewModel中具体的功能进行绑定
  4. subscribeUi() 是例如ViewModel中LiveData的变化,或是通过LiveEventBus返回的通知引起的View变化
  5. initRequest() 用于处理刚进入View时就要请求的方法
public abstract class MVVMActivity extends AppCompatActivity {

    public abstract void initViewModel();

    public abstract void init();

    public abstract void bindUi();

    public abstract void subscribeUi();

    /**
     * 请求网络数据
     */
    public abstract void initRequest();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initViewModel();
        init();
        subscribeUi();
        initRequest();
    }

    @Override
    protected void onResume() {
        super.onResume();
        bindUi();
    }
}

以上就是每个方法的调用顺序

ViewModel
public class LoginViewModel extends ViewModel {

    private final LoginRepository repository;
    private final LifecycleOwner lifecycleOwner;
    private final MutableLiveData<Integer> loginState = new MutableLiveData<>();  // 登录失败

    public LoginViewModel(LoginRepository repository,LifecycleOwner lifecycleOwner) {
        this.repository = repository;
        this.lifecycleOwner = lifecycleOwner;
    }

    /**
     * 登录行为
     *
     * @param userName 账号
     * @param passWord 密码
     */
    public void Login(String userName, String passWord) {
        if (checkInfo(userName, passWord)) {
            loginState.postValue(ERROR_CUSTOMER_SUCCESS_PASS);
            repository.loginReq(lifecycleOwner, userName, passWord);
        }
    }

    /**
     * 检测用户输入的账号密码是否合法
     *
     * @param userName 账号
     * @param passWord 密码
     * @return true:通过检测 false:未通过
     */
    private boolean checkInfo(String userName, String passWord) {
        if (!TCUtils.isUsernameVaild(userName)) {
            loginState.postValue(ERROR_CUSTOMER_USERNAME_ERROR);
            return false;
        }
        if (!TCUtils.isPasswordValid(passWord)) {
            loginState.postValue(ERROR_CUSTOMER_PASSWORD_ERROR);
            return false;
        }
        return true;
    }

    public LiveData<Integer> getLoginState() {
        return loginState;
    }
}

ViewModel作为连通View以及Model之间的通道,负责管理LiveData,以及一些业务上的逻辑,而View尽量通过LiveData的双向绑定实现UI的更新。

Model

这里时Model的代表 Repository

public class LoginRepository extends BaseRepository {

    private final static String TAG = "LoginRepository";

    private final static String PREFERENCE_USERID = "userid";
    private final static String PREFERENCE_USERPWD = "userpwd";

    /**
     * 单例模式
     */
    @SuppressLint("StaticFieldLeak")
    private static volatile LoginRepository singleton = null;

    /**********************************     本地数据缓存    **************************************/
    private LoginResponBean mUserInfo = new LoginResponBean();  // 登录返回后 用户信息存在这
    private final LoginSaveBean loginSaveBean = new LoginSaveBean();  // 用于保存用户登录信息
    private TCUserMgr.CosInfo mCosInfo = new TCUserMgr.CosInfo();   // COS 存储的 sdkappid

    private Context mContext;              //                                       初始化一些组件需要使用

    /**
     * 初始化缓存数据
     */
    private void initData() {
        loadUserInfo(); //  是否有缓存账号数据
    }

    private void loadUserInfo() {
        if (mContext == null) return;
        TXLog.d(TAG, "xzb_process: load local user info");
        SharedPreferences settings = mContext.getSharedPreferences("TCUserInfo", Context.MODE_PRIVATE);
        loginSaveBean.setmUserId(settings.getString(PREFERENCE_USERID, ""));
        loginSaveBean.setmUserPwd(settings.getString(PREFERENCE_USERPWD, ""));
    }

    private void saveUserInfo() {
        if (mContext == null) return;
        TXLog.d(TAG, "xzb_process: save local user info");
        SharedPreferences settings = mContext.getSharedPreferences("TCUserInfo", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        editor.putString(PREFERENCE_USERID, loginSaveBean.getmUserId());
        editor.putString(PREFERENCE_USERPWD, loginSaveBean.getmUserPwd());
        editor.apply();
    }

    /**
     * 登录请求
     *
     * @param userName 账号
     * @param passWord 密码
     */
    public void loginReq(LifecycleOwner lifecycleOwner, String userName, String passWord) {
        LoginRequestBuilder.loginFlowable(userName, passWord)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .flatMap((Function<BaseResponBean<LoginResponBean>, Flowable<BaseResponBean<AccountInfoBean>>>) loginBean -> {
                    if (loginBean != null) { // 登录成功
                        Optional.ofNullable(loginBean.getData()).ifPresent(userInfo -> mUserInfo = userInfo); //                        保存返回的数据
                        if (loginBean.getMessage() != null) {
                            LiveEventBus.get(RequestTags.LOGIN_REQ, BaseResponBean.class)
                                    .post(new BaseResponBean<>(loginBean.getCode(), loginBean.getMessage()));         // 页面要处理的逻辑(注册返回)
                        }
                        if (loginBean.getCode() == 200
                                && loginBean.getData() != null
                                && loginBean.getData().getToken() != null
                                && loginBean.getData().getRoomservice_sign() != null
                                && loginBean.getData().getRoomservice_sign().getUserID() != null) {
                            setToken(loginBean.getData().getToken());  //                                              Token 保存到本地 用于后期请求鉴权
                            setUserId(loginBean.getData().getRoomservice_sign().getUserID());//                        UserId 保存到本地 当前登录的账号
                            initMLVB();//                                                                              初始化直播SDK
                            return LoginRequestBuilder.accountFlowable(getUserId(), getToken()); //                             请求账户信息
                        } else {
                            return Flowable.error(new ApiException(loginBean.getCode(), loginBean.getMessage()));  // 抛出登录异常  不会继续链式调用
                        }
                    }
                    return Flowable.error(new ApiException(-1, "网络异常"));  // 抛出登录异常  不会继续链式调用
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
                .subscribe(new DisposableSubscriber<BaseResponBean<AccountInfoBean>>() {
                    @Override
                    public void onNext(BaseResponBean<AccountInfoBean> accountBean) {
                        if (accountBean != null && accountBean.getCode() == 200) {  // 查询账户信息返回
                            if (accountBean.getData() != null) {
                                if (accountBean.getData().getAvatar() != null)
                                    loginSaveBean.setmUserAvatar(accountBean.getData().getAvatar());  //      保存用户头像信息
                                if (accountBean.getData().getNickname() != null)
                                    loginSaveBean.setmUserName(accountBean.getData().getNickname()); //       用户称呼
                                if (accountBean.getData().getFrontcover() != null)
                                    loginSaveBean.setmCoverPic(accountBean.getData().getFrontcover());//      直播封面?
                                if (accountBean.getData().getSex() >= 0) {
                                    loginSaveBean.setmSex(accountBean.getData().getSex());//                  用户性别
                                }
                            }
                        }
                    }

                    @Override
                    public void onError(Throwable t) {
                        if (t instanceof ApiException) {
                            Log.e("TAG", "request error" + ((ApiException) t).getStatusDesc());
                        } else {
                            Log.e("TAG", "request error" + t.getMessage());
                        }
                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }



    /**
     * 注册账号请求
     *
     * @param username 账户名
     * @param password 密码
     */
    public void registerReq(LifecycleOwner lifecycleOwner,String username, String password) {
        LoginRequestBuilder.registerFlowable(username, password)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .to(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(lifecycleOwner)))
                .subscribe(new DisposableSubscriber<BaseResponBean>() {
                    @Override
                    public void onNext(BaseResponBean registerBean) {
                        if (registerBean != null) {
                            LiveEventBus.get(RequestTags.REGISTER_REQ, BaseResponBean.class)
                                    .post(new BaseResponBean<>(registerBean.getCode(), registerBean.getMessage()));         // 页面要处理的逻辑(登录返回)
                        }
                    }

                    @Override
                    public void onError(Throwable t) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }


    /**
     * 初始化直播SDK
     */
    public void initMLVB() {
        // 校验数据完整性
        if (mUserInfo == null || mContext == null
                || mUserInfo.getRoomservice_sign() == null
                || mUserInfo.getRoomservice_sign().getSdkAppID() == 0
                || mUserInfo.getRoomservice_sign().getUserID() == null
                || mUserInfo.getRoomservice_sign().getUserSig() == null) return;

        LoginInfo loginInfo = new LoginInfo();
        loginInfo.sdkAppID = mUserInfo.getRoomservice_sign().getSdkAppID();
        loginInfo.userID = getUserId();
        loginInfo.userSig = mUserInfo.getRoomservice_sign().getUserSig();

        String userName = loginSaveBean.getmUserName();
        loginInfo.userName = !TextUtils.isEmpty(userName) ? userName : getUserId();
        loginInfo.userAvatar = loginSaveBean.getmUserAvatar();
        MLVBLiveRoom liveRoom = MLVBLiveRoom.sharedInstance(mContext);
        liveRoom.login(loginInfo, new IMLVBLiveRoomListener.LoginCallback() {
            @Override
            public void onError(int errCode, String errInfo) {
                Log.i(TAG, "MLVB init onError: errorCode = " + errInfo + " info = " + errInfo);
            }

            @Override
            public void onSuccess() {
                Log.i(TAG, "MLVB init onSuccess: ");
            }
        });
    }

    /**
     * 自动登录
     */
    public void autoLogin() {

    }

    public void setmContext(Context context) {
        this.mContext = context;
        initData();
    }

    public LoginSaveBean getLoginInfo(){
        return loginSaveBean;
    }

    public static LoginRepository getInstance() {
        if (singleton == null) {
            synchronized (LoginRepository.class) {
                if (singleton == null) {
                    singleton = new LoginRepository();
                }
            }
        }
        return singleton;
    }
}

除去里面复杂的业务逻辑,可以看到Repository的主要作用是数据仓库,如用单例形式保存一些业务上的数据(用户账户信息),负责处理请求中的业务逻辑,通过RxAndroid和Retrofit的组合,来完成一系列的请求,并通过LiveEventBus或是LiveData来通知页面

HttpRequest

网络请求模块

// LoginRequestBuilder.java
public static Flowable<BaseResponBean<LoginResponBean>> loginFlowable(String userName, String passWord) {
    HashMap<String, String> requestParam = new HashMap<>();
    requestParam.put("userid", userName);
    requestParam.put("password", TCUtils.md5(TCUtils.md5(passWord) + userName));
    return RetrofitTools.getInstance(LoginService.class) // 这里是很标准的Retrofit写法
            .login(RequestBodyMaker.getRequestBodyForParams(requestParam));
}

// LoginService.java
@POST("/login")
Flowable<BaseResponBean<LoginResponBean>> login(@Body RequestBody requestBody);

// RetrofitTools.java
public static <T> T getInstance(final Class<T> service) {
    if (okHttpClient == null) {
        synchronized (RetrofitTools.class) {
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpInteraptorLog());
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            okHttpClient = new OkHttpClient.Builder()
                        .addInterceptor(interceptor)
                        .connectTimeout(5, TimeUnit.SECONDS)
                        .readTimeout(5, TimeUnit.SECONDS)
                        .writeTimeout(5, TimeUnit.SECONDS)
                        .build();
            }
        }

        if (retrofit == null) {
            synchronized (RetrofitTools.class) {
                if(retrofit == null) {
                    retrofit = new Retrofit.Builder()
                            .baseUrl(TCGlobalConfig.APP_SVR_URL)         //BaseUrl
                            .client(okHttpClient)                       //请求的网络框架
                            .addConverterFactory(GsonConverterFactory.create())     //解析数据格式
                            .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) // 使用RxJava作为回调适配器
                            .build();
                }
            }
        }
    return retrofit.create(service);
}

网络请求返回的Flowable(背压)可以直接通过组合,链式的方式,组合成符合业务逻辑的结构

以上看上去十分简单的一个例子就是糅合了MVVM + RxAndroid + RxView + DataBinding + LiveData + LiveEventBus + Retrofit

一些复杂的列表页面,则加入了Bravh,来优Adapter代码量

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

推荐阅读更多精彩内容