1. 程式人生 > >Android 元件化方案,從入門到精通。apply plugin: ‘com.android.application’

Android 元件化方案,從入門到精通。apply plugin: ‘com.android.application’

目錄

8、結束語

1、為什麼要專案元件化

隨著APP版本不斷的迭代,新功能的不斷增加,業務也會變的越來越複雜,APP業務模組的數量有可能還會繼續增加,而且每個模組的程式碼也變的越來越多,這樣發展下去單一工程下的APP架構勢必會影響開發效率,增加專案的維護成本,每個工程師都要熟悉如此之多的程式碼,將很難進行多人協作開發,而且Android專案在編譯程式碼的時候電腦會非常卡,又因為單一工程下程式碼耦合嚴重,每修改一處程式碼後都要重新編譯打包測試,導致非常耗時,最重要的是這樣的程式碼想要做單元測試根本無從下手,所以必須要有更靈活的架構代替過去單一的工程架構。

單一工程模型

上圖是目前比較普遍使用的Android APP技術架構,往往是在一個介面中存在大量的業務邏輯,而業務邏輯中充斥著各種網路請求、資料操作等行為,整個專案中也沒有模組的概念,只有簡單的以業務邏輯劃分的資料夾,並且業務之間也是直接相互呼叫、高度耦合在一起的;

單一工程模型下的業務關係

上圖單一工程模型下的業務關係,總的來說就是:你中有我,我中有你,相互依賴,無法分離。 
然而隨著產品的迭代,業務越來越複雜,隨之帶來的是專案結構複雜度的極度增加,此時我們會面臨如下幾個問題:

1、實際業務變化非常快,但是單一工程的業務模組耦合度太高,牽一髮而動全身; 
2、對工程所做的任何修改都必須要編譯整個工程; 
3、功能測試和系統測試每次都要進行; 
4、團隊協同開發存在較多的衝突.不得不花費更多的時間去溝通和協調,並且在開發過程中,任何一位成員沒辦法專注於自己的功能點,影響開發效率; 
5、不能靈活的對業務模組進行配置和組裝;

為了滿足各個業務模組的迭代而彼此不受影響,更好的解決上面這種讓人頭疼的依賴關係,就需要整改App的架構。

2、如何元件化

元件化工程模型

上圖是元件化工程模型,為了方便理解這張架構圖,下面會列舉一些元件化工程中用到的名詞的含義:

名詞 含義
整合模式 所有的業務元件被“app殼工程”依賴,組成一個完整的APP;
元件模式 可以獨立開發業務元件,每一個業務元件就是一個APP;
app殼工程 負責管理各個業務元件,和打包apk,沒有具體的業務功能;
業務元件 根據公司具體業務而獨立形成一個的工程;
功能元件 提供開發APP的某些基礎功能,例如列印日誌、樹狀圖等;
Main元件 屬於業務元件,指定APP啟動頁面、主介面;
Common元件 屬於功能元件,支撐業務元件的基礎,提供多數業務元件需要的功能,例如提供網路請求功能;

** 
Android APP元件化架構的目標是告別結構臃腫,讓各個業務變得相對獨立,業務元件在元件模式下可以獨立開發,而在整合模式下又可以變為arr包整合到“app殼工程”中,組成一個完整功能的APP; 
從元件化工程模型中可以看到,業務元件之間是獨立的,沒有關聯的,這些業務元件在整合模式下是一個個library,被app殼工程所依賴,組成一個具有完整業務功能的APP應用,但是在元件開發模式下,業務元件又變成了一個個application,它們可以獨立開發和除錯,由於在元件開發模式下,業務元件們的程式碼量相比於完整的專案差了很遠,因此在執行時可以顯著減少編譯時間。

元件化工程下的業務關係

這是元件化工程模型下的業務關係,業務之間將不再直接引用和依賴,而是通過“路由”這樣一箇中轉站間接產生聯絡,而Android中的路由實際就是對URL Scheme的封裝; 
如此規模大的架構整改需要付出更高的成本,還會涉及一些潛在的風險,但是整改後的架構能夠帶來很多好處:

1、加快業務迭代速度,各個業務模組元件更加獨立,不再出現業務耦合情況; 
2、穩定的公共模組採用依賴庫方式,提供給各個業務線使用,減少重複開發和維護工作量; 
3、迭代頻繁的業務模組採用元件方式,各業務研發可以互不干擾、提升協作效率,並控制產品質量; 
4、為新業務隨時整合提供了基礎,所有業務可上可下,靈活多變; 
5、降低團隊成員熟悉專案的成本,降低專案的維護難度; 
6、加快編譯速度,提高開發效率; 
7、控制程式碼許可權,將程式碼的許可權細分到更小的粒度;

3、元件化實施流程

1)元件模式和整合模式的轉換

Android Studio中的Module主要有兩種屬性,分別為:

1、application屬性,可以獨立執行的Android程式,也就是我們的APP;

 apply plugin: ‘com.android.application’

2、library屬性,不可以獨立執行,一般是Android程式依賴的庫檔案;

 apply plugin: ‘com.android.library’

Module的屬性是在每個元件的 build.gradle 檔案中配置的,當我們在元件模式開發時,業務元件應處於application屬性,這時的業務元件就是一個 Android App,可以獨立開發和除錯;而當我們轉換到整合模式開發時,業務元件應該處於 library 屬性,這樣才能被我們的“app殼工程”所依賴,組成一個具有完整功能的APP;

但是我們如何讓元件在這兩種模式之間自動轉換呢?總不能每次需要轉換模式的時候去每個業務元件的 Gralde 檔案中去手動把 Application 改成 library 吧?如果我們的專案只有兩三個元件那麼這個辦法肯定是可行的,手動去改一遍也用不了多久,但是在大型專案中我們可能會有十幾個業務元件,再去手動改一遍必定費時費力,這時候就需要程式設計師發揮下懶的本質了。

