天天看點

Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

會控應用子產品化改造及優化

  • 1. 子產品設計圖
    • 1.1 子產品劃分
    • 1.2 子產品引用原則
  • 2.應用架構圖
  • 3.資料倉庫接口設計
  • 4.子產品間通信
  • 5.産品相容支援
  • 6. 代碼實作優化
    • 6.1 企業通訊錄實作優化
    • 6.2 會控應用實作優化
  • 7. MVP/MVVM架構類圖實作

1. 子產品設計圖

Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

子產品主要考慮橫向與縱向劃分,縱向考慮代碼複用,共用部分下沉,但是當業務增多後,下沉的代碼量急劇增加,導緻形成一個很大的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子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

MVVM及MVP基礎架構中,提供了生命周期管理,可以通過子類繼承方式使用。

App内最好根據業務來分包,當業務量增多時,這種分包方式更加清晰直覺。

Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作
Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

3.資料倉庫接口設計

Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

這裡使用簡單的外觀模式将整個子產品業務抽象成一個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的調用接口。

使用

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架構抽象模型設計類圖

Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作
Android子產品化實踐總結1. 子產品設計圖2.應用架構圖3.資料倉庫接口設計4.子產品間通信5.産品相容支援6. 代碼實作優化7. MVP/MVVM架構類圖實作

繼續閱讀