会控应用模块化改造及优化
- 1. 模块设计图
- 1.1 模块划分
- 1.2 模块引用原则
- 2.应用架构图
- 3.数据仓库接口设计
- 4.模块间通信
- 5.产品兼容支持
- 6. 代码实现优化
- 6.1 企业通讯录实现优化
- 6.2 会控应用实现优化
- 7. MVP/MVVM架构类图实现
1. 模块设计图
模块主要考虑横向与纵向划分,纵向考虑代码复用,共用部分下沉,但是当业务增多后,下沉的代码量急剧增加,导致形成一个很大的Common lib,
那么要避免这种情况就要考虑横向切分,也就是业务解耦,也就是模块化划分。
1.1 模块划分
自上而下依次为应用层、业务实现层、业务框架层、基础框架层
- 应用层
App层包括各个产品Ui适配,其中contacts_ui_common、conf_ui_common中定义一些可以供各产品使用的UI抽象、ViewModel、Presenter等,
比如列表、详情、公共menu功能、各个业务模块的Presenter、ViewModel、Adapter、Fragment等;通过泛型或者模板设计,帮助App中快速实现UI功能。
- 业务实现层
主要是各业务模块的Model层的代码实现,在应用架构图也就是Repo的实现。
- 业务框架层
公共model、路由、产品适配、公共工具、Log工具、网络框架, 当前这些模块代码量较少,所以都放在同一个Module中,当业务量增加后
需要拆分成多个Module进行管理。
- 基础框架层
该层代码和业务无关,特定平台UI定制代码、thirdLibs、MVP/MVVM
1.2 模块引用原则
Module之间编译引用的原则如下:
- 上层可以引用下层,反之不可以。
- 同一层次的cmcc_contacts_lib、cmcc_conf_lib不可以相互引用,如果需要发生接口调用,通过
cmcc_common_lib作为路由中心,通过路由设计可以达到解耦目的。 - App层的UI module可以直接引用与之相关联的业务实现lib,比如gvc_conf_ui可以直接引用cmcc_conf_lib
如果需要使用contacts的接口,需要通过cmcc_common_lib中路由接口获取。 - cmcc_common_lib是业务接口路由中心,如果上层两个模块之间是同一进程中调用,可以使用Aroute框架进行接口调用,如果
是不同进程间调用,需要使用aidl或者Provider接口调用。
2.应用架构图
这个是官方提供的应用架构图,MVVM、MVP,需要注意的是,在代码实现的时候需要明确好所属的概念层级,做好代码的复用及解耦,尽量不要出现两大段逻辑相同的代码。
MVVM及MVP基础框架中,提供了生命周期管理,可以通过子类继承方式使用。
App内最好根据业务来分包,当业务量增多时,这种分包方式更加清晰直观。
3.数据仓库接口设计
这里使用简单的外观模式将整个模块业务抽象成一个Repo,Repo中包含本地数据源与远端数据源,本地数据源可以理解为需要持久化或者
与应用生命周期一致的数据,例如Db、sharePref、App静态变量等,远端数据是指从服务器获取的或者需要提交的服务器的数据,例如Http Api接口。
Repo采用单例模式注入
public class Injection {
private static IContactsRepo mRepo;
public static IContactsRepo providerContactsRepo(@Nullable Context context){
if(mRepo == null){
synchronized (Injection.class){
if(mRepo == null){
ILocalDataSource localDataSource = ContactsLocalDataSource.getInstance();
IRemoteDataSource remoteDataSource = ContactsRemoteDataSource.getInstance();
IContactsRepo contactsRepo = ContactsRepository.getInstance(localDataSource,remoteDataSource);
contactsRepo.init(context);
mRepo = contactsRepo;
}
}
}
return mRepo;
}
public static BaseSchedulerProvider provideSchedulerProvider() {
return SchedulerProvider.getInstance();
}
}
本地数据源及远端数据源获取
IContactsRepo contactsRepo = Injection.providerContactsRepo(this);
ILocalDataSource localDataSource = contactsRepo.getLocalDataSource();
IremoteDataSource remoteDataSource = mRepository.getRemoteDataSource();
4.模块间通信
模块间通讯包括App组件通信和业务模块接口通信
组件通信可以采用隐式Intent或者使用Arouter框架,进程间采用隐式Intent,进程内可以采用Arouter
框架。
模块业务接口通信可以采用路由框架,我们可以自己实现,需要考虑是进程内接口通信还是进程间接口通讯,
如果是进程间通讯,可以参考GsApi的实现,如果是进程内通信可以使用Arouter框架提供的IProvider
接口实现。
Arouter框架使用参考
通讯录IContactsService
及其实现类ContactsApiService
会控IConfService
及其实现类ConfApiService
当前的实现是通讯录与会控应用运行在独立的进程中,通讯录对外提供接口主要使用ContentProvider,会控
对外提供接口主要通过aidl提供的binder接口。
通讯录对外提供接口通过ContactsProviderHelper
类
会控制应用对外提供接口通过ConferenceRemoteProxy
类
5.产品兼容支持
为了在同一个仓库兼容新旧产品,需要通过接口对产品差异做隔离,例如现有的H60与C12及P21的兼容,
需要兼容的接口包括Call Api、Dbus Api及Nvram Api。
具体实现过程:
- 定义统一的接口方法
public interface ICallCtlApi extends IApi {
boolean endCall(Context context);
boolean isCallBusy(Context context);
}
- 对统一接口在不同产品上实现
H60实现
public class CallCtlApiImpl implements ICallCtlApi {
@Override
public boolean endCall(Context context) {
return BaseCallApi.endConf();
}
}
C12
public class CallCtlApiImpl implements ICallCtlApi {
@Override
public boolean endCall(Context context) {
CallManager callManager = (CallManager)context.
getSystemService(GSManagerServiceName.CALL_MANAGER_SERVICE);
return callManager != null && callManager.endConference(true);
}
}
- 运行时注入正确的实例
//callCtl Api
IApi callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.CallCtlApiImpl");
if(DeviceHelper.getDeviceType() == Constants.DEVICE_TYPE_C12_OLDAPI){
callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.c12.CallCtlApiImpl");
}else if(DeviceHelper.getDeviceType() == Constants.DEVICE_TYPE_P21_OLDAPI){
callCtlIApi = getApiImpl("com.grandstream.cmcc.voiceassistant.api.impl.p21.CallCtlApiImpl");
}
if (callCtlIApi != null) {
sApiMap.put(getApiKey("com.grandstream.cmcc.voiceassistant.api.client.CallCtlApi"), callCtlIApi);
}
- 通过统一的Client调用
public class CallCtlApi {
private static ICallCtlApi getCallCtlApi(Context context){
VoiceCtlApiClient client = VoiceCtlApiClient.getInstance(context);
if(client == null){
throw new ApiUninitializedException();
}
ICallCtlApi api = (ICallCtlApi) client.getApi(NAME);
return api;
}
public static boolean endCall(Context context){
return getCallCtlApi(context).endCall(context);
}
public static boolean isCallBusy(Context context){
return getCallCtlApi(context).isCallBusy(context);
}
- 编译时做动态依赖
编译时候,相关产品framework最好做动态依赖,不要打包到App中。
LOCAL_JAVA_LIBRARIES := gsframework gsframework-c12 gsframework-p21
6. 代码实现优化
- 1.统一网络框架封装
使用一套封装好的网络Api框架有利于代码复用与维护
使用ServiceGenerator
类提供网络请求框架,适配了RxJava及LiveData数据类型,静态方法
提供了Contacts及Conference的调用接口。
使用NetServiceProvider
及ContactsModule
类提供对外网络Api接口。
- 2.网络重试
一些Api常出现SocketTimeoutException异常,针对该异常使用RxJava retrywhen操作符
提供后台无感知重试操作。
参考RetryWithDelay
类及ConfCtlRemoteDataSource
类中createConf
函数。
- 3.超时时间
一些Api数据量比较大,默认超时时间可能不能满足需求,所以需要动态修改超时时间,使用OkHttp拦截器
配合反射动态修改OkHttp的默认超时时间。
参考DynamicTimeOutInterceptor
及NetServiceProvider
类中provideCmccConfApi2Service
函数。
- 4.线程模型
提供一种简单的线程封装,单例设计,线程池分组
参考AppExecutors
类。AppExecutors
定义如下:
public class AppExecutors {
private static final int THREAD_COUNT = 4;
private static AppExecutors appExecutors;
private final Executor diskIO;
private final Executor networkIO;
private final Executor compution;
private final MainThreadExecutor mainThread;
private final Executor serialThread;
@VisibleForTesting
AppExecutors(Executor diskIO, Executor networkIO, MainThreadExecutor mainThread, Executor serialThread) {
this.diskIO = diskIO;
this.networkIO = networkIO;
this.mainThread = mainThread;
this.serialThread = serialThread;
this.compution = Executors.newFixedThreadPool(THREAD_COUNT);
}
private AppExecutors() {
this(new DiskIOThreadExecutor(), Executors.newFixedThreadPool(THREAD_COUNT),
new MainThreadExecutor(), Executors.newSingleThreadExecutor());
}
public static AppExecutors getAppExecutors() {
if(appExecutors == null){
synchronized (AppExecutors.class){
if(appExecutors == null){
appExecutors = new AppExecutors();
}
}
}
return appExecutors;
}
public Executor diskIO() {
return diskIO;
}
public Executor networkIO() {
return networkIO;
}
public Executor compution(){
return compution;
}
public MainThreadExecutor mainThread() {
return mainThread;
}
public Executor serialThread(){
return serialThread;
}
public static class MainThreadExecutor implements Executor {
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
public void execute(@NonNull Runnable command, long delay){
mainThreadHandler.postDelayed(command, delay);
}
@Override
public void execute(@NonNull Runnable command) {
mainThreadHandler.post(command);
}
}
}
调用示例代码如下:
public void loadOwnerAsyn(AsynCallback<Owner> callback) {
AppExecutors.getAppExecutors().diskIO().execute(new Runnable() {
@Override
public void run() {
final Owner owner = loadOwner();
AppExecutors.getAppExecutors().mainThread().execute(new Runnable() {
@Override
public void run() {
callback.onCallback(owner);
}
});
}
});
}
6.1 企业通讯录实现优化
- 1.通讯录层级管理
层级进入及回退采用栈数据结构管理,参考ContactsActivity
中onBackPressed()
的实现
private String lastParentId = "0";
private final Stack<StackNodeInfo> stack = new Stack<StackNodeInfo>();
//1.点击进入子部门
@Override
public void onPageItemClick(View itemView, GeneralNode bean, int position, int currentPage) {
lastParentId = holder.id;
mPresenter.loadChildNodes(lastParentId);
View tv = mPathView.addView(holder.labelTxt, holder.id);
stack.add(new StackNodeInfo(lastParentId, tv));
}
//2.back键回退
public void onBackPressed() {
if("0".equals(lastParentId)){
setCheckedContactsResult();
super.onBackPressed();
}else{
upLevel();
}
}
@Override
public String getLastParentId() {
return lastParentId;
}
//3.回退到上一级
@Override
public void upLevel() {
if(stack.size()>1){
stack.pop();
StackNodeInfo nodeInfo = stack.peek();
nodeInfo.getView().performClick();
}
}
//4.回退到任意部门
@Override
public void pop(String dpt) {
if(stack.size()<1){
return;
}
StackNodeInfo nodeInfo = stack.peek();
while (!nodeInfo.getNodeId().equals(dpt)){
if(stack.size()<1){
break;
}
stack.pop();
if(stack.size()<1){
break;
}
nodeInfo = stack.peek();
}
}
- 2.层级数据加载及数据节点操作
将同一部门下的子部门及联系人抽象成节点进行操作,将整个通讯录抽象成一颗树,从而相关的操作都可以
使用树数据结构进行实现,树的节点由唯一的Id进行标识。
加载某个部门下的所有节点数据的sql语句如下,这里使用的是Room框架
参考UserDao
@Query("SELECT _id, id, name, usrNum AS summary, pinyin, 1 AS type FROM depart WHERE pid = :pid UNION SELECT _id, id, name, mobile AS summary, pinyin, 0 AS type FROM user WHERE dptId = :pid ORDER BY type, name ASC")
Cursor queryNodesSync(String pid);
节点复选操作数据结构, 节点区分为分支节点及叶子节点
分支节点数据结构参考ContactsLocalDataSource
中 allTreeNodes
及allTreeMap
叶子节点数据结构参考ContactsLocalDataSource
中 checkedLeafNodeList
企业通讯录启动时,需要生成分支节点的树数据结构,以便为后续分支节点操作(选中、取消选中、加载人数)提供递归算法,
参考ContactsLocalDataSource
中 generateEnpTree
由于节点操作可能会涉及递归算法或者数据库查询,所以都是放到异步操作中完成的,这里使用RxJava或者LiveData的响应式操作完成。
请参考相关的Menu功能代码了解相关的实现。
6.2 会控应用实现优化
- 1.会议服务启动
以前的方式比较绕,这里使用CmccConfCtlService
启动Binder实例,使用CmccConfSystem
单例来管理。
- 2.会议Api1及Api2切换
通过P值控制,会议实例初始化时控制,参考 CmccConfCtlService
中的initializeCmccConfSystem
函数
- 3.Api1与Api2数据结构转化
由于Api2返回的Json格式与Api1返回的Json格式差异较大,为了复用Api1的Bean,需要做一下
转化,通过BeanConverter
及Api2ResultConverter
两个类完成,参考相关代码实现。
- 4.传递实现了
Parcelable
接口的Bean
采用泛型设计,可以传递任何实现了Parcelable
接口的业务Bean
参考Result
类。
public class Result<T extends Parcelable> implements Parcelable {
public static final int RESULT_OK = 0;
public static final Creator<Result> CREATOR = new Creator<Result>() {
@Override
public Result createFromParcel(Parcel source) {
return new Result(source);
}
@Override
public Result[] newArray(int size) {
return new Result[size];
}
};
private static final String TAG = Result.class.getSimpleName();
private String msg;
private Class classType;
private T result;
public Result(String msg, Class classType, T result) {
this.msg = msg;
this.classType = classType;
this.result = result;
}
protected Result(Parcel in) {
readFromParcel(in);
}
public T getResult() {
return result;
}
public String getMsg() {
return msg;
}
public Class getClassType() {
return classType;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(msg);
dest.writeSerializable(classType);
dest.writeValue(result);
}
private void readFromParcel(Parcel in) {
this.msg = in.readString();
Logger.d(TAG, "msg = " + msg);
this.classType = (Class) in.readSerializable();
Logger.d(TAG, "classType = " + classType);
this.result = (T) in.readValue(classType.getClassLoader());
}
}
使用
if(callBack != null && result instanceof Parcelable){
String className = result.getClass().getSimpleName();
callBack.onSuccess(new Result(className,result.getClass(),(Parcelable) result));
}
7. MVP/MVVM架构类图实现
下面是MVVM及MVP架构抽象模型设计类图