会控应用模块化改造及优化

  • 1. 模块设计图
  • 1.1 模块划分
  • 1.2 模块引用原则
  • 2.应用架构图
  • 3.数据仓库接口设计
  • 4.模块间通信
  • 5.产品兼容支持
  • 6. 代码实现优化
  • 6.1 企业通讯录实现优化
  • 6.2 会控应用实现优化
  • 7. MVP/MVVM架构类图实现


1. 模块设计图

Android 模块AndroidManifest android 模块化ui_ide

模块主要考虑横向与纵向划分,纵向考虑代码复用,共用部分下沉,但是当业务增多后,下沉的代码量急剧增加,导致形成一个很大的Common lib,
那么要避免这种情况就要考虑横向切分,也就是业务解耦,也就是模块化划分。

1.1 模块划分

自上而下依次为应用层、业务实现层、业务框架层、基础框架层

  1. 应用层

App层包括各个产品Ui适配,其中contacts_ui_common、conf_ui_common中定义一些可以供各产品使用的UI抽象、ViewModel、Presenter等,
比如列表、详情、公共menu功能、各个业务模块的Presenter、ViewModel、Adapter、Fragment等;通过泛型或者模板设计,帮助App中快速实现UI功能。

  1. 业务实现层

主要是各业务模块的Model层的代码实现,在应用架构图也就是Repo的实现。

  1. 业务框架层

公共model、路由、产品适配、公共工具、Log工具、网络框架, 当前这些模块代码量较少,所以都放在同一个Module中,当业务量增加后
需要拆分成多个Module进行管理。

  1. 基础框架层

该层代码和业务无关,特定平台UI定制代码、thirdLibs、MVP/MVVM

1.2 模块引用原则

Module之间编译引用的原则如下:

  1. 上层可以引用下层,反之不可以。
  2. 同一层次的cmcc_contacts_lib、cmcc_conf_lib不可以相互引用,如果需要发生接口调用,通过
    cmcc_common_lib作为路由中心,通过路由设计可以达到解耦目的。
  3. App层的UI module可以直接引用与之相关联的业务实现lib,比如gvc_conf_ui可以直接引用cmcc_conf_lib
    如果需要使用contacts的接口,需要通过cmcc_common_lib中路由接口获取。
  4. cmcc_common_lib是业务接口路由中心,如果上层两个模块之间是同一进程中调用,可以使用Aroute框架进行接口调用,如果
    是不同进程间调用,需要使用aidl或者Provider接口调用。

2.应用架构图

这个是官方提供的应用架构图,MVVM、MVP,需要注意的是,在代码实现的时候需要明确好所属的概念层级,做好代码的复用及解耦,尽量不要出现两大段逻辑相同的代码。

Android 模块AndroidManifest android 模块化ui_App_02

MVVM及MVP基础框架中,提供了生命周期管理,可以通过子类继承方式使用。

App内最好根据业务来分包,当业务量增多时,这种分包方式更加清晰直观。

Android 模块AndroidManifest android 模块化ui_ide_03


Android 模块AndroidManifest android 模块化ui_ide_04

3.数据仓库接口设计

Android 模块AndroidManifest android 模块化ui_App_05

这里使用简单的外观模式将整个模块业务抽象成一个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。

具体实现过程:

  1. 定义统一的接口方法
public interface ICallCtlApi extends IApi {

    boolean endCall(Context context);

    boolean isCallBusy(Context context);
}
  1. 对统一接口在不同产品上实现
    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);
    }
}
  1. 运行时注入正确的实例
//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);
    }
  1. 通过统一的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);
    }
  1. 编译时做动态依赖
    编译时候,相关产品framework最好做动态依赖,不要打包到App中。
LOCAL_JAVA_LIBRARIES := gsframework gsframework-c12 gsframework-p21

6. 代码实现优化

  • 1.统一网络框架封装

使用一套封装好的网络Api框架有利于代码复用与维护
使用ServiceGenerator类提供网络请求框架,适配了RxJava及LiveData数据类型,静态方法
提供了Contacts及Conference的调用接口。
使用NetServiceProviderContactsModule类提供对外网络Api接口。

  • 2.网络重试

一些Api常出现SocketTimeoutException异常,针对该异常使用RxJava retrywhen操作符
提供后台无感知重试操作。
参考RetryWithDelay类及ConfCtlRemoteDataSource类中createConf函数。

  • 3.超时时间

一些Api数据量比较大,默认超时时间可能不能满足需求,所以需要动态修改超时时间,使用OkHttp拦截器
配合反射动态修改OkHttp的默认超时时间。
参考DynamicTimeOutInterceptorNetServiceProvider类中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.通讯录层级管理

层级进入及回退采用栈数据结构管理,参考ContactsActivityonBackPressed()的实现

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);

节点复选操作数据结构, 节点区分为分支节点及叶子节点

分支节点数据结构参考ContactsLocalDataSourceallTreeNodesallTreeMap

叶子节点数据结构参考ContactsLocalDataSourcecheckedLeafNodeList

企业通讯录启动时,需要生成分支节点的树数据结构,以便为后续分支节点操作(选中、取消选中、加载人数)提供递归算法,
参考ContactsLocalDataSourcegenerateEnpTree

由于节点操作可能会涉及递归算法或者数据库查询,所以都是放到异步操作中完成的,这里使用RxJava或者LiveData的响应式操作完成。
请参考相关的Menu功能代码了解相关的实现。

6.2 会控应用实现优化

  • 1.会议服务启动

以前的方式比较绕,这里使用CmccConfCtlService启动Binder实例,使用CmccConfSystem 单例来管理。

  • 2.会议Api1及Api2切换

通过P值控制,会议实例初始化时控制,参考 CmccConfCtlService 中的initializeCmccConfSystem函数

  • 3.Api1与Api2数据结构转化

由于Api2返回的Json格式与Api1返回的Json格式差异较大,为了复用Api1的Bean,需要做一下
转化,通过BeanConverterApi2ResultConverter两个类完成,参考相关代码实现。

  • 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架构抽象模型设计类图

Android 模块AndroidManifest android 模块化ui_App_06


Android 模块AndroidManifest android 模块化ui_ide_07