為什麼要元件化?
醫動力App作為公司的核心產品已經有多年曆史了,隨著版本的不斷迭代,功能越來越多,程式碼量越來越大,不可避免的會產生一下問題:
- 業務越來越複雜,維護成本高;
- 業務耦合度高,程式碼越來越臃腫,團隊內部多人協作開發困難;
- 編譯時間長,每修改一處程式碼後都要重新編譯打包測試,導致非常耗時;
- 開發測試困難,每次修改都必須打包整個專案執行.
因此,為了提高專案的可維護性和開發效率,元件化成為了必然.
元件化的目標
首先看看老版本的醫動力的專案工程結構
專案分為Doctor和Patient,它們之間公共的程式碼放在common中,common中會依賴一些第三方的庫.隨著專案的迭代,Doctor和Patient裡面的程式碼會越來越耦合,因為單個module中僅僅是以包名作為功能的劃分,而包之間是可以隨意呼叫的.
AndroidStudio/IDEA是多模組管理的,元件化的思路就是把不同的業務模組拆分到各個子Module中,同時這些業務Module之間不會存在直接的呼叫,這樣當我們移除專案中的某個業務Module的時候不會影響整體的專案執行,如下圖所示:
可以看到有「ModuleIM」,「ModuleFollow」,「ModuleDiary」三個業務模組,它們都會引用Common模組而獲取一些基礎庫的支援,同時這些業務模組是可以單獨執行的.後續會將更多的模組獨立出來,完成徹底的元件化.
如何元件化?
Android的元件化的技術點主要在兩個方面:
- 配置元件獨立執行的能力;
- 元件之間的通訊
第一點是通過在module的gradle.build中切換
apply plugin: 'com.android.application'apply plugin: 'com.android.library',複製程式碼
並設定對應模式下載入的AndroidManifest.xml和src的路徑.
第二點由於元件之間是沒有直接依賴的關係的,要想讓它們通訊就必須把它們註冊在一個公共的地方,這裡可以通過路由,也可以通過註冊介面.
目前元件化的方案在網上有很多,由於元件化並不涉及Android系統級別的操作,因此是比較成熟穩定的.我們知道,元件化是需要花費大量時間和精力的,很難做到把專案徹底的拆分成元件,那有沒有一種方式可以讓我們不改動原有專案(改動很小)的情況下,將新功能進行元件化開發?
因此我們選擇了使用CC來進行元件化的改造
Component Caller(CC)介紹
引用自CC官方的兩張圖能讓我們很快的明白什麼叫做漸進式元件化?
在漸進式元件化的方案中,可以先不用解耦,只需要讓單獨執行的元件能夠呼叫到主App中的功能即可。思路是這樣的:
- 新業務以元件形式開發
- 新元件需要呼叫的主App中的業務,在對應的模組中建立一個元件類,對外暴露對應的服務,供其它元件呼叫,並不需要現在就將這個模組解耦
- 新元件通過跨App的方式呼叫主App中的元件
- 主App也可以通過跨App的方式呼叫到單獨執行的元件App中的元件
- 在同一個module中可以建立多個元件類,將來解耦時將對應的元件類移動到解耦後的module中即可
關於CC的技術實現細節可以檢視其github主頁的wiki系列文章github.com/luckybilly/…
元件化實踐
CC的整合
1.在專案的根gradle.build中加入:
buildscript {
dependencies {
... ... classpath 'com.billy.android:autoregister:1.4.1'
}
}複製程式碼
這個autoregister外掛是用來在編譯期間動態掃描並修改class檔案,實現元件的自動註冊,具體配置後面會提到
2.在Doctor和Patient這兩個主App的gradle.build中加入
ext.mainApp = true //設定為true,表示此module為主app module,一直以application方式編譯apply from: '../cc-setting.gradle'複製程式碼
cc-setting.gradle中主要進行了以下幾點操作:
- 讀取local.properties檔案中的配置來區分元件是否以獨立app的方式編譯;
- 新增com.billy.android:cc:1.1.0依賴;
- 自動註冊元件的配置
3.實現IComponent介面建立元件類
我們統一在Doctor和Patient中的exports包內建立App對外提供的元件,不同業務的元件放在建立在對於的類中管理
IComponent的實現也很簡單,提供一個模組名稱name和對不同action的處理
主App的配置就這麼多,接下來要新建一個元件Module
4.業務module的配置
這裡需要設定module獨立執行時的applicationId,同時指定resourcePrefix資源字首(防止不同模組之間資原始檔的衝突)
因為設定了module的獨立執行,就需要準備一份module在獨立執行模式下的AndroidManifest檔案,路徑在src/main/debug下
5.業務module提供IComponent介面
和主App一樣,模組要提供服務給其他模組呼叫,需要提供實現IComponent的子類,因為業務module提供的服務會比較少且單一,我們將它放在包名下的ExportComponent下
7.主App設定需要依賴的module
dependencies {
api fileTree(include: ['*.jar'], dir: 'libs') addComponent 'module_follow' addComponent 'module_diary' addComponent 'module_im'
}複製程式碼
通過addComponent方式新增依賴,其內部會根據業務module的執行模式決定是否依賴
6.設定業務module的執行模式
業務module的執行模式包括開發模式和整合模式
- 開發模式:會以App的方式執行
- 整合模式:打包在主App中
開啟local.properties檔案
module_follow=truemodule_diary=truemodule_im=false複製程式碼
設定模組名稱等於true或者false(沒有設定則為false)
- 當等於true則該模組會以App執行,這時候打包主App的時候是不會把該模組打包進去
- 當等於false時則不能獨立執行,打包主App的時候會一起打包進去
執行除錯
現在你可以將元件單獨執行起來了,但是由於業務元件是不包含登入功能的,因此它是沒有使用者登入狀態的,所以我們需要通過CC的元件呼叫去主App中獲取
補充一點,如果想要跨App呼叫首先需要開啟CC的設定
CC.enableRemoteCC(true);
複製程式碼
元件呼叫
CCResult result = CC.obtainBuilder("common") .setActionName("httpInfo") .addParam("type", type) .build() .call();
String baseUrl = result.getDataItem("baseUrl");
String token = result.getDataItem("token");
複製程式碼
呼叫的方式很簡單,只需要指定模組名稱和對應的Action,同時可以傳遞引數,以上程式碼呼叫的是common模組的action為httpInfo的元件.
同時支援同步和非同步的呼叫,前一個例子是同步呼叫,非同步呼叫需要傳入一個回撥方法.
String callId = CC.obtainBuilder("Common") .setActionName("httpInfo") .addParam("type",type) .build() .callAsync(new IComponentCallback(){...
});
複製程式碼
被呼叫元件在處理完請求後,需要作出響應,看以下程式碼:
public class HttpComponent implements IComponent {
@Override public String getName() {
return "http";
} @Override public boolean onCall(CC cc) {
String action = cc.getActionName();
if(action.equals("action1")){
CC.sendCCResult(cc.getCallId(),CCResult.success());
return false;
}else if(action.equals("action2")){
String ccId = cc.getCallId();
return true;
}
}
}複製程式碼
注意onCall()方法的返回值,action1中返回的是false,而action2返回的是true,作用是:
- false表示立即回撥結果,這裡需要呼叫CC.sendCCResult(cc.getCallId(),CCResult.success()告訴呼叫發返回成功
- true則表示延遲迴調結果,這時你可以拿著ccCallId,等到事情處理完後才呼叫CC.sendCCResult(cc.getCallId(),CCResult.success()
這有什麼作用呢?比如你在呼叫一個登入元件去到登入介面,只有登入成功了才返回結果,這就需要延遲迴調.
網路請求元件化
專案中用到的網路請求框架是okhttp+retrofit,我們希望不同module中使用不同的retrofit的Service例項,比如在Module_follow中我們會建立FollowService.java來處理當前模組的網路請求
在整合開發模式下我們可以通過元件呼叫去獲取主App中的Retrofit物件,我們只需要在主App中定一個name=http,action=getRetrofit的IComponent;
但在開發模式下,跨程式呼叫元件是傳輸不了Retrofit物件的,因為Android的跨程式只能傳輸Parcelable物件,這裡我們可以在本module中提供一個相同名稱的IComponent,在裡面去獲取主App的baseUrl和token,並建立新的Retrofit物件,這樣就可以透明的處理獲取Retrofit物件了.
為什麼以上方式可行呢?因為CC在進行元件化呼叫的時候,會檢查當前模組是否存在要呼叫的模組,如果存在則會呼叫本地的,不存在才會去跨程式呼叫.最後我們可以把這些模擬操作抽到一個lib_mock的module裡面複用.
元件化小結
元件化後帶來的一些變化:
- 編譯時間明顯縮短
- 開發人員之間可以通過模組分工
- 可以在模組中嘗試新的技術而不擔心影響全域性(Kotlin)
CC已知侷限:
開發模式不能在Android8.0及以上環境執行,開發的時候可以使用虛擬機器或者低版本的手機,整合模式不影響.
引用