在項(xiàng)目開發(fā)過程中,隨著業(yè)務(wù)與人員的增加惩琉,如果沒有提前使用合理的架構(gòu),代碼會變得越來越臃腫夺荒,功能耦合性也越來越高瞒渠。為了代碼的質(zhì)量良蒸,這時候我們需要對工程進(jìn)行重構(gòu)。
比較簡單的重構(gòu)方案就是代碼按照模塊劃分伍玖,也就是Android中的module概率嫩痰,每一個module對應(yīng)一個模塊,各自的代碼在各自的module中提交窍箍,這種情況下是合理的串纺,但是不同的模塊有可能涉及相同的功能。
比如一個模塊有購買功能仔燕,而另一個模塊也有購買功能造垛,這時候無論把購買功能的代碼放到哪個模塊都不能避免模塊間的耦合,因此為了解決這種問題就有了組件化的思想晰搀。
一五辽、組件化與模塊化的對比
模塊化:模塊化對應(yīng)的是獨(dú)立業(yè)務(wù)模塊。
組件化:組件化是單一的功能組件外恕,每一個都可以使用module開發(fā)并可以提供sdk發(fā)布使用杆逗。
對比而言模塊化是業(yè)務(wù)為導(dǎo)向,而組件化是功能為導(dǎo)向鳞疲。
模塊可以包含多個組件罪郊,因此模塊化的顆粒度明顯大于組件化。
兩者的目的都是一致的都是為了工程的解耦尚洽。
組件化基礎(chǔ)架構(gòu)圖:
上面是最簡單的組件化架構(gòu)圖
base是基類悔橄,集成常用的基礎(chǔ)框架
login/pay是組件module,依賴base腺毫,提供各種的功能需求癣疟。
最上層的app是模塊或者主工程,依賴這三個組件完成業(yè)務(wù)的需求潮酒。
二睛挚、組件化面臨的問題
組件化開發(fā)過程中需要面臨的幾個問題:
-
1.工程中每個組件都需要獨(dú)立運(yùn)行的能力也需要被集成到主工程運(yùn)行,這樣也能夠提升組件的編譯與運(yùn)行速度急黎,節(jié)省開發(fā)時間扎狱。
-
2.組件間的數(shù)據(jù)傳遞與方法的調(diào)用,這是我們需要解決的勃教。因?yàn)榻M件之間可能需要狀態(tài)判定淤击,比如購買需要登錄的狀態(tài),那么如何優(yōu)雅的獲取組件的數(shù)據(jù)是一個問題故源。
-
3.如何在不強(qiáng)依賴的情況下優(yōu)雅的完成組件之間界面的跳轉(zhuǎn)污抬。
-
4.如何在不強(qiáng)依賴的情況下優(yōu)雅的完成組件Fragment的在主工程的創(chuàng)建
-
5.主工程集成調(diào)試時,如何在不依賴工程的情況下主工程也不報錯心软。
-
6.如何實(shí)現(xiàn)代碼隔離壕吹,資源隔離
三著蛙、組件實(shí)現(xiàn)獨(dú)立調(diào)試與集成調(diào)試
Android Studio使用Gradle構(gòu)建工程,Gradle提供了三種插件耳贬,通過配置不同的插件構(gòu)建不同的工程踏堡。
- App 插件,id: com.android.application
- Library 插件咒劲,id: com.android.library
- Test 插件顷蟆,id: com.android.test
base是純粹的library,因此只需要引用library即可
apply plugin: 'com.android.library'
llogin/pay組件則需要做些額外的配置腐魂,因?yàn)樗鼈冃枰С旨傻絘pp項(xiàng)目中帐偎,也需要能夠支持獨(dú)立運(yùn)行。
在集成到主工程中時蛔屹,它們是library削樊。獨(dú)立運(yùn)行時,它們是Application兔毒。因此就需要配置一個字段來設(shè)置該工程是否是library還是Application漫贞。
3.1動態(tài)配置插件與applicationId
在module中新增gradle.properties文件,文件中配置
isRunAlone = false //true代表獨(dú)立運(yùn)行育叁,false代表是以library形式運(yùn)行迅脐。
在module的build.gradle文件中,依賴插件語句針對新增字段做判斷:
if (isRunAlone.toBoolean()) {
//獨(dú)立運(yùn)行
apply plugin: 'com.android.application'
} else {
//依賴主工程運(yùn)行
apply plugin: 'com.android.library'
}
同時豪嗽,作為library不需要applicationId谴蔑,所以需要在配置中增加對新增字段的處理:
defaultConfig {
if (isRunAlone.toBoolean()) {
applicationId "com.xj.paymodule"
}
...
}
3.2動態(tài)配置manifest文件
module在獨(dú)立運(yùn)行時有自己的主入口文件,在集成到主工程時如果不處理龟梦,主工程合并manifest后會導(dǎo)致多個入口出現(xiàn),這是個問題隐锭。
因此我們需要額外一份manifest文件,然后在gradle文件中動態(tài)配置manifest調(diào)用。
如圖:
主工程依賴:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xj.loginmodule">
<application
android:theme="@style/AppTheme">
<activity android:name="com.xj.loginmodule.LoginActivity">
</activity>
</application>
</manifest>
獨(dú)立運(yùn)行:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xj.loginmodule">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".LoginApp"
android:theme="@style/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>
在gradle中動態(tài)配置manifest.srcFile屬性
android{
...
sourceSets {
main{
if (isRunAlone.toBoolean()){
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
此時組件化的第一步已經(jīng)完成了变秦。這時候通過更改新增屬性的值就可以動態(tài)的配置module獨(dú)立運(yùn)行或者依賴主工程運(yùn)行成榜。
四框舔、組件間數(shù)據(jù)傳遞與方法的相互調(diào)用
這時候我們需要考慮另一個問題,組件之間通信問題蹦玫。比如pay組件在購買時需要判斷登錄狀態(tài),那么就需要獲取login組件的狀態(tài)刘绣。當(dāng)然我們可以通過直接依賴的形式來處理樱溉,但是這就違背了組件相互獨(dú)立的原則,所以需要我們解決這個問題纬凤。這里我們采用了接口的方式來解決這個問題福贞。
4.1新增componsentbase工程
新增名為componsentbase的module工程,定義Service接口停士。創(chuàng)建Factory類,為所有Service提供調(diào)用方法挖帘。
工程圖如下:
public interface IAccountService {
boolean isLogin();
String getAccountId();
}
public class ServiceFactory {
private IAccountService mAccountService;
private ServiceFactory(){
}
public static ServiceFactory getInstance(){
return Inner.serviceFactory;
}
private static class Inner {
private static ServiceFactory serviceFactory = new ServiceFactory();
}
public void setAccountService(IAccountService service){
mAccountService = service;
}
public IAccountService getAccountService(){
if (null == mAccountService){
mAccountService = new EmptyAccountService();
}
return mAccountService;
}
}
4.2組件負(fù)責(zé)接口的實(shí)現(xiàn)完丽,并完成設(shè)值
LoginModule中提供接口的實(shí)現(xiàn),并在Application中完成注冊:
//實(shí)現(xiàn)
public class LoginService implements IAccountService {
@Override
public boolean isLogin() {
return null != AccountUtils.getInstance().getAccountInfo();
}
@Override
public String getAccountId() {
return null == AccountUtils.getInstance().getAccountInfo() ? "" :
AccountUtils.getInstance().getAccountInfo().accountId;
}
}
//注冊
public class LoginApp extends Application {
@Override
public void onCreate() {
ServiceFactory.getInstance().setAccountService(new LoginService());
}
4.3組件之間調(diào)用
pay組件調(diào)用:
if (ServiceFactory.getInstance().getAccountService().isLogin()){
Toast.makeText(PayActivity.this,"購買成功",Toast.LENGTH_LONG).show();
} else {
Toast.makeText(PayActivity.this,"請先登錄",Toast.LENGTH_LONG).show();
}
通過這樣的方法完成組件間交互拇舀,避免組件之間相互依賴逻族。這時候我們發(fā)現(xiàn)一個問題,LoginApp組件只有在獨(dú)立運(yùn)行的時候才會調(diào)用骄崩,集成運(yùn)行時聘鳞,組件的Application是主工程的Application,因此我們需要處理一下集成運(yùn)行時各組件Application配置注冊的問題要拂。
我們采用反射的技術(shù)來完成各組件Application動態(tài)配置的問題抠璃。
在componsentbase工程中創(chuàng)建BaseApp類,該類是抽象類繼承Application。
4.4組件Application數(shù)據(jù)初始化
public abstract class BaseApp extends Application {
public abstract void initModuleConfig(Application application);
public abstract void initModileData(Application application);
}
提供了初始化的方法脱惰。
主工程Application和組件Application都繼承BaseApp,并實(shí)現(xiàn)其抽象方法搏嗡。接口的注冊等相關(guān)調(diào)用都移入到實(shí)現(xiàn)方法中。
Login組件:
public class LoginApp extends BaseApp {
private static final String TAG = "LoginApp";
@Override
public void onCreate() {
super.onCreate();
//獨(dú)立運(yùn)行會調(diào)用
initModuleConfig(this);
}
@Override
public void initModuleConfig(Application application) {
ServiceFactory.getInstance().setAccountService(new LoginService());
}
@Override
public void initModileData(Application application) {
}
}
主工程:
主工程中配置需要反射調(diào)用的類名拉一。然后在Application中反射調(diào)用彻况。
public class MainApp extends BaseApp {
private static final String TAG = "MainApp";
//組件的application名稱
private static final String LoginApp = "com.xj.loginmodule.LoginApp";
//配置需要反射application數(shù)組
public static String[] moudleApps = {LoginApp};
@Override
public void onCreate() {
super.onCreate();
//調(diào)用
initModuleConfig(this);
initModileData(this);
}
@Override
public void initModuleConfig(Application application) {
//反射調(diào)用initModuleConfig方法
for (String appName : moudleApps){
try {
Class<?> appClass = Class.forName(appName);
BaseApp baseApp = (BaseApp) appClass.newInstance();
baseApp.initModuleConfig(application);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
@Override
public void initModileData(Application application) {
//反射調(diào)用initModileData方法
for (String appName : moudleApps){
try {
Class<?> appClass = Class.forName(appName);
BaseApp baseApp = (BaseApp) appClass.newInstance();
baseApp.initModileData(application);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
}
運(yùn)行日志:
=====MainApp onCreate =====
=====MainApp initModuleConfig =====
=====LoginApp initModuleConfig =====
=====MainApp initModileData =====
=====LoginApp initModileData =====
這樣不通過強(qiáng)依賴的就完成了依賴組件的配置。當(dāng)然也有缺點(diǎn)就是需要手動配置依賴的Application名稱舅踪。
到現(xiàn)在纽甘,組件化的搭建就已經(jīng)基本完成了。我們可以通過主工程運(yùn)行一下:
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,LoginActivity.class);
startActivity(intent);
}
});
findViewById(R.id.pay).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, PayActivity.class);
startActivity(intent);
}
});
正常抽碌!
五烁兰、組件間之間優(yōu)雅的調(diào)用
測試代碼是通過顯示意圖來實(shí)現(xiàn)界面間的跳轉(zhuǎn),顯然不符合我們解耦的需要。畢竟組件支持可配置固逗,如果當(dāng)前主工程不依賴組件,那么工程就會編譯報錯這是不能容忍的晨仑。當(dāng)前我們可以使用隱式意圖,但是隱示式圖需要通過manifest文件管理痴颊,協(xié)作比較麻煩赏迟,所以我們采用更靈活的方式,使用開源的 ARouter 來實(shí)現(xiàn)蠢棱。
一個用于幫助 Android App 進(jìn)行組件化改造的框架 —— 支持模塊間的路由锌杀、通信、解耦
路由是指從一個接口獲取數(shù)據(jù)包泻仙,從數(shù)據(jù)包中獲取路由包的目的路徑并進(jìn)行定向轉(zhuǎn)發(fā)到另一個接口的過程糕再。因此可以用來組件化解耦。
要進(jìn)行ARoute跳轉(zhuǎn)就需要進(jìn)行組件對ARoute依賴玉转,組件依賴于Base組件突想,所以我們需要在Base組件中依賴ARoute。
5.1ARoute組件引入
ARoute依賴:
Base工程:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
...
}
dependencies {
...
api 'com.alibaba:arouter-api:1.5.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
其余任何依賴需要使用ARouter的module:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
...
}
dependencies {
...
//需要 不然生成不了路由表
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
5.2ARoute組件初始化:
主工程Application中:
public class MainApp extends BaseApp {
private static final String TAG = "MainApp";
@Override
public void onCreate() {
super.onCreate();
...
if (isDebug()) { // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運(yùn)行猾担,必須開啟調(diào)試模式袭灯!線上版本需要關(guān)閉,否則有安全風(fēng)險)
}
ARouter.init(this);
...
}
}
5.3ARoute組件路由注冊
路由注冊:
通過注解Route,path中/xx/xx為路徑 主要路徑至少兩級。
@Route(path = "/login/login")
public class LoginActivity extends AppCompatActivity {}
@Route(path = "/pay/pay")
public class PayActivity extends AppCompatActivity {}
5.4組件跳轉(zhuǎn):
通過path跳轉(zhuǎn)
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build("/login/login").navigation();
}
});
findViewById(R.id.pay).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build("/pay/pay").navigation();
}
});
這樣就完成主工程與組件之間的解耦绑嘹。
5.5路由過濾攔截功能
pay模塊調(diào)用時要依賴login狀態(tài)妓蛮,登錄成功時,則進(jìn)行pay圾叼,否則中斷調(diào)用蛤克。
下面針對此業(yè)務(wù)編寫簡單的攔截器:
// 比較經(jīng)典的應(yīng)用就是在跳轉(zhuǎn)過程中處理登陸事件,這樣就不需要在目標(biāo)頁重復(fù)做登陸檢查
// 攔截器會在跳轉(zhuǎn)之間執(zhí)行夷蚊,多個攔截器會按優(yōu)先級順序依次執(zhí)行
@Interceptor(priority = 8,name = "登錄攔截器")
public class LoginInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//callback.onContinue(postcard); // 處理完成构挤,交還控制權(quán)
// callback.onInterrupt(new RuntimeException("我覺得有點(diǎn)異常")); // 覺得有問題,中斷路由流程
// 以上兩種至少需要調(diào)用其中一種惕鼓,否則不會繼續(xù)路由
if (TextUtils.equals(postcard.getPath(),"/pay/pay")){
if (ServiceFactory.getInstance().getAccountService().isLogin()){
callback.onContinue(postcard); // 處理完成筋现,交還控制權(quán)
} else {
callback.onInterrupt(new RuntimeException("請先登錄"));// 覺得有問題,中斷路由流程
}
} else {
callback.onContinue(postcard); // 處理完成箱歧,交還控制權(quán)
}
}
@Override
public void init(Context context) {
// 攔截器的初始化矾飞,會在sdk初始化的時候調(diào)用該方法,僅會調(diào)用一次
}
}
攔截器需要實(shí)現(xiàn)IInterceptor呀邢,重寫process方法洒沦。priority是優(yōu)先級,name是攔截器描述价淌。攔截器在跳轉(zhuǎn)之間調(diào)用申眼。觸發(fā)process方法后,callback.onContinue和 callback.onInterrupt必須要調(diào)用一個蝉衣,不然不會繼續(xù)路由括尸。上面代碼判斷是否是pay跳轉(zhuǎn),是的話則判斷是否登錄病毡,登錄則繼續(xù)路由濒翻,否則中斷路由。
六啦膜、組件之間優(yōu)雅的獲取Fragment
除了Activity有送,Android中也有需要Fragment的獲取。通常情況我們會直接new Fragment來引用功戚。但是現(xiàn)在為了主工程與組件之間的解耦娶眷,直接new的方式就不適用了似嗤。因此我們需要額外的方式去實(shí)現(xiàn)啸臀。ARouter新版本支持Fragment路由,因此我們可以直接使用ARouter做處理:
Fragment類添加@Route注解
@Route(path = "/login/loginFragment")
public class LoginFragment extends Fragment {}
通過路由獲取Fragment實(shí)例
Fragment fragment = (Fragment) ARouter.getInstance().build("/login/loginFragment").navigation();
獲取到實(shí)例后續(xù)操作和以前一樣。
這樣就完成了Fragment與主工程的解耦乘粒。除了借助ARouter組件外我們還可以通過接口的方式來解耦豌注。
原先接口中新增方法
public interface IAccountService {
...
void newFragment(Context context, int containId, FragmentManager manager, Bundle bundle,String tag);
}
實(shí)現(xiàn)類中處理附載Fragment的邏輯
public class LoginService implements IAccountService {
...
@Override
public void newFragment(Context context, int containId, FragmentManager manager, Bundle bundle, String tag) {
FragmentTransaction fragmentTransaction = manager.beginTransaction();
LoginFragment fragment = new LoginFragment();
fragment.setArguments(bundle);
fragmentTransaction.add(containId,fragment,tag);
fragmentTransaction.commitNow();
}
}
主工程調(diào)用:
ServiceFactory.getInstance().getAccountService().newFragment(this,R.id.content,getSupportFragmentManager(),null,"");
這樣也完成了解耦并且保證了主工程編譯不會失敗。
七灯萍、主工程集成調(diào)試
到現(xiàn)在工程解耦基本上完成了轧铁,集成調(diào)試的問題在上面幾個問題中已經(jīng)解決了。通過componsentBase組件的各個Service接口解決了直接引用類的問題旦棉,并且保證主工程能夠訪問依賴的工程齿风。通過ARouter組件進(jìn)行組件之間的跳轉(zhuǎn)與獲取Fragment。這樣組件之間沒有強(qiáng)依賴绑洛,所以即便主工程不依賴組件救斑,主工程也不會編譯失敗。
七真屯、代碼隔離與資源隔離
我們面向接口編程方式來完成工程解耦的脸候,但是主工程還是能訪問到組件的代碼。那么就有可能有意無意的直接引用到組件的類绑蔫,這樣一來上面的解耦就白做了运沦。所以我們想要主工程只有在打包階段才能訪問組件的代碼,在開發(fā)階段不能訪問配深,這樣就杜絕了直接引用的存在携添。
這個問題我們通過gradle去實(shí)現(xiàn),gradle3.0提供了新的依賴方式runtimeOnly篓叶,同過runtimeOnly依賴的組件只有在運(yùn)行時對工程和消費(fèi)者可用薪寓,開發(fā)階段完全隔離。
所以我們需要在主工程依賴時采用runtimeOnly方式:
dependencies {
...
runtimeOnly project(path: ':payModule')
runtimeOnly project(path: ':loginModule')
runtimeOnly project(path: ':livemodule')
}
這樣主工程就不會引用到組件的代碼了澜共。
在gradle版本中這樣雖然避免的了代碼引用向叉,但是資源確還是能夠引用的。新的版本上主工程引用組件的資源會報紅嗦董,因此新版本基本上已經(jīng)達(dá)到資源隔離了母谎。那么我們看下老版本gradle的做法:
android {
...
resourcePrefix "login_"
...
}
通過在組件的gradle中配置resourcePrefix字段來添加資源前綴。當(dāng)資源不以前綴開頭的話則資源會報紅京革,而以前綴開頭則正常奇唤。
但是resourcePrefix只能限制xml中的資源并不能限制圖片資源,因此我們針對圖片資源需要手動設(shè)置前綴,同時將共用的資源放入到Base庫中匹摇,這樣最大化的實(shí)現(xiàn)資源的隔離咬扇。
到這里組件化基本上結(jié)束了。最終結(jié)構(gòu)變成了如下所示:
本文是針對組件化學(xué)習(xí)的記錄廊勃,主要參考以下文章懈贺,感謝作者:
組件化最佳實(shí)踐