試想,我們經常在寫程式碼的時候定義靜態常量,那麼定義靜態常量的目的什麼呢?當一個常量需要被好幾處程式碼引用的時候,把這個常量定義為靜態常量的好處是當這個常量的值需要改變時我們只需要改變靜態常量的值,其他引用了這個靜態常量的地方都會被改變,做到了一次改變,到處生效;根據這個思想,那麼我們就可以在我們的程式碼中的某處定義一個決定業務元件屬性的常量,然後讓所有業務元件的build.gradle都引用這個常量,這樣當我們改變了常量值的時候,所有引用了這個常量值的業務元件就會根據值的變化改變自己的屬性;可是問題來了?靜態常量是用Java程式碼定義的,而改變元件屬性是需要在Gradle中定義的,Gradle能做到嗎?

Gradle自動構建工具有一個重要屬性,可以幫助我們完成這個事情。每當我們用AndroidStudio建立一個Android專案後,就會在專案的根目錄中生成一個檔案 gradle.properties,我們將使用這個檔案的一個重要屬性:在Android專案中的任何一個build.gradle檔案中都可以把gradle.properties中的常量讀取出來;那麼我們在上面提到解決辦法就有了實際行動的方法,首先我們在gradle.properties中定義一個常量值 isModule(是否是元件開發模式,true為是,false為否)

  1.  # 每次更改“isModule”的值後,需要點選 "Sync Project" 按鈕

  2. isModule=false

然後我們在業務元件的build.gradle中讀取 isModule,但是 gradle.properties 還有一個重要屬性: gradle.properties 中的資料型別都是String型別,使用其他資料型別需要自行轉換;也就是說我們讀到 isModule 是個String型別的值,而我們需要的是Boolean值,程式碼如下: 

  1.  if (isModule.toBoolean()) {

  2. apply plugin: 'com.android.application'

  3. } else {

  4. apply plugin: 'com.android.library'

  5. }

這樣我們第一個問題就解決了,當然了 每次改變isModule的值後,都要同步專案才能生效;

2)元件之間AndroidManifest合併問題

在 AndroidStudio 中每一個元件都會有對應的 AndroidManifest.xml,用於宣告需要的許可權、Application、Activity、Service、Broadcast等,當專案處於元件模式時,業務元件的 AndroidManifest.xml 應該具有一個 Android APP 所具有的的所有屬性,尤其是宣告 Application 和要 launch的Activity,但是當專案處於整合模式的時候,每一個業務元件的 AndroidManifest.xml 都要合併到“app殼工程”中,要是每一個業務元件都有自己的 Application 和 launch的Activity,那麼合併的時候肯定會衝突,試想一個APP怎麼可能會有多個 Application 和 launch 的Activity呢?

但是大家應該注意到這個問題是在元件開發模式和整合開發模式之間轉換引起的問題,而在上一節中我們已經解決了元件模式和整合模式轉換的問題,另外大家應該都經歷過將 Android 專案從 Eclipse 切換到 AndroidStudio 的過程,由於 Android 專案在 Eclipse 和 AndroidStudio開發時 AndroidManifest.xml 檔案的位置是不一樣的,我們需要在build.gradle 中指定下 AndroidManifest.xml 的位置,AndroidStudio 才能讀取到 AndroidManifest.xml,這樣解決辦法也就有了,我們可以為元件開發模式下的業務元件再建立一個 AndroidManifest.xml,然後根據isModule指定AndroidManifest.xml的檔案路徑,讓業務元件在整合模式和元件模式下使用不同的AndroidManifest.xml,這樣表單衝突的問題就可以規避了。

業務元件的目錄結構

上圖是元件化專案中一個標準的業務元件目錄結構,首先我們在main資料夾下建立一個module資料夾用於存放元件開發模式下業務元件的 AndroidManifest.xml,而 AndroidStudio 生成的 AndroidManifest.xml 則依然保留,並用於整合開發模式下業務元件的表單;然後我們需要在業務元件的 build.gradle 中指定表單的路徑,程式碼如下:

  1.   sourceSets {

  2. main {

  3. if (isModule.toBoolean()) {

  4. manifest.srcFile 'src/main/module/AndroidManifest.xml'

  5. } else {

  6. manifest.srcFile 'src/main/AndroidManifest.xml'

  7. }

  8. }

  9. }

這樣在不同的開發模式下就會讀取到不同的 AndroidManifest.xml ,然後我們需要修改這兩個表單的內容以為我們不同的開發模式服務。

首先是整合開發模式下的 AndroidManifest.xml,前面我們說過整合模式下,業務元件的表單是絕對不能擁有自己的 Application 和 launch 的 Activity的,也不能宣告APP名稱、圖示等屬性,總之app殼工程有的屬性,業務元件都不能有,下面是一份標準的整合開發模式下業務元件的 AndroidManifest.xml:

  1.  <manifest xmlns:android="http://schemas.android.com/apk/res/android"

  2. package="com.guiying.girls">

  3. <application android:theme="@style/AppTheme">

  4. <activity

  5. android:name=".main.GirlsActivity"

  6. android:screenOrientation="portrait" />

  7. <activity

  8. android:name=".girl.GirlActivity"

  9. android:screenOrientation="portrait"

  10. android:theme="@style/AppTheme.NoActionBar" />

  11. </application>

  12. </manifest>

我在這個表單中只聲明瞭應用的主題,而且這個主題還是跟app殼工程中的主題是一致的,都引用了common元件中的資原始檔,在這裡宣告主題是為了方便這個業務元件中有使用預設主題的Activity時就不用再給Activity單獨宣告theme了。

