what细溅?什么是組件化?
簡介
Android組件化開發(fā)實(shí)踐和案例分享(方案1)
Android 組件化最佳實(shí)踐(方案1)
Android組件化開發(fā)思想與實(shí)踐(方案1)
回歸初心:極簡 Android 組件化方案(方案2:推薦)
可以閱讀以上三篇文章儡嘶,了解組件化的基礎(chǔ)概念喇聊。
實(shí)現(xiàn)組件化目前有2套主要的方案:(1)改造module,支持library和application兩種運(yùn)行模式蹦狂,開發(fā)過程中進(jìn)行手動切換誓篱。(2)為每個module新增一個專門用于獨(dú)立運(yùn)行的AppModule,module無需改動凯楔,跑整包或組件包燕鸽,運(yùn)行不同的AppModule即可;基于開閉原則啼辣,及低侵入性啊研,操作便捷性等優(yōu)點(diǎn),推薦使用方案2
演示
將多鹿app中test相關(guān)的代碼抽取成獨(dú)立module后鸥拧,即可獨(dú)立運(yùn)行党远。
why?為什么需要組件化富弦?組件化有什么優(yōu)點(diǎn)沟娱?
組件獨(dú)立打包運(yùn)行,提升開發(fā)效率(提升10倍左右編譯效率):
這是我們平常的開發(fā)模式腕柜,當(dāng)我們有新業(yè)務(wù)時济似,會創(chuàng)建一個新的module A矫废,或者直接在module app中開發(fā),每次修改一行Java代碼砰蠢,進(jìn)行的增量編譯時間大概是50秒(自己電腦親測)蓖扑。為什么這么慢?因為這時候打的是整包台舱,每次編譯都會觸發(fā)app里所有代碼的javac律杠,dexMerge,packageApk等竞惋,然后去編譯一個完整的多鹿apk柜去。假設(shè)我只是修改Module A的一行代碼,需要去編譯APP的所有代碼嗎拆宛?如何只編譯Module A中我修改的代碼呢嗓奢?答案是:跳過APP,只打Module A的組件包浑厚。
如圖:為Module A單獨(dú)創(chuàng)建一個Module A App(這是一個Application)股耽,包含了Module A運(yùn)行所需要的必要依賴library、biz瞻颂、api和其它三方SDK豺谈,如:Arouter,Retrofit等(不需要像APP那樣依賴眾多的三方SDK)贡这。此時單獨(dú)運(yùn)行Module A App茬末,修改一行Java代碼的增量編譯時間僅僅只要5秒左右(原因可想而知:1.擺脫了APP,此時moduleA中的業(yè)務(wù)代碼極少盖矫;2.ModuleA移除了不必要的三方SDK和依賴丽惭,只有少量,必要的SDK)辈双。當(dāng)然责掏,如果這個module A要依賴更多的三方SDK,編譯速度也會有所下降湃望,所以要盡量保持組件module的最小化换衬,才能實(shí)現(xiàn)編譯效率的最大提升。開發(fā)階段证芭,通過Module A APP運(yùn)行組件包瞳浦,調(diào)試代碼;集成階段废士,通過Module APP運(yùn)行整包叫潦,完善與其他組件的聯(lián)調(diào),Module A則還是一個獨(dú)立的Library官硝,除了業(yè)務(wù)代碼矗蕊,無需其他任何改動短蜕。
動態(tài)化/插件化
組件化的最終目的是為了實(shí)現(xiàn)插件化,在運(yùn)行期間動態(tài)的替換傻咖,更新組件朋魔,實(shí)現(xiàn)業(yè)務(wù)功能的動態(tài)化。
高內(nèi)聚没龙,低耦合
如果不進(jìn)行組件化拆分铺厨,隨著APP代碼的增多缎玫,編譯會越來越慢
減少代碼耦合硬纤,提高代碼復(fù)用率
module拆分出來后,可以更方便的進(jìn)行單元測試赃磨,A/B Test等
組件微服務(wù)化筝家,更利于跨部門協(xié)作。大團(tuán)隊中邻辉,不同部門/團(tuán)隊負(fù)責(zé)不同組件溪王,發(fā)版前集成并編譯整包。小團(tuán)隊中值骇,如果可以將代碼打散成一個個module莹菱,每個人負(fù)責(zé)1-2個組件,各個模塊之間根據(jù)api層進(jìn)行解耦吱瘩,就可以最大限度的減少沖突道伟,實(shí)現(xiàn)更高效的并行開發(fā)。
編輯APK時使碾,可以根據(jù)BC端蜜徽,依賴不同的module,只將BC端中用到的代碼打進(jìn)apk票摇,可以優(yōu)化APK大小拘鞋。
基于多鹿/多鹿老師APP,可以拆出哪些組件矢门?
以業(yè)務(wù)為導(dǎo)向進(jìn)行拆分的module盆色,稱謂模塊,可實(shí)現(xiàn)C/B端的復(fù)用祟剔。例如:首頁模塊隔躲,動態(tài)列表模塊,消息模塊峡扩,我的模塊蹭越,動態(tài)發(fā)布模塊,寶寶模塊教届,班級模塊响鹃,學(xué)校模塊驾霜,直播模塊,成長冊模塊买置,小目標(biāo)模塊...等等
以技術(shù)為導(dǎo)向進(jìn)行拆分的module粪糙,稱謂組件,可實(shí)現(xiàn)不同模塊之間的復(fù)用忿项。例如:登錄組件蓉冈,播放器組件,支付組件轩触,分享組件寞酿,IM組件,短視頻處理組件...等等
總結(jié)
1.能立馬見到的收益:組件獨(dú)立運(yùn)行脱柱,提升10倍編譯效率伐弹。
2.長期收益:代碼架構(gòu)的健壯性,復(fù)用性榨为,和可擴(kuò)展性惨好。
how?如何創(chuàng)建出一個可以獨(dú)立運(yùn)行起來的組件随闺?
module接口層?
如圖日川,這是我們將要采用的方案2的組件開發(fā)架構(gòu)。但是還存在一個問題矩乐,就是不同module的循環(huán)依賴龄句。
假設(shè)moduleA可能依賴moduleB的某些方法,同時moduleB也會依賴moduleA某些方法的情況绰精,這樣會產(chǎn)生循環(huán)依賴撒璧,是不被允許的。解決方案:為A和B分別創(chuàng)建Module A-I笨使,Module B-I接口層卿樱,將A、B對外開放的model硫椰,interface繁调,或是event都放在接口層,A靶草、B分別實(shí)現(xiàn)A-I和B-I的接口協(xié)議蹄胰,彼此module之間不依賴,且不應(yīng)該知道彼此的實(shí)現(xiàn)細(xì)節(jié)奕翔,如下圖裕寨。
所以假設(shè)現(xiàn)在有個module A,要實(shí)現(xiàn)組件化拆分,就需要為這個module分別創(chuàng)建module A-I(對外暴露接口)宾袜,module A-App(獨(dú)立運(yùn)行組件)兩個module捻艳,APP應(yīng)該只依賴module A-I里面的接口,不該使用Module A的實(shí)現(xiàn)類庆猫。
多鹿APP基于組件化拆分的示例模板
標(biāo)準(zhǔn)的組件化module模板
module_template_api
ModuleTemplateApiEvent:組件對外暴露的event认轨,盡量把和這個組件相關(guān)的event放在module_api中,而不是api月培,保證內(nèi)聚嘁字,app或者其他module可以依賴這個組件的module_api。
public class ModuleTemplateApiEvent {
? ? public String status;
? ? public ModuleTemplateApiEvent(String status) {
? ? ? ? this.status = status;
? ? }
}
ModuleTemplateApiModel:組件對外暴露的model杉畜。
public class ModuleTemplateApiModel extends BaseReqModel {
? ? public String id;
? ? public String name;
}
ModuleTemplateApiScheme:組件對外暴露的scheme協(xié)議纪蜒。
/**
* 組件scheme協(xié)議
* @author listen
*/
public class ModuleTemplateApiScheme {
? ? public static final String MODULE_MAIN = "/module_template_api/main";
? ? public static final String MODULE_SERVICE_NAME = "/module_template_api/service";
}
ModuleTemplateApiService:組件對外暴露的接口協(xié)議(這里使用Arouter Service實(shí)現(xiàn))。
public interface ModuleTemplateApiService extends IProvider {
? ? /**
? ? * 獲取moduleName
? ? * @return moduleName
? ? */
? ? String getModuleName();
}
module_template
Constanst:常量
ModuleTemplateActivity:組件中示例代碼寻行,如網(wǎng)絡(luò)請求霍掺,發(fā)送消息匾荆,跳轉(zhuǎn)H5拌蜘,F(xiàn)lutter等
ModuleTemplateApplication:組件的內(nèi)部私有的初始化邏輯,假設(shè)當(dāng)前是直播組件牙丽,則這里就是zegoSDK的初始化邏輯简卧,保證直播相關(guān)邏輯內(nèi)聚在這個module中。
/**
* 當(dāng)前業(yè)務(wù)module內(nèi)部私有的初始化邏輯
*/
public class ModuleTemplateApplication extends Application {
? ? private static final String TAG = "ModuleTemplateApplication";
? ? private static Context sApplication;
? ? public static Context getInstance() {
? ? ? ? return sApplication;
? ? }
? ? @SuppressLint("LongLogTag")
? ? public ModuleTemplateApplication(Context application) {
? ? ? ? Log.d(TAG, "init");
? ? ? ? sApplication = application;
? ? }
}
ModuleTemplateServiceImpl:ModuleTemplateService的實(shí)現(xiàn)類烤芦,具體的邏輯在此處實(shí)現(xiàn)举娩,外部module依賴ModuleTemplateService,而不是ModuleTemplateServiceImpl
@Route(path = ModuleTemplateApiScheme.MODULE_SERVICE_NAME)
public class ModuleTemplateServiceImpl implements ModuleTemplateApiService {
? ? @Override
? ? public String getModuleName() {
? ? ? ? return Constants.MODULE_NAME;
? ? }
? ? @SuppressLint("LongLogTag")
? ? @Override
? ? public void init(Context context) {
? ? ? ? // 第一次ARouter.navigation()調(diào)用的時候构罗,會執(zhí)行init方法铜涉,且只會調(diào)用一次,可以在這里觸發(fā)module內(nèi)部的初始化邏輯
? ? ? ? Log.e("ModuleTemplateServiceImpl", "init=" + context);
? ? ? ? new ModuleTemplateApplication(context);
? ? }
}
module_template_run
MainActivity:歡迎頁遂唧,簡單的組件頁面跳轉(zhuǎn)
ModuleApplication:當(dāng)我們獨(dú)立運(yùn)行組件時芙代,需要一些網(wǎng)絡(luò)庫,圖片庫一些最基礎(chǔ)的初始化時盖彭,就可以在這里實(shí)現(xiàn)纹烹,簡單理解就是AndroidApp里面做的事情,需要搬到這里來≌俦撸現(xiàn)在里面只有Arouter和網(wǎng)絡(luò)庫的初始化邏輯铺呵,如果需要拆分IM或是直播組件,則這里初始化的SDK就需要增加了隧熙。
public class ModuleApplication extends ApiApplication {
? ? @Override
? ? protected void attachBaseContext(Context base) {
? ? ? ? super.attachBaseContext(base);
? ? ? ? MultiDex.install(this);
? ? ? ? EnvConfig.init(isDebug, 1, "1.0", "Android");
? ? ? ? AppInit.beforeInit(BuildConfig.DEBUG);
? ? }
? ? @Override
? ? public void onCreate() {
? ? ? ? super.onCreate();
? ? ? ? // 盡可能早片挂,推薦在Application中初始化
? ? ? ? ARouter.init(this);
? ? ? ? AppInit.onInit(BuildConfig.DEBUG);
? ? ? ? // 初始化ModuleTemplateRun組件
? ? ? ? ARouter.getInstance().build(ModuleTemplateApiScheme.MODULE_SERVICE_NAME).navigation();
? ? }
}
module創(chuàng)建之一鍵生成
在MaltBaby-Android的根目錄下,我寫了一個shell腳本"moduleCreate.sh",只要在根目錄下輸入:"./moduleCreate.sh module_a"音念,就會根據(jù)module_template的項目結(jié)構(gòu)滋将,創(chuàng)建出module_a,module_a_api症昏,module_a_run三個標(biāo)準(zhǔn)的組件化module随闽。同步下build.gradle文件后,就會在app執(zhí)行框中看到module_a_run肝谭,直接運(yùn)行即可掘宪。
問題
組件整理、抽取
新module創(chuàng)建:如果當(dāng)前需要創(chuàng)建的module是新的功能攘烛,則可以根據(jù)module_template直接copy即可魏滚。
老module抽取:如果當(dāng)前需要從app中拆分老的代碼成為module坟漱,則成本較高鼠次,需要將module依賴的所有類,xml芋齿,sdk抽取到module中腥寇,并把一些app中用到公用代碼,提取到library中觅捆,然后定義好api層赦役,供外部調(diào)用。
組件間的交互和通信
頁面跳轉(zhuǎn):使用Arouter進(jìn)行schme跳轉(zhuǎn)栅炒。頁面跳轉(zhuǎn)時掂摔,統(tǒng)一使用scheme還有個好處,就是未來可以進(jìn)行Native到H5的頁面降級赢赊。
方法調(diào)用(有入?yún)?出參):要調(diào)用別的Module的某個方法乙漓,并希望有返回值時,則使用Arouter Service實(shí)現(xiàn)释移,其實(shí)就是定義一個接口叭披,面向抽象編程。
單向通信(只有入?yún)ⅲ喝绻皇菃蜗虻南騽e的module發(fā)個消息秀鞭,或通知趋观,使用EventBus即可
數(shù)據(jù)傳遞:sp,sqlite锋边。比如登錄組件中皱坛,登錄成功后將UserInfo保存在sp中,別的module從sp中直接獲取即可豆巨。
組件的隔離(代碼和資源)
代碼隔離(推薦runtimeOnly依賴剩辟,不過datdbinding的編譯會失敗,所以先用implement):組件包括module和module_api兩個module,理論上module_api是接口抽象層贩猎,module是基于module_api層的具體實(shí)現(xiàn)類熊户,組件間相互依賴的時候,不該暴露太多細(xì)節(jié)吭服,只把需要讓外界知道的通過api層對外暴露即可嚷堡。這樣做是為了避免組件間產(chǎn)生不必要的耦合,畢竟組件化的收益之一就是降低耦合艇棕。組件代碼相互隔離的較好的情況下蝌戒,未來替換或更新組件才能成為可能。例如:播放器組件沼琉,只在api層對外暴露北苟,播放,暫停等常用接口打瘪,至于內(nèi)部實(shí)現(xiàn)是阿里云友鼻,還是七牛,外界不需要知道闺骚,就可以更方便的實(shí)現(xiàn)組件替換彩扔。
資源隔離:不同module的資源可能重命,可以通過resourcePrefix葛碧,給每個module添加資源前綴的校驗借杰,如果某個jpg,或是xml資源进泼,沒有按照這個前綴規(guī)則命名,就會報紅色警告纤虽。
TODO?
將app中的業(yè)務(wù)代碼拆分成module乳绕,最后app應(yīng)該只是個殼工程,負(fù)責(zé)將不同module引入并串聯(lián)起來
將所有module上傳到maven逼纸,不同module之間依賴抽象api洋措,不依賴具體實(shí)現(xiàn),每個module支持版本升級杰刽,降級菠发。
每個組件module單獨(dú)編譯運(yùn)行,若依賴其他若干module贺嫂,可以按需引入滓鸠,若干個組件module組合后,編譯運(yùn)行第喳。
B/C端APK打包時糜俗,只依賴各自業(yè)務(wù)module