背景
什么是組件化?
組件化就是模塊化,在Android工程實(shí)踐中可以實(shí)現(xiàn)單獨(dú)編譯搂橙、運(yùn)行澎怒、調(diào)試。 --個(gè)人見解為什么要組件化宦棺?
A. 解耦
B. 代碼隔離
C. 團(tuán)隊(duì)協(xié)作組件化的具體操作?
請(qǐng)看下文
組件化的具體操作
Git 倉庫地址:
https://github.com/Wangct23/Component
Demo整體架構(gòu)簡要說明
先介紹下整個(gè)Demo,分為應(yīng)用層踪宠,組件層,基礎(chǔ)層妈嘹。
應(yīng)用層為App柳琢,將所有組件結(jié)合起來。
組件層包含兩個(gè)組件:Login和Share〖砹常基礎(chǔ)層包含公共依賴他去。
Login組件負(fù)責(zé)用戶登錄,并記錄用戶的登錄狀態(tài)和用戶信息倒堕;
Share組件負(fù)責(zé)分享到第三方平臺(tái)灾测,在分享之前需要調(diào)用Login組件提供的服務(wù)來驗(yàn)證用戶是否登錄。
整個(gè)App的Module結(jié)構(gòu)是如下圖這樣的垦巴,詳細(xì)可以參看Demo中的build.gradle查看具體依賴關(guān)系:
在實(shí)際項(xiàng)目中在基礎(chǔ)層還應(yīng)包含網(wǎng)絡(luò)庫媳搪、圖片庫等基礎(chǔ)Library,本Demo旨在演示組件相關(guān)的基本操作骤宣,期望本文能夠把組件化的核心內(nèi)容表達(dá)清楚蛾号,相信明白組件化的核心思想和操作后,再往上進(jìn)階是水到渠成的事情涯雅。網(wǎng)上也流傳著不同大廠各自的實(shí)踐鲜结,均可以參考。實(shí)際使用的時(shí)候活逆,還是要以項(xiàng)目的業(yè)務(wù)特點(diǎn)以及團(tuán)隊(duì)的配置為基礎(chǔ)靈活選型和設(shè)計(jì)精刷,切忌生搬硬套。因此本文沒有將網(wǎng)絡(luò)請(qǐng)求之類的基礎(chǔ)庫包含在內(nèi)蔗候。
將Module單獨(dú)編譯運(yùn)行 V.S. 作為Library供其他Module依賴使用
背景:為什么需要單獨(dú)編譯運(yùn)行一個(gè)Module怒允?
當(dāng)團(tuán)隊(duì)規(guī)模達(dá)到一定數(shù)量時(shí),就要根據(jù)情況劃分為不同的小團(tuán)隊(duì)各自負(fù)責(zé)自己的業(yè)務(wù)锈遥。這種情況下纫事,不同Module之間定義好清晰的接口,各團(tuán)隊(duì)獨(dú)立互不影響的開發(fā)自己負(fù)責(zé)的業(yè)務(wù)所灸,并單獨(dú)編譯丽惶、運(yùn)行、調(diào)試爬立,以及Test钾唬,就顯得尤為重要了。
下面演示如何操作侠驯。
我們通過Android Studio在Project中先創(chuàng)建兩個(gè)Module:login-impl抡秆,以及share-impl。
以share-impl為例吟策,創(chuàng)建過程如下圖所示:
|
創(chuàng)建順序在上圖中以箭頭為標(biāo)示儒士,分別為圖1 -> 圖2 -> 圖3 -> 圖4,需要注意每一步中高亮選中的選項(xiàng)和Module Type檩坚。特別是圖2着撩,沒有選擇在創(chuàng)建普通Android Module時(shí)常用的Android Library诅福,而是選擇了Phone & Tablet Module,兩者的區(qū)別在于下圖:
|
左側(cè)是選擇Phone & Tablet Module后的默認(rèn)build.gradle 文件睹酌,右側(cè)是選擇 Android Library后默認(rèn)的build.gradle 文件权谁,兩者的主要區(qū)別在于紅框內(nèi)字段配置不一樣剩檀。左側(cè)可以作為一個(gè)獨(dú)立app單獨(dú)編譯運(yùn)行憋沿,不能作為library被其他Module依賴;右側(cè)可以作為一個(gè)library被其他Module依賴沪猴,不能作為獨(dú)立的app單獨(dú)編譯運(yùn)行辐啄。
下面我們也會(huì)通過條件變量的方式將當(dāng)前Module動(dòng)態(tài)設(shè)置為Library供其他Module依賴,或者設(shè)置為Application运嗜,單獨(dú)編譯運(yùn)行壶辜。
以login-impl為例:
|
如上圖所示,在login-impl目錄下担租,創(chuàng)建gradle.properties文件砸民,然后打開文件,添加 isRunAlone變量(名字隨意)奋救,并設(shè)置為true岭参。
然后修改login-impl目錄下的build.gradle 文件,如下圖所示尝艘,左側(cè)為修改前演侯,右側(cè)為修改后。
|
修改分為兩處:
- 在文件頭部背亥,將引入的plugin通過條件變量來控制秒际,當(dāng)isRunAlone為true時(shí),執(zhí)行plugins.apply('com.android.application')狡汉,將當(dāng)前Module作為Application單獨(dú)編譯運(yùn)行娄徊;當(dāng)isRunAlone為false時(shí),執(zhí)行plugins.apply('com.android.library')盾戴,將當(dāng)前Module作為Library供其他Module依賴使用嵌莉。
- 動(dòng)態(tài)設(shè)置是否添加 ?applicationId "com.wct.login_impl"?,作為Library時(shí)去掉捻脖,作為Application時(shí)執(zhí)行锐峭。
我們通過Android Studio可以很容易看出不同設(shè)置的區(qū)別,參看下圖:
|
- (1)如上圖可婶,當(dāng)gradle.properties文件中isRunAlone設(shè)置為true時(shí)沿癞,通過Android Studio可以看到login-impl Module和app Module一樣,可以選中矛渴,并且可以運(yùn)行
|
(2)如上圖椎扬,當(dāng)gradle.properties文件中isRunAlone設(shè)置為false時(shí)惫搏,通過Android Studio可以看到login-impl Module和被標(biāo)記了×號(hào),這種情況下就不能單獨(dú)編譯運(yùn)行了蚕涤。我們點(diǎn)擊圖片頂部右側(cè)綠色運(yùn)行按鈕筐赔,會(huì)彈出Edit Configuration彈窗,并提示Error揖铜,如下圖:
|
動(dòng)態(tài)加載不同的AndroidManifest.xml 文件
首先茴丰,為什么要?jiǎng)討B(tài)加載不同的AndroidManifest.xml文件呢?
因?yàn)楫?dāng)一個(gè)Module作為Application時(shí)與作為Library時(shí)的構(gòu)造是不同的天吓。如下圖所示:
|
簡單來說贿肩,當(dāng)一個(gè)Module作為Library時(shí),不需要生命<application></application>內(nèi)的諸多必要元素龄寞,而作為Application汰规,這些都是不可或缺的,不然編譯器就會(huì)報(bào)錯(cuò)物邑。
接著溜哮,演示一下動(dòng)態(tài)加載不同AndroidManifest.xml文件的方式之一:
- 在login-impl目錄下創(chuàng)建manifest目錄,然后在剛才創(chuàng)建的manifest目錄下創(chuàng)建AndroidManifest.xml文件色解,我們期望將此文件作為獨(dú)立編譯運(yùn)行時(shí)會(huì)加載的文件茂嗓。因?yàn)槭仟?dú)立編譯運(yùn)行,因此和app Module下面的AndroidManifest.xml文件的結(jié)構(gòu)應(yīng)該是一致的冒签,因此可以直接將其拷貝過來在抛,然后修改下內(nèi)容就可以了,如下圖所示:
|
- 在build.gradle文件中根據(jù)我們之前在gradle.properties文件中設(shè)置的isRunAlone參數(shù)萧恕,動(dòng)態(tài)加載不同路徑的AndroidManifext.xml刚梭。如下代碼所示:
android {
...
defaultConfig {
...
sourceSets {
main {
// 單獨(dú)調(diào)試與集成調(diào)試時(shí)使用不同的 AndroidManifest.xml 文件
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
...
}
以上,便是動(dòng)態(tài)加載不同AndroidManifest.xml文件的一種方法票唆。詳細(xì)內(nèi)容朴读,可以參見Demo源碼。
依賴隔離&代碼隔離
在Module內(nèi)部使用的類或方法走趋,很有可能會(huì)改動(dòng)或者刪除衅金,因此當(dāng)我們能直接訪問到其他Module 內(nèi)部使用的類和方法時(shí),會(huì)非常危險(xiǎn)簿煌。當(dāng)對(duì)方的類或方法內(nèi)部邏輯改動(dòng)時(shí)氮唯,很可能會(huì)導(dǎo)致我們自己的Module運(yùn)行結(jié)果出錯(cuò)。
在多團(tuán)隊(duì)協(xié)作的場(chǎng)景姨伟,為了避免在Moudle內(nèi)部使用的方法被外部調(diào)用惩琉,我們需要實(shí)現(xiàn)依賴隔離&代碼隔離 。在這部分文字中還將介紹一下整個(gè)組件化的樞紐:ServiceManager
下面介紹一種實(shí)現(xiàn)方法:
我們將每個(gè)組件分為 api 和 impl 兩個(gè)Module夺荒。其中api僅包含對(duì)外開放的接口和對(duì)應(yīng)的空實(shí)現(xiàn)瞒渠,impl包含接口的實(shí)現(xiàn)和其內(nèi)部邏輯良蒸,如下圖:
|
其中IAccountService是login組件對(duì)外提供的服務(wù),所有方法均在接口中聲明伍玖。需要獲取用戶的登錄信息的Module只允許依賴 api嫩痰,不允許依賴 impl,這從代碼的層級(jí)隔離了impl中的內(nèi)容窍箍。因此串纺,對(duì)于impl來說,只要保障api中對(duì)外接口的運(yùn)算結(jié)果不發(fā)生變化仔燕,我們可以根據(jù)業(yè)務(wù)需求自由的調(diào)整impl的內(nèi)部邏輯造垛,不用擔(dān)心會(huì)對(duì)外產(chǎn)生影響魔招。
再來看下具體怎么操作:
- 在分享組件中晰搀,我們需要用到用戶的登錄信息,因此需要依賴登錄組件的api Module办斑,即 login-api:
|
- 判斷用戶的登錄狀態(tài)外恕,當(dāng)用戶處于登錄狀態(tài)時(shí)允許分享,當(dāng)用戶處于未登錄狀態(tài)時(shí)禁止分享乡翅。
|[圖片上傳中...(image-81344c-1604741808414-1)]
如上圖所示鳞疲,我們?cè)赟hareService中,通過ServiceManager獲取到IAccountService的一個(gè)實(shí)例蠕蚜,然后調(diào)用其方法實(shí)現(xiàn)分享的邏輯尚洽。
接下來介紹一下各個(gè)組件之間的樞紐:ServiceManager
前面介紹了代碼隔離,各個(gè)組件通過依賴對(duì)方的api來獲取對(duì)應(yīng)的實(shí)現(xiàn)靶累,然后使用其提供的服務(wù)腺毫,那么,在不能通過new來獲取對(duì)方實(shí)例的情況下挣柬,怎么獲取呢潮酒?其實(shí)現(xiàn)的原理是什么呢?
答案是:反射邪蛔。
接下來介紹下具體操作:
- 首先新建一個(gè)componenbase Module急黎,作為所有組件的公共依賴庫,在componentbase中侧到,我們?yōu)楦鱾€(gè)組件提供服務(wù)獲取的能力勃教。
- 在 componnetbase中,我們創(chuàng)建IService接口匠抗,以及ServiceManager類故源。其中,IService是所有組件對(duì)外接口類的公共基類戈咳,每個(gè)接口都要繼承心软;在ServiceManager中壕吹,我們提供一個(gè)Map用來存放服務(wù)接口和對(duì)應(yīng)實(shí)現(xiàn)的映射。在各個(gè)組件初始化的時(shí)候删铃,將自己的實(shí)現(xiàn)注冊(cè)進(jìn)來耳贬,這里稍后會(huì)說明。文字描述起來有點(diǎn)費(fèi)解猎唁,看下代碼和注釋:
public class ServiceManager {
private static Application sApplication;
/**
* 用來存放接口類和其對(duì)應(yīng)實(shí)例
*/
private static final ConcurrentHashMap<Class, Object> SERVICES = new ConcurrentHashMap<>();
public static void init(Application application) {
sApplication = application;
}
public static Application getApplication() {
return sApplication;
}
/**
* 獲取對(duì)應(yīng)的接口的實(shí)例
* @param clazz 接口類
* @param <T>
* @return 接口類對(duì)應(yīng)的實(shí)例咒劲。對(duì)于未注冊(cè)的接口類,返回其對(duì)應(yīng)的空實(shí)現(xiàn)诫隅。
*/
public static <T extends IService> T getService(Class<T> clazz) {
T impl = (T) SERVICES.get(clazz);
if (impl == null) {
impl = getEmptyImpl(clazz);
registerService(clazz, impl);
}
return impl;
}
/**
* 注冊(cè)接口類和其對(duì)應(yīng)的實(shí)現(xiàn)
* @param clazz
* @param obj
* @param <T> 接口類
*/
public static <T extends IService> void registerService(Class<T> clazz, T obj) {
SERVICES.put(clazz, obj);
}
/**
* 獲取接口類對(duì)應(yīng)的空實(shí)現(xiàn)
* @param klass
* @param <T>
* @param <C>
* @return
*/
@NonNull
private static <T, C> T getEmptyImpl(Class<C> klass) {
String fullPackage = klass.getPackage().getName();
String name = klass.getSimpleName();
name = name.substring(1); //去掉接口類名稱的首字母 I腐魂,eg: IAccountService --> AccountService
final String implName = fullPackage + "." + name + "Empty"; // AccountService --> AccountServiceEmpty
try {
@SuppressWarnings("unchecked") final Class<T> aClass = (Class<T>) Class.forName(implName);
return aClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find implementation for "
+ klass.getCanonicalName() + ". " + implName + " does not exist");
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access the constructor"
+ klass.getCanonicalName());
} catch (InstantiationException e) {
throw new RuntimeException("Failed to create an instance of "
+ klass.getCanonicalName());
}
}
}
各個(gè)組件在初始化的時(shí)候?qū)⒆约簩?duì)外服務(wù)的實(shí)現(xiàn)注冊(cè)進(jìn)ServiceManager,以login-impl為例:
public class LoginApp extends BaseApp {
...
@Override
public void init(Application application) {
Log.i("Application", "LoginApp init");
ServiceManager.registerService(IAccountService.class, new AccountService());
}
}
以上逐纬,介紹了一種實(shí)現(xiàn)代碼隔離&實(shí)現(xiàn)隔離的方法蛔屹。
資源沖突
當(dāng)不同組件中出現(xiàn)名稱相同的資源文件時(shí),先加載的文件會(huì)被后加載的文件覆蓋豁生,這當(dāng)然不是我們想要的兔毒。比如:不同組件都有返回按鈕back.png文件,而各自的樣式不同甸箱,那么如果都命名為back.png育叁,那么先加載的文件就會(huì)被后加載的文件覆蓋,那么最終所有組件展現(xiàn)出的返回按鈕都會(huì)變成最后加載的back.png的樣式芍殖,這當(dāng)然不是我們想要的豪嗽。
不過,這種情況很容易解決豌骏,團(tuán)隊(duì)之間只需要協(xié)商好彼此的命名規(guī)范即可龟梦,比如:在所有資源名稱前都加上組件的名稱作為前綴。
Gradle 的Android插件還提供了一種自動(dòng)檢查的方法肯适,以login組件為例变秦,當(dāng)我們?cè)赽uild.gradle中加上如下配置,那么gradle自動(dòng)檢查 res 中 xml 文件的命名是否以 "login_"開頭框舔,如果不是蹦玫,會(huì)報(bào)錯(cuò)。不過這種方法僅限于res 中的 xml 文件刘绣,對(duì)于圖片等資源需要開發(fā)者人工檢查了樱溉。
android {
resourcePrefix "login_"
// 其他配置 ...
}
插播:剩下的內(nèi)容不多了,如果你能堅(jiān)持讀到這里纬凤,我真的是非常開心了福贞。
頁面跳轉(zhuǎn)
在Android中常用的頁面跳轉(zhuǎn)方式有兩種:顯示Intent和隱式Intent,以及路由(底層實(shí)現(xiàn)也是startActivity())停士。本文介紹通過顯示Intent實(shí)現(xiàn)頁面跳轉(zhuǎn)的方式挖帘。
因?yàn)榻M件之間的代碼隔離完丽,無法直接訪問其他Module的類,因此無法直接通過startActivity來實(shí)現(xiàn)跳轉(zhuǎn)拇舀。不過逻族,可以反過來調(diào)用來實(shí)現(xiàn)。
舉例:
在AccountService類中骄崩,實(shí)現(xiàn)如下方法
//AccountService.java
@Override
public void startLoginActivity(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
在要跳轉(zhuǎn)LoginActivity頁面的組件中聘鳞,調(diào)用此方法就可以了:
//ShareActivity.java
public void shareLogin(View view) {
// ARouter.getInstance().build("/loginimpl/login").navigation(); // 通過路由的方式實(shí)現(xiàn)頁面跳轉(zhuǎn)
IAccountService accountService = ServiceManager.getService(IAccountService.class);
accountService.startLoginActivity(this); //通過接口的方式實(shí)現(xiàn)頁面跳轉(zhuǎn)
}
雖然沒辦法直接方位對(duì)方Module的類,也可以通過上面這種方式迂回的startActivity()要拂。
在Demo中也提供了通過ARouter實(shí)現(xiàn)頁面跳轉(zhuǎn)的示例抠璃,網(wǎng)絡(luò)上流行很多關(guān)于ARouter的介紹,而ARouter不屬于本文的重點(diǎn)脱惰,在這里就不詳細(xì)說了搏嗡。
動(dòng)態(tài)配置Application
前面講到,理論上每個(gè)組件都支持單獨(dú)編譯運(yùn)行以及與其他組件一起集成調(diào)試枪芒。那么在這兩種不同場(chǎng)景彻况,一般情況下都需要加載不同的Application谁尸。
做法也很簡單舅踪,以login-impl為例,
- 在build.gradle中修改配置:
android {
...
defaultConfig {
...
sourceSets {
main {
// 單獨(dú)調(diào)試與集成調(diào)試時(shí)使用不同的 AndroidManifest.xml 文件
if (isRunAlone.toBoolean()) {
java {
///當(dāng)此Module作為單獨(dú)的Application運(yùn)行時(shí)良蛮,加載 "src/component"路徑下的文件
srcDirs "src/component"
}
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
...
}
- 在src/component 目錄下創(chuàng)建單獨(dú)運(yùn)行時(shí)的Application
|
- 修改之前配置的單獨(dú)運(yùn)行時(shí)生效的AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wct.login_impl">
<application
android:name=".ComponentApp"`
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/login_app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/login_AppTheme">
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
總結(jié)
至此抽碌,關(guān)于組件化的核心內(nèi)容就介紹差不多了,需要再次說明的是决瞳,本文所述并非組件化的標(biāo)準(zhǔn)模板货徙,而是組件化萬般形態(tài)中的一種,而且是比較初級(jí)的一種皮胡。
另外限于篇幅痴颊,諸如更多數(shù)據(jù)通信的方式、路由屡贺、公共基礎(chǔ)庫的設(shè)計(jì)蠢棱、公共組件的抽取等內(nèi)容沒在文中擴(kuò)展。
這只是組件化的一個(gè)開始甩栈,實(shí)際的組件化過程遠(yuǎn)沒有這么簡單泻仙。
Git 倉庫地址:
https://github.com/Wangct23/Component
條條大路通紐約,在了解清楚業(yè)務(wù)模型以及思考清晰架構(gòu)目標(biāo)后量没,我們可以自由選擇不同的實(shí)現(xiàn)方式玉转。
_ 感謝閱讀 _