然後是元件開發模式下的表單檔案:

  1.  <manifest xmlns:android="http://schemas.android.com/apk/res/android"

  2. package="com.guiying.girls">

  3. <application

  4. android:name="debug.GirlsApplication"

  5. android:allowBackup="true"

  6. android:icon="@mipmap/ic_launcher"

  7. android:label="@string/girls_name"

  8. android:supportsRtl="true"

  9. android:theme="@style/AppTheme">

  10. <activity

  11. android:name=".main.GirlsActivity"

  12. android:screenOrientation="portrait">

  13. <intent-filter>

  14. <action android:name="android.intent.action.MAIN" />

  15. <category android:name="android.intent.category.LAUNCHER" />

  16. </intent-filter>

  17. </activity>

  18. <activity

  19. android:name=".girl.GirlActivity"

  20. android:screenOrientation="portrait"

  21. android:theme="@style/AppTheme.NoActionBar" />

  22. </application>

  23. </manifest>

元件模式下的業務元件表單就是一個Android專案普通的AndroidManifest.xml,這裡就不在過多介紹了。

3)全域性Context的獲取及元件資料初始化

當Android程式啟動時,Android系統會為每個程式建立一個 Application 類的物件,並且只建立一個,application物件的生命週期是整個程式中最長的,它的生命週期就等於這個程式的生命週期。在預設情況下應用系統會自動生成 Application 物件,但是如果我們自定義了 Application,那就需要在 AndroidManifest.xml 中宣告告知系統,例項化的時候,是例項化我們自定義的,而非預設的。

但是我們在元件化開發的時候,可能為了資料的問題每一個元件都會自定義一個Application類,如果我們在自己的元件中開發時需要獲取 全域性的Context,一般都會直接獲取 application 物件,但是當所有元件要打包合併在一起的時候就會出現問題,因為最後程式只有一個 Application,我們元件中自己定義的 Application 肯定是沒法使用的,因此我們需要想辦法再任何一個業務元件中都能獲取到全域性的 Context,而且這個 Context 不管是在元件開發模式還是在整合開發模式都是生效的。

在 元件化工程模型圖中,功能元件集合中有一個 Common 元件, Common 有公共、公用、共同的意思,所以這個元件中主要封裝了專案中需要的基礎功能,並且每一個業務元件都要依賴Common元件,Common 元件就像是萬丈高樓的地基,而業務元件就是在 Common 元件這個地基上搭建起來我們的APP的,Common 元件會專門在一個章節中講解,這裡只講 Common元件中的一個功能,在Common元件中我們封裝了專案中用到的各種Base類,這些基類中就有BaseApplication 類

BaseApplication 主要用於各個業務元件和app殼工程中宣告的 Application 類繼承用的,只要各個業務元件和app殼工程中宣告的Application類繼承了 BaseApplication,當應用啟動時 BaseApplication 就會被動例項化,這樣從 BaseApplication 獲取的 Context 就會生效,也就從根本上解決了我們不能直接從各個元件獲取全域性 Context 的問題;

這時候大家肯定都會有個疑問?不是說了業務元件不能有自己的 Application 嗎,怎麼還讓他們繼承 BaseApplication 呢?其實我前面說的是業務元件不能在整合模式下擁有自己的 Application,但是這不代表業務元件也不能在元件開發模式下擁有自己的Application,其實業務元件在元件開發模式下必須要有自己的 Application 類,一方面是為了讓 BaseApplication 被例項化從而獲取 Context,還有一個作用是,業務元件自己的 Application 可以在元件開發模式下初始化一些資料,例如在元件開發模式下,A元件沒有登入頁面也沒法登入,因此就無法獲取到 Token,這樣請求網路就無法成功,因此我們需要在A元件這個 APP 啟動後就應該已經登入了,這時候元件自己的 Application 類就有了用武之地,我們在元件的 Application的 onCreate 方法中模擬一個登陸介面,在登陸成功後將資料儲存到本地,這樣就可以處理A元件中的資料業務了;另外我們也可以在元件Application中初始化一些第三方庫

但是,實際上業務元件中的Application在最終的整合專案中是沒有什麼實際作用的,元件自己的 Application 僅限於在元件模式下發揮功能,因此我們需要在將專案從元件模式轉換到整合模式後將元件自己的Application剔除出我們的專案;在 AndroidManifest 合併問題小節中介紹瞭如何在不同開發模式下讓 Gradle 識別元件表單的路徑,這個方法也同樣適用於Java程式碼;

業務元件的java目錄結構

我們在Java資料夾下建立一個 debug 資料夾,用於存放不會在業務元件中引用的類,例如上圖中的 NewsApplication ,你甚至可以在 debug 資料夾中建立一個Activity,然後元件表單中宣告啟動這個Activity,在這個Activity中不用setContentView,只需要在啟動你的目標Activity的時候傳遞引數就行,這樣就就可以解決元件模式下某些Activity需要getIntent資料而沒有辦法拿到的情況,程式碼如下;

  1.  public class LauncherActivity extends AppCompatActivity {

  2. @Override

  3. protected void onCreate(Bundle savedInstanceState) {

  4. super.onCreate(savedInstanceState);

  5. request();

  6. Intent intent = new Intent(this, TargetActivity.class);

  7. intent.putExtra("name", "avcd");

  8. intent.putExtra("syscode", "023e2e12ed");

  9. startActivity(intent);

  10. finish();

  11. }

  12. //申請讀寫許可權

  13. private void request() {

  14. AndPermission.with(this)

  15. .requestCode(110)

  16. .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,

  17. Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE)

  18. .callback(this)

  19. .start();

  20. }

  21. }

接下來在業務元件的 build.gradle 中,根據 isModule 是否是整合模式將 debug 這個 Java程式碼資料夾排除:

  1.   sourceSets {

  2. main {

  3. if (isModule.toBoolean()) {

  4. manifest.srcFile 'src/main/module/AndroidManifest.xml'

  5. } else {

  6. manifest.srcFile 'src/main/AndroidManifest.xml'

  7. //整合開發模式下排除debug資料夾中的所有Java檔案

  8. java {

  9. exclude 'debug/**'

  10. }

  11. }

  12. }

  13. }

4)library依賴問題

