前言
本来想记录一下最近相机相关的知识点的,但发现需要时间整理一下,那这里就介绍一下最近写的直播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中包含几个模块
- initViewModel() 是为了保证MVVM的完整性,进行的VIewModel初始化
- init() 用于处理一些View中控件的初始化
- bindUi() 是通过RxView,将页面的事件转换成Observable,然后在于ViewModel中具体的功能进行绑定
- subscribeUi() 是例如ViewModel中LiveData的变化,或是通过LiveEventBus返回的通知引起的View变化
- 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 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 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, Flowable>>) 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>() {
@Override
public void onNext(BaseResponBean 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() {
@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> loginFlowable(String userName, String passWord) {
HashMap 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> login(@Body RequestBody requestBody);
// RetrofitTools.java
public static T getInstance(final Class 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代码量