在上一篇文章中講解了為什麼要元件化、元件化的概念、建立元件化框架;這篇文章則來詳細講一些關於Android元件化開發的案例,其中融合數10個專案模組......
目錄介紹
1.實際開發案例
-
1.1 元件化實踐的開源專案
-
1.1 如何建立模組
-
1.2 如何建立依賴
-
1.3 如何統一配置檔案
-
1.4 元件化的基礎庫
-
1.5 元件模式和整合模式如何切換
-
1.6 元件化解決重複依賴
-
1.7 元件化注意要點
-
1.8 元件化時資源名衝突
-
1.9 元件化開發遇到問題
2.元件間通訊
-
2.1 選擇那個開源路由庫
-
2.2 阿里Arouter基礎原理
-
2.3 使用Arouter注意事項
0.元件化開發案例開源地址
1.實際開發案例
1.1 元件化實踐的開源專案
- 關於元件化開發一點感想
- 關於網上有許多關於元件化的部落格,講解了什麼是元件化,為何要元件化,以及元件化的好處。大多數文章提供了元件化的思路,給我著手元件化開發提供了大量的便利。感謝前輩大神的分享!雖然有一些收穫,但是很少有文章能夠給出一個整體且有效的方案,或者一個具體的Demo。
- 但是畢竟看部落格也是為了實踐做準備,當著手將之前的開源案例改版成元件化案例時,出現了大量的問題,也解決了一些問題。主要是學些了元件化開發流程。
- 大多數公司慢慢著手元件化開發,在小公司,有的人由於之前沒有做過元件化開發,嘗試元件化也是挺好的;在大公司,有的人一去只是負責某個模組,可能剛開始元件化已經有人弄好了,那學習實踐元件化那更快一些。業餘實踐,改版之前開源專案,寫了這篇部落格,耗費我不少時間,要是對你有些幫助,那我就很開心呢。由於我也是個小人物,所以寫的不好,勿噴,歡迎提出建議!
- 關於元件化開源專案
- 專案整體架構模式採用:元件化+MVP+Rx+Retrofit+design+Dagger2+VLayout+X5
- 包含的模組:wanAndroid【kotlin】+乾貨集中營+知乎日報+番茄Todo+精選新聞+豆瓣音樂電影小說+小說讀書+簡易記事本+搞笑視訊+經典遊戲+其他更多等等
- 此專案屬於業餘時間練手的專案,介面資料來源均來自網路,如果存在侵權情況,請第一時間告知。本專案僅做學習交流使用,API資料內容所有權歸原作公司所有,請勿用於其他用途。
- 關於開源元件化的專案地址:github.com/yangchong21…
1.1 如何建立模組
- 根據上一篇文章3.3 架構設計圖可以知道
- 主工程:
- 除了一些全域性配置和主 Activity 之外,不包含任何業務程式碼。有的也叫做空殼app,主要是依賴業務元件進行執行。
- 業務元件:
- 最上層的業務,每個元件表示一條完整的業務線,彼此之間互相獨立。原則上來說:各個業務元件之間不能有直接依賴!所有的業務元件均需要可以做到獨立執行的效果。對於測試的時候,需要依賴多個業務元件的功能進行整合測試的時候。可以使用app殼進行多元件依賴管理執行。
- 該案例中分為:幹活集中營,玩Android,知乎日報,微信新聞,頭條新聞,搞笑視訊,百度音樂,我的記事本,豆瓣音樂讀書電影,遊戲元件等等。
- 功能元件:
- 該案例中分為,分享元件,評論反饋元件,支付元件,畫廊元件等等。同時注意,可能會涉及多個業務元件對某個功能元件進行依賴!
- 基礎元件:
- 支撐上層業務元件執行的基礎業務服務。此部分元件為上層業務元件提供基本的功能支援。
- 該案例中:在基礎元件庫中主要有,網路請求,圖片載入,通訊機制,工具類,分享功能,支付功能等等。當然,我把一些公共第三方庫放到了這個基礎元件中!
1.2 如何建立依賴
- 關於工程中元件依賴結構圖如下所示
- 業務模組下完整配置程式碼
//控制元件模式和整合模式
if (rootProject.ext.isGankApplication) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
if (rootProject.ext.isGankApplication){
//元件模式下設定applicationId
applicationId "com.ycbjie.gank"
}
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//jdk1.8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
if (rootProject.ext.isGankApplication) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':library')
annotationProcessor rootProject.ext.dependencies["router-compiler"]
}
複製程式碼
1.3 如何統一配置檔案
-
由於元件化實踐中模組比較多,因此配置gradle,新增依賴庫時,需要考慮簡化工作。那麼究竟如何做呢?
-
第一步,首先在專案根目錄下建立一個yc.gradle檔案。實際開發中只需要更改該檔案中版本資訊即可。
- 我在網上看到的絕大多數案例,都是通過一個開關控制元件元件模式和整合模式的切換,但是這裡我配置了多個元件的開關,分別控制對應的元件切換狀態。
ext {
isApplication = false //false:作為Lib元件存在, true:作為application存在
isAndroidApplication = false //玩Android模組開關,false:作為Lib元件存在, true:作為application存在
isLoveApplication = false //愛意表達模組開關,false:作為Lib元件存在, true:作為application存在
isVideoApplication = false //視訊模組開關,false:作為Lib元件存在, true:作為application存在
isNoteApplication = false //記事本模組開關,false:作為Lib元件存在, true:作為application存在
isBookApplication = false //book模組開關,false:作為Lib元件存在, true:作為application存在
isDouBanApplication = false //豆瓣模組開關,false:作為Lib元件存在, true:作為application存在
isGankApplication = false //乾貨模組開關,false:作為Lib元件存在, true:作為application存在
isMusicApplication = false //音樂模組開關,false:作為Lib元件存在, true:作為application存在
isNewsApplication = false //新聞模組開關,false:作為Lib元件存在, true:作為application存在
isToDoApplication = false //todo模組開關,false:作為Lib元件存在, true:作為application存在
isZhiHuApplication = false //知乎模組開關,false:作為Lib元件存在, true:作為application存在
isOtherApplication = false //其他模組開關,false:作為Lib元件存在, true:作為application存在
android = [
compileSdkVersion : 28,
buildToolsVersion : "28.0.3",
minSdkVersion : 17,
targetSdkVersion : 28,
versionCode : 22,
versionName : "1.8.2" //必須是int或者float,否則影響線上升級
]
version = [
androidSupportSdkVersion: "28.0.0",
retrofitSdkVersion : "2.4.0",
glideSdkVersion : "4.8.0",
canarySdkVersion : "1.5.4",
constraintVersion : "1.0.2"
]
dependencies = [
//support
"appcompat-v7" : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
"multidex" : "com.android.support:multidex:1.0.1",
//network
"retrofit" : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",
"retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",
"retrofit-adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",
//這裡省略一部分程式碼
]
}
複製程式碼
- 第二步,然後在專案中的lib【注意這裡是放到基礎元件庫的build.gradle】中新增程式碼,如下所示
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api rootProject.ext.dependencies["appcompat-v7"]
api rootProject.ext.dependencies["design"]
api rootProject.ext.dependencies["palette"]
api rootProject.ext.dependencies["glide"]
api (rootProject.ext.dependencies["glide-transformations"]){
exclude module: 'glide'
}
annotationProcessor rootProject.ext.dependencies["glide-compiler"]
api files('libs/tbs_sdk_thirdapp_v3.2.0.jar')
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//省略部分程式碼
}
複製程式碼
-
第三步,在其他model中新增依賴
- implementation project(':library')即可。
1.4 元件化的基礎庫
-
基礎庫元件封裝
- 基礎庫元件封裝庫中主要包括開發常用的一些框架。可以直接看我的專案更加直觀!
- 1、網路請求(採用Retrofit+RxJava框架),攔截器
- 2、圖片載入(策略模式,Glide與Picasso之間可以切換)
- 3、通訊機制(RxBus),路由ARouter簡單封裝工具類(不同model之間通訊)
- 4、mvp框架,常用的base類,比如BaseActivity,BaseFragment等等
- 5、通用的工具類,比如切割圓角,動畫工具類等等
- 6、自定義view(包括對話方塊,ToolBar佈局,圓形圖片等view的自定義)
- 7、共有的shape,drawable,layout,color等資原始檔
- 8、全域性初始化非同步執行緒池封裝庫,各個元件均可以用到
-
元件初始化
- 比如,你將該案例中的新聞元件切換成獨立執行的app,那麼由於新聞跳轉詳情頁需要使用到x5的WebView,因此需要對它進行初始化。最剛開始做法是,為每一個可以切換成app的元件配置一個獨立的application,然後初始化一些該元件需要初始化的任務。但是這麼做,有一點不好,不是很方便管理。後來看了知乎元件化實踐方案後,該方案提出,開發了一套多執行緒初始化框架,每個元件只要新建若干個啟動 Task 類,並在 Task 中宣告依賴關係。但是具體怎麼用到程式碼中後期有待實現!
-
如何簡化不熟悉元件化的人快速適應元件獨立執行 設定多個元件開關,需要切換那個元件就改那個。如果設定一個開關,要麼把所有元件切成整合模式,要麼把所有元件切成元件模式,有點容易出問題。更多可以往下看!
-
嚴格限制公共基礎元件的增長 隨著開發不斷進行,要注意不要往基礎公共元件加入太多內容。而是應該減小體積!倘若是基礎元件過於龐大,那麼執行元件也是比較緩慢的!
1.5 元件模式和整合模式如何切換
-
在玩Android元件下的build.gradle檔案,其他元件類似。
- 通過一個開關來控制這個狀態的切換,module如果是一個庫,會使用com.android.library外掛;如果是一個應用,則使用com.android.application外掛
//控制元件模式和整合模式
if (rootProject.ext.isAndroidApplication) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
複製程式碼
-
整合模式如下所示
- 首先需要在yc.gradle檔案中設定 isApplication=false。Sync下後,發現該元件是library
ext {
isAndroidApplication = false //false:作為Lib元件存在, true:作為application存在
複製程式碼
-
元件模式如下所示
- 首先需要在yc.gradle檔案中設定 isApplication=true。Sync下後,發現該元件是application,即可針對模組進行執行
ext {
isAndroidApplication = true //false:作為Lib元件存在, true:作為application存在
複製程式碼
-
需要注意的地方,這個很重要
- 首先看看網上絕大多數的作法,非常感謝這些大神的無私奉獻!但是我覺得多個元件用一個開關控制也可以,但是sourceSets裡面切換成元件app時,可以直接不用下面這麼麻煩,可以複用java和res檔案。
-
接下來看看我的做法:
-
下面這個配置十分重要。也就是說當該玩Android元件從library切換到application時,由於可以作為獨立app執行,所以序意設定applicationId,並且配置清單檔案,如下所示!
-
在 library 和 application 之間切換,manifest檔案也需要提供兩套
android {
defaultConfig {
if (rootProject.ext.isAndroidApplication){
//元件模式下設定applicationId
applicationId "com.ycbjie.android"
}
}
sourceSets {
main {
if (rootProject.ext.isAndroidApplication) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
jniLibs.srcDirs = ['libs']
}
}
}
複製程式碼
- 具體在專案中如下所示
-
重複依賴問題說明
-
重複依賴問題其實在開發中經常會遇到,比如專案 implementation 了一個A,然後在這個庫裡面又 implementation 了一個B,然後你的工程中又 implementation 了一個同樣的B,就依賴了兩次。
-
預設情況下,如果是 aar 依賴,gradle 會自動幫我們找出新版本的庫而拋棄舊版本的重複依賴。但是如果使用的是project依賴,gradle並不會去去重,最後打包就會出現程式碼中有重複的類了。
-
-
解決辦法,舉個例子
api(rootProject.ext.dependencies["logger"]) {
exclude module: 'support-v4'//根據元件名排除
exclude group: 'android.support.v4'//根據包名排除
}
複製程式碼
1.7 元件化注意要點
-
業務元件之間聯動導致耦合嚴重
-
比如,實際開發中,購物車和首頁商品分別是兩個元件。但是遇到產品需求,比如過節做個活動,發個購物券之類的需求,由於購物車和商品詳情頁都有活動,因此會造成元件經常會發生聯動。倘若前期準備不足,隨著時間的推移,各個業務線的程式碼邊界會像元件化之前的主工程一樣逐漸劣化,耦合會越來越嚴重。
-
第一種解決方式:使用 sourceSets 的方式將不同的業務程式碼放到不同的資料夾,但是 sourceSets 的問題在於,它並不能限制各個 sourceSet 之間互相引用,所以這種方式並不太友好!
-
第二種解決方式:抽取需求為工具類,通過不同元件傳值而達到呼叫關係,這樣只需要改工具類即可改需求。但是這種只是符合需求一樣,但是用在不同模組的場景。
-
-
元件化開發之資料庫分離
- 比如,我現在開發的視訊模組想要給別人用,由於快取之類需要用到資料庫,難道還要把這個lib還得依賴一個體積較大的第三方資料庫?但是使用系統原生sql資料庫又不太方便,怎麼辦?暫時我也沒找到辦法……
####1.8 元件化時資源名衝突
-
資源名衝突有哪些?
-
比如,color,shape,drawable,圖片資源,佈局資源,或者anim資源等等,都有可能造成資源名稱衝突。這是為何了,有時候大家負責不同的模組,如果不是按照統一規範命名,則會偶發出現該問題。
-
尤其是如果string, color,dimens這些資源分佈在了程式碼的各個角落,一個個去拆,非常繁瑣。其實大可不必這麼做。因為android在build時,會進行資源的merge和shrink。res/values下的各個檔案(styles.xml需注意)最後都只會把用到的放到intermediate/res/merged/../valus.xml,無用的都會自動刪除。並且最後我們可以使用lint來自動刪除。所以這個地方不要耗費太多的時間。
-
-
解決辦法
- 這個問題也不是新問題了,第三方SDK基本都會遇到,可以通過設定 resourcePrefix 來避免。設定了這個值後,你所有的資源名必須以指定的字串做字首,否則會報錯。但是 resourcePrefix 這個值只能限定 xml 裡面的資源,並不能限定圖片資源,所有圖片資源仍然需要你手動去修改資源名。
-
個人建議
- 將color,shape等放到基礎庫元件中,因為所有的業務元件都會依賴基礎元件庫。在styles.xml需注意,寫屬性名字的時候,一定要加上字首限定詞。假如說不加的話,有可能會在打包成aar後給其他模組使用的時候,會出現屬性名名字重複的衝突,為什麼呢?因為BezelImageView這個名字根本不會出現在intermediate/res/merged/../valus.xml裡, 所以不要以為這是屬性的限定詞!
1.9 元件化開發遇到問題
-
如何做到各個元件化模組能獲取到全域性上下文
-
情景再現
- 比如,剛開始線上專案是在app主工程裡建立的單利,那麼在lib中或者後期劃分的元件化,是無法拿到主工程的application類中的上下文。這個時候可以
-
解決辦法
- 很容易,在lib裡寫一個Utils工具類,然後在主工程application中初始化Utils.init(this),這樣就可以在lib和所有業務元件[已經依賴公共基礎元件庫]中拿到全域性上下文呢!
-
-
butterKnife使用問題
- 儘管網上有不少部落格說可以解決butterKnife在不同元件之間的引用。但是我在實際開發時,遇到元件模式和整合模式切換狀態時,導致出現編譯錯誤問題。要是那位在元件化中解決butterKnife引用問題,可以告訴我,非常感謝!
-
當元件化是lib時
- 不能使用switch(R.id.xx),需要使用if..else來代替。
-
不要亂髮bus訊息
-
如果專案中大量的使用eventbus,那麼會看到一個類中有大量的onEventMainThread()方法,寫起來很爽,閱讀起來很痛苦。
-
雖然說,前期使用EventBus或者RxBus傳送訊息來實現元件間通訊十分方便和簡單,但是隨著業務增大,和後期不斷更新,有的還經過多個程式設計師前前後後修改,會使程式碼閱讀量降低。專案中傳送這個Event的地方非常多,接收這個Event的地方也很多。在後期想要改進為元件化開發,而進行程式碼拆分時,都不敢輕舉妄動,生怕哪些事件沒有被接收。
-
-
頁面跳轉存在問題
-
如果一個頁面需要登陸狀態才可以檢視,那麼會寫if(isLogin()){//跳轉頁面}else{//跳轉到登入頁面},每次操作都要寫這些個相同的邏輯。
-
原生startActivity跳轉,無法監聽到跳轉的狀態,比如跳轉錯誤,成功,異常等問題。
-
後時候,後臺會控制從點選按鈕【不同場景下】跳轉到不同的頁面,假如後臺配置資訊錯誤,或者少了引數,那麼跳轉可能不成功或者導致崩潰,這個也沒有一個好的處理機制。
-
阿里推出的開源框架Arouter,便可以解決頁面跳轉問題,可以新增攔截,或者即使後臺配置引數錯誤,當監聽到跳轉異常或者跳轉錯誤時的狀態,可以直接預設跳轉到首頁。我在該開源案例就是這麼做的!
-
-
關於跳轉引數問題
- 先來看一下這種程式碼寫法,這種寫法本沒有問題,只是在多人開發時,如果別人想要跳轉到你開發模組的某個頁面,那麼就容易傳錯值。建議將key這個值,寫成靜態常量,放到一個專門的類中。方便自己,也方便他人。
//跳轉
intent.setClass(this,CommentActivity.class);
intent.putExtra("id",id);
intent.putExtra("allNum",allNum);
intent.putExtra("shortNum",shortNum);
intent.putExtra("longNum",longNum);
startActivity(intent);
//接收
Intent intent = getIntent();
int allNum = intent.getExtras().getInt("allNum");
int shortNum = intent.getExtras().getInt("shortNum");
int longNum = intent.getExtras().getInt("longNum");
int id = intent.getExtras().getInt("id");
複製程式碼
2.元件間通訊
2.1 選擇那個開源路由庫
*比較有代表性的元件化開源框架有得到得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。
-
得到DDComponentForAndroid:一套完整有效的android元件化方案,支援元件的元件完全隔離、單獨除錯、整合除錯、元件互動、UI跳轉、動態載入解除安裝等功能。
-
阿里Arouter:對頁面、服務提供路由功能的中介軟體,簡單且夠用好用,網上的使用介紹部落格也很多,在該元件化案例中,我就是使用這個。
-
Router:一款單品、元件化、外掛化全支援的路由框架
2.2 阿里Arouter基礎原理
-
這裡只是說一下基礎的思路
-
在程式碼里加入的@Route註解,會在編譯時期通過apt生成一些儲存path和activityClass對映關係的類檔案,然後app程式啟動的時候會拿到這些類檔案,把儲存這些對映關係的資料讀到記憶體裡(儲存在map裡),然後在進行路由跳轉的時候,通過build()方法傳入要到達頁面的路由地址。
- 新增@Route註解然後編譯一下,就可以生成這個類,然後看一下這個類。如下所示:
-
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$video implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class, "/video/videoactivity", "video", null, -1, -2147483648));
}
}
複製程式碼
-
ARouter會通過它自己儲存的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),然後new Intent(),當呼叫ARouter的withString()方法它的內部會呼叫intent.putExtra(String name, String value),呼叫navigation()方法,它的內部會呼叫startActivity(intent)進行跳轉,這樣便可以實現兩個相互沒有依賴的module順利的啟動對方的Activity了。
- 看_ARouter類中的 _navigation方法程式碼,在345行。
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// Set Actions
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
case PROVIDER:
//這裡省略程式碼
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
//這裡省略程式碼
case METHOD:
case SERVICE:
default:
return null;
}
return null;
}
複製程式碼
2.3 使用Arouter注意事項
-
使用阿里路由抽取工具類,方便後期維護!
- 首先看一下網路上有一種寫法。
//首先通過註解新增下面程式碼
@Route(path = "/test/TestActivity")
public class TestActivity extends BaseActivity {
}
//跳轉
ARouter.getInstance().inject("/test/TestActivity");
複製程式碼
-
優化後的寫法
- 下面這種做法,是方便後期維護。
//存放所有的路由路徑常量
public class ARouterConstant {
//跳轉到視訊頁面
public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity";
//省略部分diamagnetic
}
//存放所有的路由跳轉,工具類
public class ARouterUtils {
/**
* 簡單的跳轉頁面
* @param string string目標介面對應的路徑
*/
public static void navigation(String string){
if (string==null){
return;
}
ARouter.getInstance().build(string).navigation();
}
}
//呼叫
@Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)
public class VideoActivity extends BaseActivity {
}
ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);
複製程式碼
本人Java開發4年Android開發5年,定期分享Android高階技術及經驗分享,歡迎大家關注~(喜歡文章的點個贊鼓勵下叭)