在介紹這一節的時候,先說一個問題,在元件化工程模型圖中,多媒體元件和Common元件都依賴了日誌元件,而A業務元件有同時依賴了多媒體元件和Common元件,這時候就會有人問,你這樣搞豈不是日誌元件要被重複依賴了,而且Common元件也被每一個業務元件依賴了,這樣不出問題嗎?

其實大家完全沒有必要擔心這個問題,如果真有重複依賴的問題,在你編譯打包的時候就會報錯,如果你還是不相信的話可以反編譯下最後打包出來的APP,看看裡面的程式碼你就知道了。元件只是我們在程式碼開發階段中為了方便叫的一個術語,在元件被打包進APP的時候是沒有這個概念的,這些元件最後都會被打包成arr包,然後被app殼工程所依賴,在構建APP的過程中Gradle會自動將重複的arr包排除,APP中也就不會存在相同的程式碼了;

但是雖然元件是不會重複了,但是我們還是要考慮另一個情況,我們在build.gradle中compile的第三方庫,例如AndroidSupport庫經常會被一些開源的控制元件所依賴,而我們自己一定也會compile AndroidSupport庫 ,這就會造成第三方包和我們自己的包存在重複載入,解決辦法就是找出那個多出來的庫,並將多出來的庫給排除掉,而且Gradle也是支援這樣做的,分別有兩種方式:根據元件名排除或者根據包名排除,下面以排除support-v4庫為例:

  1.  dependencies {

  2. compile fileTree(dir: 'libs', include: ['*.jar'])

  3. compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {

  4. exclude module: 'support-v4'//根據元件名排除

  5. exclude group: 'android.support.v4'//根據包名排除

  6. }

  7. }

library重複依賴的問題算是都解決了,但是我們在開發專案的時候會依賴很多開源庫,而這些庫每個元件都需要用到,要是每個元件都去依賴一遍也是很麻煩的,尤其是給這些庫升級的時候,為了方便我們統一管理第三方庫,我們將給給整個工程提供統一的依賴第三方庫的入口,前面介紹的Common庫的作用之一就是統一依賴開源庫,因為其他業務元件都依賴了Common庫,所以這些業務元件也就間接依賴了Common所依賴的開源庫。

  1.  dependencies {

  2. compile fileTree(dir: 'libs', include: ['*.jar'])

  3. //Android Support

  4. compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"

  5. compile "com.android.support:design:$rootProject.supportLibraryVersion"

  6. compile "com.android.support:percent:$rootProject.supportLibraryVersion"

  7. //網路請求相關

  8. compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"

  9. compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"

  10. compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"

  11. //穩定的

  12. compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"

  13. compile "com.orhanobut:logger:$rootProject.loggerVersion"

  14. compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"

  15. compile "com.google.code.gson:gson:$rootProject.gsonVersion"

  16. compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"

  17. compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"

  18. compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"

  19. //router

  20. compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"

  21. }

5)元件之間呼叫和通訊

在元件化開發的時候,元件之間是沒有依賴關係,我們不能在使用顯示呼叫來跳轉頁面了,因為我們元件化的目的之一就是解決模組間的強依賴問題,假如現在要從A業務元件跳轉到業務B元件,並且要攜帶引數跳轉,這時候怎麼辦呢?而且元件這麼多怎麼管理也是個問題,這時候就需要引入“路由”的概念了,由本文開始的元件化模型下的業務關係圖可知路由就是起到一個轉發的作用。

這裡我將介紹開源庫的“ActivityRouter” ,有興趣的同學情直接去ActivityRouter的Github主頁學習:ActivityRouter,ActivityRouter支援給Activity定義 URL,這樣就可以通過 URL 跳轉到Activity,並且支援從瀏覽器以及 APP 中跳入我們的Activity,而且還支援通過 url 呼叫方法。下面將介紹如何將ActivityRouter整合到元件化專案中以實現元件之間的呼叫;

1、首先我們需要在 Common 元件中的 build.gradle 將ActivityRouter 依賴進來,方便我們在業務元件中呼叫:

  1.  dependencies {

  2. compile fileTree(dir: 'libs', include: ['*.jar'])

  3. //router

  4. compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"

  5. }

2、這一步我們需要先了解 APT這個概念,APT(Annotation Processing Tool)是一種處理註解的工具,它對原始碼檔案進行檢測找出其中的Annotation,使用Annotation進行額外的處理。 Annotation處理器在處理Annotation時可以根據原始檔中的Annotation生成額外的原始檔和其它的檔案(檔案具體內容由Annotation處理器的編寫者決定),APT還會編譯生成的原始檔和原來的原始檔,將它們一起生成class檔案。在這裡我們將在每一個業務元件的 build.gradle 都引入ActivityRouter 的 Annotation處理器,我們將會在宣告元件和Url的時候使用,annotationProcessor是Android官方提供的Annotation處理器外掛,程式碼如下:

  1.  dependencies {

  2. compile fileTree(dir: 'libs', include: ['*.jar'])

  3. annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"

  4. }

3、接下來需要在 app殼工程的 AndroidManifest.xml 配置,到這裡ActivityRouter配置就算完成了:

  1.   <!--宣告整個應用程式的路由協議-->

  2. <activity

  3. android:name="com.github.mzule.activityrouter.router.RouterActivity"

  4. android:theme="@android:style/Theme.NoDisplay">

  5. <intent-filter>

  6. <action android:name="android.intent.action.VIEW" />

  7. <category android:name="android.intent.category.DEFAULT" />

  8. <category android:name="android.intent.category.BROWSABLE" />

  9. <data android:scheme="@string/global_scheme" /> <!-- 改成自己的scheme -->

  10. </intent-filter>

  11. </activity>

  12. <!--傳送崩潰日誌介面-->

4、接下來我們將宣告專案中的業務元件,宣告方法如下:

  1. @Module("girls")

  2. public class Girls {

  3. }

