在App開發(fā)的初期鳖悠,代碼量不大假瞬,業(yè)務(wù)量比較少环肘,一個(gè)App作為一個(gè)單獨(dú)的模塊進(jìn)行開發(fā)燎含,往往問題不大锚赤。但隨著業(yè)務(wù)的增多馒胆,代碼變的越來越復(fù)雜狭郑,每個(gè)模塊之間的代碼耦合變得越來越嚴(yán)重亚茬,結(jié)構(gòu)越來越臃腫偏陪,修改一處代碼要編譯整個(gè)工程抢呆,導(dǎo)致非常耗時(shí),這時(shí)候解耦問題急需解決笛谦。
同時(shí)抱虐,如果公司有多個(gè)終端設(shè)備的App,而且有塊功能是通用的(比如說下單功能)饥脑,那么通用的這一塊功能被復(fù)制集成到不同App里恳邀,就顯得很重復(fù),而且維護(hù)時(shí)要修改多套代碼灶轰,嚴(yán)重影響開發(fā)效率谣沸,因此模塊化開發(fā)就很有必要。
App模塊化的目標(biāo)是告別結(jié)構(gòu)臃腫笋颤,讓各個(gè)業(yè)務(wù)變得相對(duì)獨(dú)立乳附,業(yè)務(wù)模塊在組件模式下可以獨(dú)立開發(fā),而在集成模式下又可以變?yōu)橐蕾嚢傻健癮pp殼工程”中伴澄,組成一個(gè)完整功能的APP赋除。
一、模塊化開發(fā)的好處
- 公用功能非凌,不用重復(fù)開發(fā)举农、修改,代碼復(fù)用性更強(qiáng)
- 獨(dú)立運(yùn)行敞嗡,提高編譯速度颁糟,也就提高了開發(fā)效率
- 更利于團(tuán)隊(duì)開發(fā)祭犯,不同的人可以獨(dú)立負(fù)責(zé)不同的模塊
- 獨(dú)立模塊可以采用不同的技術(shù)架構(gòu),嘗試新的技術(shù)方案滚停,比如采用新的網(wǎng)絡(luò)框架沃粗,甚至換成Kotlin來開發(fā)App
二、模塊化要解決的問題
- 模塊間頁面跳轉(zhuǎn)(路由)键畴;
- 模塊間事件通信最盅;
- 模塊間服務(wù)調(diào)用;
- 模塊的獨(dú)立運(yùn)行起惕;
- 模塊間頁面跳轉(zhuǎn)路由攔截(登錄)
三涡贱、ARouter路由框架
以上模塊化需要要解決的問題,2017年阿里開源的路由框架ARouter都有提供解決方案惹想。
官方對(duì)這個(gè)框架的定義是:一個(gè)用于幫助 Android App 進(jìn)行組件化改造的框架 —— 支持模塊間的路由问词、通信、解耦嘀粱。
ARouter提供的功能有:
- 支持直接解析標(biāo)準(zhǔn)URL進(jìn)行跳轉(zhuǎn)激挪,并自動(dòng)注入?yún)?shù)到目標(biāo)頁面中
- 支持多模塊工程使用
- 支持添加多個(gè)攔截器,自定義攔截順序
- 支持依賴注入锋叨,可單獨(dú)作為依賴注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射關(guān)系按組分類垄分、多級(jí)管理,按需初始化
- 支持用戶指定全局降級(jí)與局部降級(jí)策略
- 頁面娃磺、攔截器薄湿、服務(wù)等組件均自動(dòng)注冊(cè)到框架
- 支持多種方式配置轉(zhuǎn)場(chǎng)動(dòng)畫
- 支持獲取Fragment
- 完全支持Kotlin以及混編(配置見文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 實(shí)現(xiàn)自動(dòng)注冊(cè))
- 支持生成路由文檔
- 提供 IDE 插件便捷的關(guān)聯(lián)路徑和目標(biāo)類
附上ARouter官網(wǎng)地址:ARouter
其中,關(guān)于路由方面偷卧,Google提供的原生路由主要是通過Intent豺瘤,Intent可以分成顯示和隱式兩種。顯示的方案會(huì)導(dǎo)致類之間的直接依賴問題听诸,耦合嚴(yán)重;隱式Intent需要在配置清單中統(tǒng)一聲明坐求,首先有個(gè)暴露的問題,另外在多模塊開發(fā)中協(xié)作也比較困難蛇更。除此之外瞻赶,使用原生的路由方案會(huì)出現(xiàn)跳轉(zhuǎn)過程無法控制的問題,因?yàn)橐坏┦褂昧藄tartActivity()就無法插手其中任何環(huán)節(jié)了派任,只能交給系統(tǒng)管理砸逊,這就導(dǎo)致了在跳轉(zhuǎn)失敗的情況下無法降級(jí),而是會(huì)直接拋出運(yùn)營(yíng)級(jí)的異常掌逛。
// Intent顯式啟動(dòng)Activity
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
// Intent隱式啟動(dòng)Activity
Intent intent = new Intent("com.example.activity.ACTION_START");
startActivity(intent);
如果使用ARouter师逸,可以在跳轉(zhuǎn)過程中進(jìn)行攔截,出現(xiàn)錯(cuò)誤時(shí)可以實(shí)現(xiàn)降級(jí)策略. 比如跳轉(zhuǎn)頁面不存在不是直接crash而是可以跳轉(zhuǎn)到一個(gè)指定的默認(rèn)頁面豆混。
四篓像、用ARouter進(jìn)行模塊化開發(fā)
接下來动知,將會(huì)用一個(gè)demo介紹如何用ARouter進(jìn)行模塊化開發(fā),demo模塊化的整體架構(gòu)如下:
- app:項(xiàng)目的宿主模塊员辩,僅僅是一個(gè)空殼盒粮,依賴于其他模塊,成為項(xiàng)目架構(gòu)的入口
- baselibrary:項(xiàng)目的基類庫奠滑,每個(gè)子模塊都依賴共享公用的類和資源丹皱,防止公用的功能在不同的模塊中有多個(gè)實(shí)現(xiàn)方式
- module_route:集中管理所有模塊的route
- module_main:閃屏頁,登錄頁宋税,主頁等
- module_home:首頁模塊
- module_mine:我的模塊
-
module_video:視頻模塊
五摊崭、依賴模式與獨(dú)立運(yùn)行模式切換
在項(xiàng)目開發(fā)中,各個(gè)模塊可以同時(shí)開發(fā)杰赛,獨(dú)立運(yùn)行而不必依賴于宿主app呢簸,也就是每個(gè)module是一個(gè)獨(dú)立的App,項(xiàng)目發(fā)布的時(shí)候依賴到宿主app中乏屯。各業(yè)務(wù)模塊之間不允許存在相互依賴關(guān)系根时,但是需要依賴基類庫。單一模塊生成的apk體積也小瓶珊,編譯時(shí)間也快啸箫,開發(fā)效率會(huì)高很多,同時(shí)也可以獨(dú)立測(cè)試伞芹。要實(shí)現(xiàn)這樣的效果需要對(duì)項(xiàng)目做一些配置。
1蝉娜、gradle.properties配置
在項(xiàng)目gradle.properties中需要設(shè)置一個(gè)開關(guān)唱较,用來控制module的編譯,如下:
isModule=false
當(dāng)isModule為false作為依賴庫召川,只能以宿主app啟動(dòng)項(xiàng)目南缓,選擇運(yùn)行模塊時(shí)其他module前都是紅色的X,表示無法運(yùn)行
當(dāng)isModule為true的時(shí)候作為單獨(dú)的模塊進(jìn)行運(yùn)行荧呐,選擇其中一個(gè)module可以直接運(yùn)行
2汉形、清單文件配置
module清單文件需要配置兩個(gè),一個(gè)作為獨(dú)立項(xiàng)目的清單文件倍阐,一個(gè)作為庫的清單文件概疆,以module_main模塊為例:
buildApp作為依賴庫的清單文件,和獨(dú)立項(xiàng)目的清單文件buildModule區(qū)別是依賴庫的清單文件Application中沒有配置入口的Activity峰搪,其他都一樣
3岔冀、gradle配置
4、宿主app配置
六概耻、ARouter功能詳解
1使套、添加依賴和配置
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
}
dependencies {
// 替換成最新版本, 需要注意的是api
// 要與compiler匹配使用罐呼,均使用最新版可以保證兼容
compile 'com.alibaba:arouter-api:x.x.x'
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
...
}
注意,arouter-api:1.3.1侦高、arouter-compiler:1.1.4配置是
arguments = [moduleName: project.getName()]
arouter-api:1.4.1嫉柴、arouter-compiler:1.2.2配置是
arguments = [AROUTER_MODULE_NAME: project.getName()]
2、添加注解
// 在支持路由的頁面上添加注解(必選)
// 這里的路徑需要注意的是至少需要有兩級(jí)奉呛,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
在我的demo中计螺,各模塊的route地址都統(tǒng)一放在module_route中集中管理,其他module都需要依賴module_route侧馅,同時(shí)不同模塊的route有單獨(dú)的RoutePath類危尿。如圖:
@Route(path = MainRoutePath.LOGIN_ACTIVITY)
public class LoginActivity extends BaseActivity {
...
}
public class MainRoutePath {
private static final String PREFIX = "/main/";
public static final String MAIN_ACTIVITY = PREFIX+"MainActivity";
public static final String LOGIN_ACTIVITY = PREFIX+"LoginActivity";
}
- 問題1:為什么要把route地址寫在一個(gè)常量類里?
//聲明的地方
@Route(path = "/test/activity")
//使用的地方
ARouter.getInstance().build("/test/activity").navigation();
聲明的地方和使用的地方可以是處于不同的module馁痴,這種寫法各module不需要相互依賴谊娇,貌似很好,耦合度很低罗晕。但這只是表面上看代碼沒有了耦合度济欢,但他們的耦合關(guān)系還在,試想一下小渊,聲明的地方哪天把route地址改了法褥,使用的地方完全“無感”,只有等到真正運(yùn)行時(shí)才能發(fā)現(xiàn)出錯(cuò)了酬屉,這種寫法風(fēng)險(xiǎn)很大半等,而且不容易提前發(fā)現(xiàn)。如果聲明的地方和使用的地方route都用一個(gè)常量來表示呐萨,就能很好的避免這種風(fēng)險(xiǎn)杀饵。
- 問題2:為什么不同模塊的route有單獨(dú)的RoutePath類
因?yàn)橐粋€(gè)App一般頁面都比較多,如果所有route都用一個(gè)RoutePath類來裝谬擦,那這個(gè)類將會(huì)很大切距,且不同模塊開發(fā)人員都需要去改這個(gè)類,容易產(chǎn)生混亂惨远。如果不同模塊的route有單獨(dú)的RoutePath類谜悟,不同模塊的開發(fā)人員只去改對(duì)應(yīng)的類,代碼會(huì)更好管理北秽。 - 問題3:為什么要單獨(dú)建一個(gè)module_route葡幸,僅僅只配置route,為什么不把RoutePath類放在baselibrary里羡儿?
baselibrary一般是一些通用的基礎(chǔ)功能或通用配置礼患,正常情況下應(yīng)只能讓少數(shù)的有架構(gòu)層次的開發(fā)人員去改動(dòng),所以應(yīng)該做權(quán)限保護(hù),如果把RoutePath放在baselibrary里缅叠,相當(dāng)于baselibrary對(duì)所有開發(fā)人員都是開發(fā)的悄泥。 - 問題4:配置route地址時(shí),有什么講究肤粱?
如上MainRoutePath中弹囚,我的route地址配置規(guī)則采用的是: 前綴 + Activity類名,前綴一般用module名字领曼。根據(jù)官方文檔說明
1鸥鹉、SDK中針對(duì)所有的路徑(/test/1 /test/2)進(jìn)行分組,分組只有在分組中的某一個(gè)路徑第一次被訪問的時(shí)候庶骄,該分組才會(huì)被初始化毁渗;
2、可以通過 @Route 注解主動(dòng)指定分組单刁,否則使用路徑中第一段字符串(/*/)作為分組灸异。
意思是用分組可以按需加載,提高性能羔飞,當(dāng)沒有主動(dòng)分組時(shí)肺樟,ARouter用第一段字符串作為分組。所以我的前綴就是分組名逻淌,不用再去主動(dòng)指定分組么伯。
3、初始化SDK
if (isDebug()) { // 這兩行必須寫在init之前卡儒,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 開啟調(diào)試模式(如果在InstantRun模式下運(yùn)行田柔,必須開啟調(diào)試模式!線上版本需要關(guān)閉,否則有安全風(fēng)險(xiǎn))
}
ARouter.init(mApplication); // 盡可能早骨望,推薦在Application中初始化
4凯楔、發(fā)起路由操作
// 1. 簡(jiǎn)單的跳轉(zhuǎn)
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation();
// 2. 跳轉(zhuǎn)并攜帶參數(shù)
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY)
.withString("name", name)
.withInt("age", 28)
.navigation();
5、目標(biāo)頁面接收參數(shù)
@Route(path = MainRoutePath.MAIN_ACTIVITY)
public class MainActivity extends BaseActivity {
/**
* 接收參數(shù)
*/
@Autowired(name = "name")
public String name;
@Autowired(name = "age")
public int age;
...
}
6锦募、聲明攔截器(攔截跳轉(zhuǎn)過程,面向切面編程)
攔截都是全局性的邻遏,因此一般寫在baselibrary里糠亩,如權(quán)限校驗(yàn)的攔截器。攔截器會(huì)在跳轉(zhuǎn)之間執(zhí)行准验,多個(gè)攔截器會(huì)按優(yōu)先級(jí)順序依次執(zhí)行
但是需要注意的是赎线,每次所有的跳轉(zhuǎn)都會(huì)執(zhí)行攔截器操作,ARouter提供了greenChannel()方法進(jìn)行跳轉(zhuǎn)過去一切攔截器糊饱,在不需要攔截器的地方跳轉(zhuǎn)的時(shí)候加上即可垂寥。
//greenChannel表示跳過攔截器驗(yàn)證
ARouter.getInstance().build(MainRoutePath.LOGIN_ACTIVITY).greenChannel().navigation();
7、降級(jí)策略
ARouter提供的降級(jí)策略主要有兩種方式,一種是通過回調(diào)的方式滞项;一種是提供服務(wù)接口的方式狭归。我們分別來看看兩種方式的使用方法:
- 一、單獨(dú)降級(jí)-回調(diào)的方式
這種方式在跳轉(zhuǎn)失敗的時(shí)候會(huì)回調(diào)NavCallback接口的onLost方法文判。
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation(this, new NavCallback() {
@Override
public void onFound(Postcard postcard) {
Log.d("ARouter", "找到了");
}
@Override
public void onLost(Postcard postcard) {
Log.d("ARouter", "找不到了");
}
@Override
public void onArrival(Postcard postcard) {
Log.d("ARouter", "跳轉(zhuǎn)完了");
}
@Override
public void onInterrupt(Postcard postcard) {
Log.d("ARouter", "被攔截了");
}
});
回調(diào)接口,對(duì)于降級(jí)策略主要實(shí)現(xiàn)感興趣的onLost方法即可过椎。
- 二、全局降級(jí)-服務(wù)接口的方式
這種方式很簡(jiǎn)單戏仓,主要處理邏輯在內(nèi)部疚宇,暴露的接口很友好。
//跳轉(zhuǎn)目標(biāo)頁面不存在赏殃,觸發(fā)降級(jí)策略 避免crash
ARouter.getInstance().build("/test/test").navigation();
這種降級(jí)策略主要是實(shí)現(xiàn)服務(wù)接口DegradeService敷待,就一個(gè)方法就是onLost,和上面的類似仁热。
//要用ARouter跳轉(zhuǎn)才能攔截到榜揖,用Intent隱式或顯示跳轉(zhuǎn)無法攔截,出錯(cuò)還是會(huì)crash
@Route(path = RoutePath.DEGRADE)
public class DegradeServiceImpl implements DegradeService {
@Override
public void onLost(Context context, Postcard postcard) {
ARouter.getInstance().build(RoutePath.DEGRADE_TIP).greenChannel().navigation();
}
@Override
public void init(Context context) {
}
}
全局降級(jí)-服務(wù)接口也應(yīng)該寫在baselibrary里
8股耽、使用 IDE 插件導(dǎo)航到目標(biāo)類
在 Android Studio 插件市場(chǎng)中搜索 ARouter Helper
, 或者直接下載文檔上方 最新版本
中列出的 arouter-idea-plugin
zip 安裝包手動(dòng)安裝根盒,安裝后 插件無任何設(shè)置,可以在跳轉(zhuǎn)代碼的行首找到一個(gè)圖標(biāo) 點(diǎn)擊該圖標(biāo)物蝙,即可跳轉(zhuǎn)到標(biāo)識(shí)了代碼中路徑的目標(biāo)類炎滞,如圖:
9、生成路由文檔
// 更新 build.gradle, 添加參數(shù) AROUTER_GENERATE_DOC = enable
// 生成的文檔路徑 : build/generated/source/apt/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json
arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
七诬乞、Android Butterknife在library組件化模塊中的使用問題
1册赛、問題
當(dāng)項(xiàng)目中有多module時(shí),在使用Butterknife的時(shí)候會(huì)發(fā)現(xiàn)在library模塊中使用會(huì)出問題震嫉。當(dāng)library模塊中的頁面通過butterknife找id的時(shí)候森瘪,就會(huì)報(bào)錯(cuò),提示@BindView的屬性必須是一個(gè)常數(shù)票堵,也就是說library module編譯的時(shí)候扼睬,R文件中所有的數(shù)據(jù)并沒有被加上final,也就是R文件中的數(shù)據(jù)并非常量悴势。
2窗宇、解決步驟
- I 首先在項(xiàng)目的總build.gradle中添加classpath
classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.1'
- II 在library中build.gradle中引入插件
apply plugin: 'com.jakewharton.butterknife'
- III 在library中build.gradle中dependencies添加依賴
compile "com.jakewharton:butterknife:8.5.1"
annotationProcessor "com.jakewharton:butterknife-compiler:8.5.1"
3、butterknife在library activity中的使用和注意事項(xiàng)
1特纤、用R2代替R findviewid
@BindView(R2.id.textView)
TextView textView;
@BindView(R2.id.button1)
Button button1;
@BindView(R2.id.image)
ImageView image;
2军俊、在click方法中同樣使用R2,但是找id的時(shí)候使用R
@OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.textView:
break;
case R.id.button1:
break;
case R.id.image:
break;
}
}
3捧存、特別注意library中switch-case的使用粪躬,在library中是不能使用switch- case 找id的担败,解決方法就是用if-else代替
@OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
public void onViewClicked(View view) {
int i = view.getId();
if (i == R.id.textView) {
} else if (i == R.id.button1) {
} else if (i == R.id.image) {
}
}
八、 Demo地址
- ARouter的其他詳細(xì)功能镰官,可閱讀官方文檔:ARouter
- 最后附上我的Demo地址:ARouter Demo