在每一個業務元件的java檔案的根目錄下建立一個類,用 註解@Module 宣告這個業務元件; 
然後在“app殼工程”的  應用Application 中使用 註解@Modules 管理我們宣告的所有業務元件,方法如下:

  1.  @Modules({"main", "girls", "news"})

  2. public class MyApplication extends BaseApplication {

  3. }

到這裡元件化專案中的所有業務元件就被宣告和管理起來了,元件之間的也就可以互相呼叫了,當然前提是要給業務元件中的Activity定義 URL。

5、例如我們給 Girls元件 中的 GirlsActivity 使用  註解@Router 定義一個 URL:“news”,方法如下:

  1.  @Router("girls")

  2. public class GirlsActivity extends BaseActionBarActivity {

  3. private GirlsView mView;

  4. private GirlsContract.Presenter mPresenter;

  5. @Override

  6. protected int setTitleId() {

  7. return R.string.girls_activity_title;

  8. }

  9. @Override

  10. protected void onCreate(Bundle savedInstanceState) {

  11. super.onCreate(savedInstanceState);

  12. mView = new GirlsView(this);

  13. setContentView(mView);

  14. mPresenter = new GirlsPresenter(mView);

  15. mPresenter.start();

  16. }

  17. }

然後我們就可以在專案中的任何一個地方通過 URL地址 : module://girls, 呼叫 GirlsActivity,方法如下:

  Routers.open(MainActivity.this, "module://girls");

元件之間的呼叫解決後,另外需要解決的就是元件之間的通訊,例如A業務元件中有訊息列表,而使用者在B元件中操作某個事件後會產生一條新訊息,需要通知A元件重新整理訊息列表,這樣業務場景需求可以使用Android廣播來解決,也可以使用第三方的事件匯流排來實現,比如EventBus

6)元件之間資源名衝突

因為我們拆分出了很多業務元件和功能元件,在把這些元件合併到“app殼工程”時候就有可能會出現資源名衝突問題,例如A元件和B元件都有一張叫做“ic_back”的圖示,這時候在整合模式下打包APP就會編譯出錯,解決這個問題最簡單的辦法就是在專案中約定資原始檔命名規約,比如強制使每個資原始檔的名稱以元件名開始,這個可以根據實際情況和開發人員制定規則。當然了萬能的Gradle構建工具也提供瞭解決方法,通過在在元件的build.gradle中新增如下的程式碼:

  1.   //設定了resourcePrefix值後,所有的資源名必須以指定的字串做字首,否則會報錯。

  2. //但是resourcePrefix這個值只能限定xml裡面的資源,並不能限定圖片資源,所有圖片資源仍然需要手動去修改資源名。

  3. resourcePrefix "girls_"

但是設定了這個屬性後有個問題,所有的資源名必須以指定的字串做字首,否則會報錯,而且resourcePrefix這個值只能限定xml裡面的資源,並不能限定圖片資源,所有圖片資源仍然需要手動去修改資源名;所以我並不推薦使用這種方法來解決資源名衝突。

4、元件化專案的工程型別

在元件化工程模型中主要有:app殼工程、業務元件和功能元件3種類型,而業務元件中的Main元件和功能元件中的Common元件比較特殊,下面將分別介紹。

1)app殼工程

app殼工程是從名稱來解釋就是一個空殼工程,沒有任何的業務程式碼,也不能有Activity,但它又必須被單獨劃分成一個元件,而不能融合到其他元件中,是因為它有如下幾點重要功能:

1、app殼工程中聲明瞭我們Android應用的 Application,這個 Application 必須繼承自 Common元件中的 BaseApplication(如果你無需實現自己的Application可以直接在表單宣告BaseApplication),因為只有這樣,在打包應用後才能讓BaseApplication中的Context生效,當然你還可以在這個 Application中初始化我們工程中使用到的庫檔案,還可以在這裡解決Android引用方法數不能超過 65535 的限制,對崩潰事件的捕獲和傳送也可以在這裡宣告。

2、app殼工程的 AndroidManifest.xml 是我Android應用的根表單,應用的名稱、圖示以及是否支援備份等等屬性都是在這份表單中配置的,其他元件中的表單最終在整合開發模式下都被合併到這份 AndroidManifest.xml 中。

3、app殼工程的 build.gradle 是比較特殊的,app殼不管是在整合開發模式還是元件開發模式,它的屬性始終都是:com.android.application,因為最終其他的元件都要被app殼工程所依賴,被打包進app殼工程中,這一點從元件化工程模型圖中就能體現出來,所以app殼工程是不需要單獨除錯單獨開發的。另外Android應用的打包簽名,以及buildTypes和defaultConfig都需要在這裡配置,而它的dependencies則需要根據isModule的值分別依賴不同的元件,在元件開發模式下app殼工程只需要依賴Common元件,或者為了防止報錯也可以根據實際情況依賴其他功能元件,而在整合模式下app殼工程必須依賴所有在應用Application中宣告的業務元件,並且不需要再依賴任何功能元件。

下面是一份 app殼工程 的 build.gradle檔案

  1.  apply plugin: 'com.android.application'

  2. static def buildTime() {

  3. return new Date().format("yyyyMMdd");

  4. }

  5. android {

  6. signingConfigs {

  7. release {

  8. keyAlias 'guiying712'

  9. keyPassword 'guiying712'

  10. storeFile file('/mykey.jks')

  11. storePassword 'guiying712'

  12. }

  13. }

  14. compileSdkVersion rootProject.ext.compileSdkVersion

  15. buildToolsVersion rootProject.ext.buildToolsVersion

  16. defaultConfig {

  17. applicationId "com.guiying.androidmodulepattern"

  18. minSdkVersion rootProject.ext.minSdkVersion

  19. targetSdkVersion rootProject.ext.targetSdkVersion

  20. versionCode rootProject.ext.versionCode

  21. versionName rootProject.ext.versionName

  22. multiDexEnabled false

  23. //打包時間

  24. resValue "string", "build_time", buildTime()

  25. }

  26. buildTypes {

  27. release {

  28. //更改AndroidManifest.xml中預先定義好佔位符資訊

  29. //manifestPlaceholders = [app_icon: "@drawable/icon"]

  30. // 不顯示Log

  31. buildConfigField "boolean", "LEO_DEBUG", "false"

  32. //是否zip對齊

  33. zipAlignEnabled true

  34. // 縮減resource檔案

  35. shrinkResources true

  36. //Proguard

  37. minifyEnabled true

  38. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

  39. //簽名

  40. signingConfig signingConfigs.release

  41. }

  42. debug {

  43. //給applicationId新增字尾“.debug”

  44. applicationIdSuffix ".debug"

  45. //manifestPlaceholders = [app_icon: "@drawable/launch_beta"]

  46. buildConfigField "boolean", "LOG_DEBUG", "true"

  47. zipAlignEnabled false

  48. shrinkResources false

  49. minifyEnabled false

  50. debuggable true

  51. }

  52. }

  53. }

  54. dependencies {

  55. compile fileTree(dir: 'libs', include: ['*.jar'])

  56. annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"

  57. if (isModule.toBoolean()) {

  58. compile project(':lib_common')

  59. } else {

  60. compile project(':module_main')

  61. compile project(':module_girls')

  62. compile project(':module_news')

  63. }

  64. }

2)功能元件和Common元件

功能元件是為了支撐業務元件的某些功能而獨立劃分出來的元件,功能實質上跟專案中引入的第三方庫是一樣的,功能元件的特徵如下:

1、功能元件的 AndroidManifest.xml 是一張空表,這張表中只有功能元件的包名;

2、功能元件不管是在整合開發模式下還是元件開發模式下屬性始終是: com.android.library,所以功能元件是不需要讀取 gradle.properties 中的 isModule 值的;另外功能元件的 build.gradle 也無需設定 buildTypes ,只需要 dependencies 這個功能元件需要的jar包和開源庫。

下面是一份 普通 的功能元件的 build.gradle檔案

  1.  apply plugin: 'com.android.library'

  2. android {

  3. compileSdkVersion rootProject.ext.compileSdkVersion

  4. buildToolsVersion rootProject.ext.buildToolsVersion

  5. defaultConfig {

  6. minSdkVersion rootProject.ext.minSdkVersion

  7. targetSdkVersion rootProject.ext.targetSdkVersion

  8. versionCode rootProject.ext.versionCode

  9. versionName rootProject.ext.versionName

  10. }

  11. }

  12. dependencies {

  13. compile fileTree(dir: 'libs', include: ['*.jar'])

  14. }

Common元件除了有功能元件的普遍屬性外,還具有其他功能

1、Common元件的 AndroidManifest.xml 不是一張空表,這張表中聲明瞭我們 Android應用用到的所有使用許可權 uses-permission 和 uses-feature,放到這裡是因為在元件開發模式下,所有業務元件就無需在自己的 AndroidManifest.xm 宣告自己要用到的許可權了。

2、Common元件的 build.gradle 需要統一依賴業務元件中用到的 第三方依賴庫和jar包,例如我們用到的ActivityRouter、Okhttp等等。

3、Common元件中封裝了Android應用的 Base類和網路請求工具、圖片載入工具等等,公用的 widget控制元件也應該放在Common 元件中;業務元件中都用到的資料也應放於Common元件中,例如儲存到 SharedPreferences 和 DataBase 中的登陸資料;

4、Common元件的資原始檔中需要放置專案公用的 Drawable、layout、sting、dimen、color和style 等等,另外專案中的 Activity 主題必須定義在 Common中,方便和 BaseActivity 配合保持整個Android應用的介面風格統一。

下面是一份 Common功能元件的 build.gradle檔案

  1. apply plugin: 'com.android.library'

  2. android {

  3. compileSdkVersion rootProject.ext.compileSdkVersion

  4. buildToolsVersion rootProject.ext.buildToolsVersion

  5. defaultConfig {

  6. minSdkVersion rootProject.ext.minSdkVersion

  7. targetSdkVersion rootProject.ext.targetSdkVersion

  8. versionCode rootProject.ext.versionCode

  9. versionName rootProject.ext.versionName

  10. }

  11. }

  12. dependencies {

  13. compile fileTree(dir: 'libs', include: ['*.jar'])

  14. //Android Support

  15. compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"

  16. compile "com.android.support:design:$rootProject.supportLibraryVersion"

  17. compile "com.android.support:percent:$rootProject.supportLibraryVersion"

  18. //網路請求相關

  19. compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"

  20. compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"

  21. compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"

  22. //穩定的

  23. compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"

  24. compile "com.orhanobut:logger:$rootProject.loggerVersion"

  25. compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"

  26. compile "com.google.code.gson:gson:$rootProject.gsonVersion"

  27. compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"

  28. compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"

  29. compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"

  30. //router

  31. compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"

  32. }

2)業務元件和Main元件

業務元件就是根據業務邏輯的不同拆分出來的元件,業務元件的特徵如下:

1、業務元件中要有兩張AndroidManifest.xml,分別對應元件開發模式和整合開發模式,這兩張表的區別請檢視 元件之間AndroidManifest合併問題 小節。

2、業務元件在整合模式下是不能有自己的Application的,但在元件開發模式下又必須實現自己的Application並且要繼承自Common元件的BaseApplication,並且這個Application不能被業務元件中的程式碼引用,因為它的功能就是為了使業務元件從BaseApplication中獲取的全域性Context生效,還有初始化資料之用。

3、業務元件有debug資料夾,這個資料夾在整合模式下會從業務元件的程式碼中排除掉,所以debug資料夾中的類不能被業務元件強引用,例如元件模式下的 Application 就是置於這個資料夾中,還有元件模式下開發給目標 Activity 傳遞引數的用的 launch Activity 也應該置於 debug 資料夾中;

4、業務元件必須在自己的 Java資料夾中建立業務元件宣告類,以使  app殼工程 中的 應用Application能夠引用,實現元件跳轉,具體請檢視 元件之間呼叫和通訊 小節;

5、業務元件必須在自己的 build.gradle 中根據 isModule 值的不同改變自己的屬性,在元件模式下是:com.android.application,而在整合模式下com.android.library;同時還需要在build.gradle配置資原始檔,如 指定不同開發模式下的AndroidManifest.xml檔案路徑,排除debug資料夾等;業務元件還必須在dependencies中依賴Common元件,並且引入ActivityRouter的註解處理器annotationProcessor,以及依賴其他用到的功能元件。

下面是一份普通業務元件的 build.gradle檔案

  1.  if (isModule.toBoolean()) {

  2. apply plugin: 'com.android.application'

  3. } else {

  4. apply plugin: 'com.android.library'

  5. }

  6. android {

  7. compileSdkVersion rootProject.ext.compileSdkVersion

  8. buildToolsVersion rootProject.ext.buildToolsVersion

  9. defaultConfig {

  10. minSdkVersion rootProject.ext.minSdkVersion

  11. targetSdkVersion rootProject.ext.targetSdkVersion

  12. versionCode rootProject.ext.versionCode

  13. versionName rootProject.ext.versionName

  14. }

  15. sourceSets {

  16. main {

  17. if (isModule.toBoolean()) {

  18. manifest.srcFile 'src/main/module/AndroidManifest.xml'

  19. } else {

  20. manifest.srcFile 'src/main/AndroidManifest.xml'

  21. //整合開發模式下排除debug資料夾中的所有Java檔案

  22. java {

  23. exclude 'debug/**'

  24. }

  25. }

  26. }

  27. }

  28. //設定了resourcePrefix值後,所有的資源名必須以指定的字串做字首,否則會報錯。

  29. //但是resourcePrefix這個值只能限定xml裡面的資源,並不能限定圖片資源,所有圖片資源仍然需要手動去修改資源名。

  30. //resourcePrefix "girls_"

  31. }

  32. dependencies {

  33. compile fileTree(dir: 'libs', include: ['*.jar'])

  34. annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"

  35. compile project(':lib_common')

  36. }

Main元件除了有業務元件的普遍屬性外,還有一項重要功能

1、Main元件整合模式下的AndroidManifest.xml是跟其他業務元件不一樣的,Main元件的表單中聲明瞭我們整個Android應用的launch Activity,這就是Main元件的獨特之處;所以我建議SplashActivity、登陸Activity以及主介面都應屬於Main元件,也就是說Android應用啟動後要呼叫的頁面應置於Main元件。

  1.   <activity

  2. android:name=".splash.SplashActivity"

  3. android:launchMode="singleTop"

  4. android:screenOrientation="portrait"

  5. android:theme="@style/SplashTheme">

  6. <intent-filter>

  7. <action android:name="android.intent.action.MAIN" />

  8. <category android:name="android.intent.category.LAUNCHER" />

  9. </intent-filter>

  10. </activity>

5、元件化專案的混淆方案

元件化專案的Java程式碼混淆方案採用在整合模式下集中在app殼工程中混淆,各個業務元件不配置混淆檔案。整合開發模式下在app殼工程中build.gradle檔案的release構建型別中開啟混淆屬性,其他buildTypes配置方案跟普通專案保持一致,Java混淆配置檔案也放置在app殼工程中,各個業務元件的混淆配置規則都應該在app殼工程中的混淆配置檔案中新增和修改。

之所以不採用在每個業務元件中開啟混淆的方案,是因為 元件在整合模式下都被 Gradle 構建成了 release 型別的arr包,一旦業務元件的程式碼被混淆,而這時候程式碼中又出現了bug,將很難根據日誌找出導致bug的原因;另外每個業務元件中都保留一份混淆配置檔案非常不便於修改和管理,這也是不推薦在業務元件的 build.gradle 檔案中配置 buildTypes (構建型別)的原因。

6、工程的build.gradle和gradle.properties檔案

1)元件化工程的build.gradle檔案

在元件化專案中因為每個元件的 build.gradle 都需要配置 compileSdkVersion、buildToolsVersion和defaultConfig 等的版本號,而且每個元件都需要用到 annotationProcessor,為了能夠使元件化專案中的所有元件的 build.gradle 中的這些配置都能保持統一,並且也是為了方便修改版本號,我們統一在Android工程根目錄下的build.gradle中定義這些版本號,當然為了方便管理Common元件中的第三方開源庫的版本號,最好也在這裡定義這些開源庫的版本號,然後在各個元件的build.gradle中引用Android工程根目錄下的build.gradle定義的版本號,元件化工程的 build.gradle 檔案程式碼如下:

  1.  buildscript {

  2. repositories {

  3. jcenter()

  4. mavenCentral()

  5. }

  6. dependencies {

  7. //classpath "com.android.tools.build:gradle:$localGradlePluginVersion"

  8. //$localGradlePluginVersion是gradle.properties中的資料

  9. classpath "com.android.tools.build:gradle:$localGradlePluginVersion"

  10. }

  11. }

  12. allprojects {

  13. repositories {

  14. jcenter()

  15. mavenCentral()

  16. //Add the JitPack repository

  17. maven { url "https://jitpack.io" }

  18. //支援arr包

  19. flatDir {

  20. dirs 'libs'

  21. }

  22. }

  23. }

  24. task clean(type: Delete) {

  25. delete rootProject.buildDir

  26. }

  27. // Define versions in a single place

  28. //時間:2017.2.13;每次修改版本號都要新增修改時間

  29. ext {

  30. // Sdk and tools

  31. //localBuildToolsVersion是gradle.properties中的資料

  32. buildToolsVersion = localBuildToolsVersion

  33. compileSdkVersion = 25

  34. minSdkVersion = 16

  35. targetSdkVersion = 25

  36. versionCode = 1

  37. versionName = "1.0"

  38. javaVersion = JavaVersion.VERSION_1_8

  39. // App dependencies version

  40. supportLibraryVersion = "25.3.1"

  41. retrofitVersion = "2.1.0"

  42. glideVersion = "3.7.0"

  43. loggerVersion = "1.15"

  44. eventbusVersion = "3.0.0"

  45. gsonVersion = "2.8.0"

  46. photoViewVersion = "2.0.0"

  47. //需檢查升級版本

  48. annotationProcessor = "1.1.7"

  49. routerVersion = "1.2.2"

  50. easyRecyclerVersion = "4.4.0"

  51. cookieVersion = "v1.0.1"

  52. toastyVersion = "1.1.3"

  53. }

2)元件化工程的gradle.properties檔案

在元件化實施流程中我們瞭解到gradle.properties有兩個屬性對我們非常有用:

1、在Android專案中的任何一個build.gradle檔案中都可以把gradle.properties中的常量讀取出來,不管這個build.gradle是元件的還是整個專案工程的build.gradle;

2、gradle.properties中的資料型別都是String型別,使用其他資料型別需要自行轉換;

利用gradle.properties的屬性不僅可以解決整合開發模式和元件開發模式的轉換,而且還可以解決在多人協同開發Android專案的時候,因為開發團隊成員的Android開發環境(開發環境指Android SDK和AndroidStudio)不一致而導致頻繁改變線上專案的build.gradle配置。

在每個Android元件的 build.gradle 中有一個屬性:buildToolsVersion,表示構建工具的版本號,這個屬性值對應 AndroidSDK 中的 Android SDK Build-tools,正常情況下 build.gradle 中的 buildToolsVersion 跟你電腦中 Android SDK Build-tools 的最新版本是一致的,比如現在 Android SDK Build-tools 的最新的版本是:25.0.3,那麼我的Android專案中 build.gradle 中的 buildToolsVersion 版本號也是 25.0.3,但是一旦一個Android專案是由好幾個人同時開發,總會出現每個人的開發環境 Android SDK Build-tools 是都是不一樣的,並不是所有人都會經常升級更新 Android SDK,而且程式碼是儲存到線上環境的(例如使用 SVN/Git 等工具),某個開發人員提交程式碼後線上Android專案中 build.gradle 中的 buildToolsVersion 也會被不斷地改變。 

另外一個原因是因為Android工程的根目錄下的 build.gradle 聲明瞭 Android Gradle 構建工具,而這個工具也是有版本號的,而且 Gradle Build Tools 的版本號跟 AndroidStudio 版本號一致的,但是有些開發人員基本很久都不會升級自己的 AndroidStudio 版本,導致團隊中每個開發人員的 Gradle Build Tools 的版本號也不一致。

如果每次同步程式碼後這兩個工具的版本號被改變了,開發人員可以自己手動改回來,並且不要把改動工具版本號的程式碼提交到線上環境,這樣還可以勉強繼續開發;但是很多公司都會使用持續整合工具(例如Jenkins)用於持續的軟體版本釋出,而Android出包是需要 Android SDK Build-tools 和 Gradle Build Tools 配合的,一旦提交到線上的版本跟持續整合工具所依賴的Android環境構建工具版本號不一致就會導致Android打包失敗。

為了解決上面問題就必須將Android專案中 build.gradle 中的 buildToolsVersion 和 GradleBuildTools 版本號從線上程式碼隔離出來,保證線上程式碼的 buildToolsVersion 和 Gradle Build Tools 版本號不會被人為改變。

7、元件化專案Router的其他方案-ARouter

在元件化專案中使用到的跨元件跳轉庫ActivityRouter可以使用阿里巴巴的開源路由專案:阿里巴巴ARouter

ActivityRouter和ARouter的接入元件化專案的方式是一樣的,ActivityRouter提供的功能目前ARouter也全部支援,但是ARouter還支援依賴注入解耦,頁面、攔截器、服務等元件均會自動註冊到框架。對於大家來說,沒有最好的只有最適合的,大家可以根據自己的專案選擇合適的Router。

下面將介紹ARouter的基礎使用方法,更多功能還需大家去Github自己學習;

1、首先 ARouter 這個框架是需要初始化SDK的,所以你需要在“app殼工程”中的應用Application中加入下面的程式碼,注意:在 debug 模式下一定要 openDebug

  1.   if (BuildConfig.DEBUG) {

  2. //一定要在ARouter.init之前呼叫openDebug

  3. ARouter.openDebug();

  4. ARouter.openLog();

  5. }

  6. ARouter.init(this);

2、首先我們依然需要在 Common 元件中的 build.gradle 將ARouter 依賴進來,方便我們在業務元件中呼叫:

  1.  dependencies {

  2. compile fileTree(dir: 'libs', include: ['*.jar'])

  3. //router

  4. compile 'com.alibaba:arouter-api:1.2.1.1'

  5. }

3、然後在每一個業務元件的 build.gradle 都引入ARouter 的 Annotation處理器,程式碼如下:

  1.  android {

  2. defaultConfig {

  3. ...

  4. javaCompileOptions {

  5. annotationProcessorOptions {

  6. arguments = [ moduleName : project.getName() ]

  7. }

  8. }

  9. }

  10. }

  11. dependencies {

  12. compile fileTree(dir: 'libs', include: ['*.jar'])

  13. annotationProcessor 'com.alibaba:arouter-compiler:1.0.3'

  14. }

4、由於ARouter支援自動註冊到框架,所以我們不用像ActivityRouter那樣在各個元件中宣告元件,當然更不需要在Application中管理元件了。 我們給 Girls元件 中的 GirlsActivity 添加註解:@Route(path = “/girls/list”),需要注意的是這裡的路徑至少需要有兩級,/xx/xx,之所以這樣是因為ARouter使用了路徑中第一段字串(/*/)作為