Android組件化方案

Android組件化項目地址:Android組件化項目AndroidModulePatternhtml

Android組件化之終極方案地址:http://blog.csdn.net/guiying712/article/details/78057120java

一、爲何要項目組件化

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

單一工程模型

上圖是目前比較廣泛使用的Android APP技術架構,每每是在一個界面中存在大量的業務邏輯,而業務邏輯中充斥着各類網絡請求、數據操做等行爲,整個項目中也沒有模塊的概念,只有簡單的以業務邏輯劃分的文件夾,而且業務之間也是直接相互調用、高度耦合在一塊兒的;git

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

上圖單一工程模型下的業務關係,總的來講就是:你中有我,我中有你,相互依賴,沒法分離。
然而隨着產品的迭代,業務愈來愈複雜,隨之帶來的是項目結構複雜度的極度增長,此時咱們會面臨以下幾個問題:程序員

一、實際業務變化很是快,可是單一工程的業務模塊耦合度過高,牽一髮而動全身;
二、對工程所作的任何修改都必需要編譯整個工程;
三、功能測試和系統測試每次都要進行;
四、團隊協同開發存在較多的衝突.不得不花費更多的時間去溝通和協調,而且在開發過程當中,任何一位成員沒辦法專一於本身的功能點,影響開發效率;
五、不能靈活的對業務模塊進行配置和組裝;github

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


二、如何組件化

組件化工程模型

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

名詞 含義
集成模式 全部的業務組件被「app殼工程」依賴,組成一個完整的APP;
組件模式 能夠獨立開發業務組件,每個業務組件就是一個APP;
app殼工程 負責管理各個業務組件,和打包apk,沒有具體的業務功能;
業務組件 根據公司具體業務而獨立造成一個的工程;
功能組件 提供開發APP的某些基礎功能,例如打印日誌、樹狀圖等;
Main組件 屬於業務組件,指定APP啓動頁面、主界面;
Common組件 屬於功能組件,支撐業務組件的基礎,提供多數業務組件須要的功能,例如提供網絡請求功能;

**
Android APP組件化架構的目標是告別結構臃腫,讓各個業務變得相對獨立,業務組件在組件模式下能夠獨立開發,而在集成模式下又能夠變爲arr包集成到「app殼工程」中,組成一個完整功能的APP;
從組件化工程模型中能夠看到,業務組件之間是獨立的,沒有關聯的,這些業務組件在集成模式下是一個個library,被app殼工程所依賴,組成一個具備完整業務功能的APP應用,可是在組件開發模式下,業務組件又變成了一個個application,它們能夠獨立開發和調試,因爲在組件開發模式下,業務組件們的代碼量相比於完整的項目差了很遠,所以在運行時能夠顯著減小編譯時間。cookie

組件化工程下的業務關係

這是組件化工程模型下的業務關係,業務之間將再也不直接引用和依賴,而是經過「路由」這樣一箇中轉站間接產生聯繫,而Android中的路由實際就是對URL Scheme的封裝;
如此規模大的架構整改須要付出更高的成本,還會涉及一些潛在的風險,可是整改後的架構可以帶來不少好處:網絡

一、加快業務迭代速度,各個業務模塊組件更加獨立,再也不出現業務耦合狀況;
二、穩定的公共模塊採用依賴庫方式,提供給各個業務線使用,減小重複開發和維護工做量;
三、迭代頻繁的業務模塊採用組件方式,各業務研發能夠互不干擾、提高協做效率,並控制產品質量;
四、爲新業務隨時集成提供了基礎,全部業務可上可下,靈活多變;
五、下降團隊成員熟悉項目的成本,下降項目的維護難度;
六、加快編譯速度,提升開發效率;
七、控制代碼權限,將代碼的權限細分到更小的粒度;


三、組件化實施流程

1)組件模式和集成模式的轉換

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

一、application屬性,能夠獨立運行的Android程序,也就是咱們的APP;

apply plugin: ‘com.android.application’

 

二、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爲否)

# 每次更改「isModule」的值後,須要點擊 "Sync Project" 按鈕 isModule=false

 

而後咱們在業務組件的build.gradle中讀取 isModule,可是 gradle.properties 還有一個重要屬性: gradle.properties 中的數據類型都是String類型,使用其餘數據類型須要自行轉換;也就是說咱們讀到 isModule 是個String類型的值,而咱們須要的是Boolean值,代碼以下:

if (isModule.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }

 

這樣咱們第一個問題就解決了,固然了 每次改變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 中指定表單的路徑,代碼以下:

sourceSets {
        main {
            if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } }

 

這樣在不一樣的開發模式下就會讀取到不一樣的 AndroidManifest.xml ,而後咱們須要修改這兩個表單的內容覺得咱們不一樣的開發模式服務。

首先是集成開發模式下的 AndroidManifest.xml,前面咱們說過集成模式下,業務組件的表單是絕對不能擁有本身的 Application 和 launch 的 Activity的,也不能聲明APP名稱、圖標等屬性,總之app殼工程有的屬性,業務組件都不能有,下面是一份標準的集成開發模式下業務組件的 AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.guiying.girls"> <application android:theme="@style/AppTheme"> <activity android:name=".main.GirlsActivity" android:screenOrientation="portrait" /> <activity android:name=".girl.GirlActivity" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" /> </application> </manifest> 

我在這個表單中只聲明瞭應用的主題,並且這個主題仍是跟app殼工程中的主題是一致的,都引用了common組件中的資源文件,在這裏聲明主題是爲了方便這個業務組件中有使用默認主題的Activity時就不用再給Activity單獨聲明theme了。

而後是組件開發模式下的表單文件:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.guiying.girls"> <application android:name="debug.GirlsApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/girls_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".main.GirlsActivity" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".girl.GirlActivity" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" /> </application> </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數據而沒有辦法拿到的狀況,代碼以下;

public class LauncherActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); request(); Intent intent = new Intent(this, TargetActivity.class); intent.putExtra("name", "avcd"); intent.putExtra("syscode", "023e2e12ed"); startActivity(intent); finish(); } //申請讀寫權限 private void request() { AndPermission.with(this) .requestCode(110) .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE) .callback(this) .start(); } }

接下來在業務組件的 build.gradle 中,根據 isModule 是不是集成模式將 debug 這個 Java代碼文件夾排除:

sourceSets {
        main {
            if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' //集成開發模式下排除debug文件夾中的全部Java文件 java { exclude 'debug/**' } } } }

 


4)library依賴問題

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

其實你們徹底沒有必要擔憂這個問題,若是真有重複依賴的問題,在你編譯打包的時候就會報錯,若是你仍是不相信的話能夠反編譯下最後打包出來的APP,看看裏面的代碼你就知道了。組件只是咱們在代碼開發階段中爲了方便叫的一個術語,在組件被打包進APP的時候是沒有這個概念的,這些組件最後都會被打包成arr包,而後被app殼工程所依賴,在構建APP的過程當中Gradle會自動將重複的arr包排除,APP中也就不會存在相同的代碼了;

可是雖然組件是不會重複了,可是咱們仍是要考慮另外一個狀況,咱們在build.gradle中compile的第三方庫,例如AndroidSupport庫常常會被一些開源的控件所依賴,而咱們本身必定也會compile AndroidSupport庫 ,這就會形成第三方包和咱們本身的包存在重複加載,解決辦法就是找出那個多出來的庫,並將多出來的庫給排除掉,並且Gradle也是支持這樣作的,分別有兩種方式:根據組件名排除或者根據包名排除,下面以排除support-v4庫爲例:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") { exclude module: 'support-v4'//根據組件名排除 exclude group: 'android.support.v4'//根據包名排除 } }

 

library重複依賴的問題算是都解決了,可是咱們在開發項目的時候會依賴不少開源庫,而這些庫每一個組件都須要用到,要是每一個組件都去依賴一遍也是很麻煩的,尤爲是給這些庫升級的時候,爲了方便咱們統一管理第三方庫,咱們將給給整個工程提供統一的依賴第三方庫的入口,前面介紹的Common庫的做用之一就是統一依賴開源庫,由於其餘業務組件都依賴了Common庫,因此這些業務組件也就間接依賴了Common所依賴的開源庫。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) //Android Support compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" compile "com.android.support:design:$rootProject.supportLibraryVersion" compile "com.android.support:percent:$rootProject.supportLibraryVersion" //網絡請求相關 compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion" compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion" //穩定的 compile "com.github.bumptech.glide:glide:$rootProject.glideVersion" compile "com.orhanobut:logger:$rootProject.loggerVersion" compile "org.greenrobot:eventbus:$rootProject.eventbusVersion" compile "com.google.code.gson:gson:$rootProject.gsonVersion" compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion" compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion" compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion" //router compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion" }

 


5)組件之間調用和通訊

在組件化開發的時候,組件之間是沒有依賴關係,咱們不能在使用顯示調用來跳轉頁面了,由於咱們組件化的目的之一就是解決模塊間的強依賴問題,假如如今要從A業務組件跳轉到業務B組件,而且要攜帶參數跳轉,這時候怎麼辦呢?並且組件這麼多怎麼管理也是個問題,這時候就須要引入「路由」的概念了,由本文開始的組件化模型下的業務關係圖可知路由就是起到一個轉發的做用。

這裏我將介紹開源庫的「ActivityRouter」 ,有興趣的同窗情直接去ActivityRouter的Github主頁學習:ActivityRouter,ActivityRouter支持給Activity定義 URL,這樣就能夠經過 URL 跳轉到Activity,而且支持從瀏覽器以及 APP 中跳入咱們的Activity,並且還支持經過 url 調用方法。下面將介紹如何將ActivityRouter集成到組件化項目中以實現組件之間的調用;

一、首先咱們須要在 Common 組件中的 build.gradle 將ActivityRouter 依賴進來,方便咱們在業務組件中調用:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) //router compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion" }

 

二、這一步咱們須要先了解 APT這個概念,APT(Annotation Processing Tool)是一種處理註解的工具,它對源代碼文件進行檢測找出其中的Annotation,使用Annotation進行額外的處理。 Annotation處理器在處理Annotation時能夠根據源文件中的Annotation生成額外的源文件和其它的文件(文件具體內容由Annotation處理器的編寫者決定),APT還會編譯生成的源文件和原來的源文件,將它們一塊兒生成class文件。在這裏咱們將在每個業務組件的 build.gradle 都引入ActivityRouter 的 Annotation處理器,咱們將會在聲明組件和Url的時候使用,annotationProcessor是Android官方提供的Annotation處理器插件,代碼以下:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor" }

 

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

<!--聲明整個應用程序的路由協議--> <activity android:name="com.github.mzule.activityrouter.router.RouterActivity" android:theme="@android:style/Theme.NoDisplay"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/global_scheme" /> <!-- 改爲本身的scheme --> </intent-filter> </activity> <!--發送崩潰日誌界面-->

 

四、接下來咱們將聲明項目中的業務組件,聲明方法以下:

@Module("girls") public class Girls { }

在每個業務組件的java文件的根目錄下建立一個類,用 註解@Module 聲明這個業務組件;
而後在「app殼工程」的 應用Application 中使用 註解@Modules 管理咱們聲明的全部業務組件,方法以下:

@Modules({"main", "girls", "news"}) public class MyApplication extends BaseApplication { }

 

到這裏組件化項目中的全部業務組件就被聲明和管理起來了,組件之間的也就能夠互相調用了,固然前提是要給業務組件中的Activity定義 URL。

五、例如咱們給 Girls組件 中的 GirlsActivity 使用 註解@Router 定義一個 URL:「news」,方法以下:

@Router("girls") public class GirlsActivity extends BaseActionBarActivity { private GirlsView mView; private GirlsContract.Presenter mPresenter; @Override protected int setTitleId() { return R.string.girls_activity_title; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mView = new GirlsView(this); setContentView(mView); mPresenter = new GirlsPresenter(mView); mPresenter.start(); } }

 

而後咱們就能夠在項目中的任何一個地方經過 URL地址 : module://girls, 調用 GirlsActivity,方法以下:

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

組件之間的調用解決後,另外須要解決的就是組件之間的通訊,例如A業務組件中有消息列表,而用戶在B組件中操做某個事件後會產生一條新消息,須要通知A組件刷新消息列表,這樣業務場景需求可使用Android廣播來解決,也可使用第三方的事件總線來實現,好比EventBus


6)組件之間資源名衝突

由於咱們拆分出了不少業務組件和功能組件,在把這些組件合併到「app殼工程」時候就有可能會出現資源名衝突問題,例如A組件和B組件都有一張叫作「ic_back」的圖標,這時候在集成模式下打包APP就會編譯出錯,解決這個問題最簡單的辦法就是在項目中約定資源文件命名規約,好比強制使每一個資源文件的名稱以組件名開始,這個能夠根據實際狀況和開發人員制定規則。固然了萬能的Gradle構建工具也提供瞭解決方法,經過在在組件的build.gradle中添加以下的代碼:

//設置了resourcePrefix值後,全部的資源名必須以指定的字符串作前綴,不然會報錯。 //可是resourcePrefix這個值只能限定xml裏面的資源,並不能限定圖片資源,全部圖片資源仍然須要手動去修改資源名。 resourcePrefix "girls_"

 

可是設置了這個屬性後有個問題,全部的資源名必須以指定的字符串作前綴,不然會報錯,並且resourcePrefix這個值只能限定xml裏面的資源,並不能限定圖片資源,全部圖片資源仍然須要手動去修改資源名;因此我並不推薦使用這種方法來解決資源名衝突。


四、組件化項目的工程類型

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

1)app殼工程

app殼工程是從名稱來解釋就是一個空殼工程,沒有任何的業務代碼,也不能有Activity,但它又必須被單獨劃分紅一個組件,而不能融合到其餘組件中,是由於它有以下幾點重要功能:

一、app殼工程中聲明瞭咱們Android應用的 Application,這個 Application 必須繼承自 Common組件中的 BaseApplication(若是你無需實現本身的Application能夠直接在表單聲明BaseApplication),由於只有這樣,在打包應用後才能讓BaseApplication中的Context生效,固然你還能夠在這個 Application中初始化咱們工程中使用到的庫文件,還能夠在這裏解決Android引用方法數不能超過 65535 的限制,對崩潰事件的捕獲和發送也能夠在這裏聲明。

二、app殼工程的 AndroidManifest.xml 是我Android應用的根表單,應用的名稱、圖標以及是否支持備份等等屬性都是在這份表單中配置的,其餘組件中的表單最終在集成開發模式下都被合併到這份 AndroidManifest.xml 中。

三、app殼工程的 build.gradle 是比較特殊的,app殼無論是在集成開發模式仍是組件開發模式,它的屬性始終都是:com.android.application,由於最終其餘的組件都要被app殼工程所依賴,被打包進app殼工程中,這一點從組件化工程模型圖中就能體現出來,因此app殼工程是不須要單獨調試單獨開發的。另外Android應用的打包簽名,以及buildTypes和defaultConfig都須要在這裏配置,而它的dependencies則須要根據isModule的值分別依賴不一樣的組件,在組件開發模式下app殼工程只須要依賴Common組件,或者爲了防止報錯也能夠根據實際狀況依賴其餘功能組件,而在集成模式下app殼工程必須依賴全部在應用Application中聲明的業務組件,而且不須要再依賴任何功能組件。

下面是一份 app殼工程 的 build.gradle文件

apply plugin: 'com.android.application' static def buildTime() { return new Date().format("yyyyMMdd"); } android { signingConfigs { release { keyAlias 'guiying712' keyPassword 'guiying712' storeFile file('/mykey.jks') storePassword 'guiying712' } } compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { applicationId "com.guiying.androidmodulepattern" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName multiDexEnabled false //打包時間 resValue "string", "build_time", buildTime() } buildTypes { release { //更改AndroidManifest.xml中預先定義好佔位符信息 //manifestPlaceholders = [app_icon: "@drawable/icon"] // 不顯示Log buildConfigField "boolean", "LEO_DEBUG", "false" //是否zip對齊 zipAlignEnabled true // 縮減resource文件 shrinkResources true //Proguard minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' //簽名 signingConfig signingConfigs.release } debug { //給applicationId添加後綴「.debug」 applicationIdSuffix ".debug" //manifestPlaceholders = [app_icon: "@drawable/launch_beta"] buildConfigField "boolean", "LOG_DEBUG", "true" zipAlignEnabled false shrinkResources false minifyEnabled false debuggable true } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor" if (isModule.toBoolean()) { compile project(':lib_common') } else { compile project(':module_main') compile project(':module_girls') compile project(':module_news') } }

 

2)功能組件和Common組件

功能組件是爲了支撐業務組件的某些功能而獨立劃分出來的組件,功能實質上跟項目中引入的第三方庫是同樣的,功能組件的特徵以下:

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

二、功能組件無論是在集成開發模式下仍是組件開發模式下屬性始終是: com.android.library,因此功能組件是不須要讀取 gradle.properties 中的 isModule 值的;另外功能組件的 build.gradle 也無需設置 buildTypes ,只須要 dependencies 這個功能組件須要的jar包和開源庫。

下面是一份 普通 的功能組件的 build.gradle文件

apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) }

 

Common組件除了有功能組件的廣泛屬性外,還具備其餘功能

一、Common組件的 AndroidManifest.xml 不是一張空表,這張表中聲明瞭咱們 Android應用用到的全部使用權限 uses-permission 和 uses-feature,放到這裏是由於在組件開發模式下,全部業務組件就無需在本身的 AndroidManifest.xm 聲明本身要用到的權限了。

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

三、Common組件中封裝了Android應用的 Base類和網絡請求工具、圖片加載工具等等,公用的 widget控件也應該放在Common 組件中;業務組件中都用到的數據也應放於Common組件中,例如保存到 SharedPreferences 和 DataBase 中的登錄數據;

四、Common組件的資源文件中須要放置項目公用的 Drawable、layout、sting、dimen、color和style 等等,另外項目中的 Activity 主題必須定義在 Common中,方便和 BaseActivity 配合保持整個Android應用的界面風格統一。

下面是一份 Common功能組件的 build.gradle文件

apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) //Android Support compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" compile "com.android.support:design:$rootProject.supportLibraryVersion" compile "com.android.support:percent:$rootProject.supportLibraryVersion" //網絡請求相關 compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion" compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion" //穩定的 compile "com.github.bumptech.glide:glide:$rootProject.glideVersion" compile "com.orhanobut:logger:$rootProject.loggerVersion" compile "org.greenrobot:eventbus:$rootProject.eventbusVersion" compile "com.google.code.gson:gson:$rootProject.gsonVersion" compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion" compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion" compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion" //router compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion" } 

 

2)業務組件和Main組件

業務組件就是根據業務邏輯的不一樣拆分出來的組件,業務組件的特徵以下:

一、業務組件中要有兩張AndroidManifest.xml,分別對應組件開發模式和集成開發模式,這兩張表的區別請查看 組件之間AndroidManifest合併問題 小節。

二、業務組件在集成模式下是不能有本身的Application的,但在組件開發模式下又必須實現本身的Application而且要繼承自Common組件的BaseApplication,而且這個Application不能被業務組件中的代碼引用,由於它的功能就是爲了使業務組件從BaseApplication中獲取的全局Context生效,還有初始化數據之用。

三、業務組件有debug文件夾,這個文件夾在集成模式下會從業務組件的代碼中排除掉,因此debug文件夾中的類不能被業務組件強引用,例如組件模式下的 Application 就是置於這個文件夾中,還有組件模式下開發給目標 Activity 傳遞參數的用的 launch Activity 也應該置於 debug 文件夾中;

四、業務組件必須在本身的 Java文件夾中建立業務組件聲明類,以使 app殼工程 中的 應用Application可以引用,實現組件跳轉,具體請查看 組件之間調用和通訊 小節;

五、業務組件必須在本身的 build.gradle 中根據 isModule 值的不一樣改變本身的屬性,在組件模式下是:com.android.application,而在集成模式下com.android.library;同時還須要在build.gradle配置資源文件,如 指定不一樣開發模式下的AndroidManifest.xml文件路徑,排除debug文件夾等;業務組件還必須在dependencies中依賴Common組件,而且引入ActivityRouter的註解處理器annotationProcessor,以及依賴其餘用到的功能組件。

下面是一份普通業務組件的 build.gradle文件

if (isModule.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName } sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' //集成開發模式下排除debug文件夾中的全部Java文件 java { exclude 'debug/**' } } } } //設置了resourcePrefix值後,全部的資源名必須以指定的字符串作前綴,不然會報錯。 //可是resourcePrefix這個值只能限定xml裏面的資源,並不能限定圖片資源,全部圖片資源仍然須要手動去修改資源名。 //resourcePrefix "girls_" } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor" compile project(':lib_common') }

 

Main組件除了有業務組件的廣泛屬性外,還有一項重要功能

一、Main組件集成模式下的AndroidManifest.xml是跟其餘業務組件不同的,Main組件的表單中聲明瞭咱們整個Android應用的launch Activity,這就是Main組件的獨特之處;因此我建議SplashActivity、登錄Activity以及主界面都應屬於Main組件,也就是說Android應用啓動後要調用的頁面應置於Main組件。

<activity
            android:name=".splash.SplashActivity" android:launchMode="singleTop" android:screenOrientation="portrait" android:theme="@style/SplashTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>

 


五、組件化項目的混淆方案

組件化項目的Java代碼混淆方案採用在集成模式下集中在app殼工程中混淆,各個業務組件不配置混淆文件。集成開發模式下在app殼工程中build.gradle文件的release構建類型中開啓混淆屬性,其餘buildTypes配置方案跟普通項目保持一致,Java混淆配置文件也放置在app殼工程中,各個業務組件的混淆配置規則都應該在app殼工程中的混淆配置文件中添加和修改。

之因此不採用在每一個業務組件中開啓混淆的方案,是由於 組件在集成模式下都被 Gradle 構建成了 release 類型的arr包,一旦業務組件的代碼被混淆,而這時候代碼中又出現了bug,將很難根據日誌找出致使bug的緣由;另外每一個業務組件中都保留一份混淆配置文件很是不便於修改和管理,這也是不推薦在業務組件的 build.gradle 文件中配置 buildTypes (構建類型)的緣由。


六、工程的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 文件代碼以下:

buildscript {
    repositories {
        jcenter()
        mavenCentral()
    }

    dependencies {
        //classpath "com.android.tools.build:gradle:$localGradlePluginVersion" //$localGradlePluginVersion是gradle.properties中的數據 classpath "com.android.tools.build:gradle:$localGradlePluginVersion" } } allprojects { repositories { jcenter() mavenCentral() //Add the JitPack repository maven { url "https://jitpack.io" } //支持arr包 flatDir { dirs 'libs' } } } task clean(type: Delete) { delete rootProject.buildDir } // Define versions in a single place //時間:2017.2.13;每次修改版本號都要添加修改時間 ext { // Sdk and tools //localBuildToolsVersion是gradle.properties中的數據 buildToolsVersion = localBuildToolsVersion compileSdkVersion = 25 minSdkVersion = 16 targetSdkVersion = 25 versionCode = 1 versionName = "1.0" javaVersion = JavaVersion.VERSION_1_8 // App dependencies version supportLibraryVersion = "25.3.1" retrofitVersion = "2.1.0" glideVersion = "3.7.0" loggerVersion = "1.15" eventbusVersion = "3.0.0" gsonVersion = "2.8.0" photoViewVersion = "2.0.0" //需檢查升級版本 annotationProcessor = "1.1.7" routerVersion = "1.2.2" easyRecyclerVersion = "4.4.0" cookieVersion = "v1.0.1" toastyVersion = "1.1.3" } 

 

2)組件化工程的gradle.properties文件

在組件化實施流程中咱們瞭解到gradle.properties有兩個屬性對咱們很是有用:

一、在Android項目中的任何一個build.gradle文件中均可以把gradle.properties中的常量讀取出來,無論這個build.gradle是組件的仍是整個項目工程的build.gradle;

二、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 版本號不會被人爲改變。

具體的實施流程你們能夠查看個人這篇博文 AndroidStudio本地化配置gradle的buildToolsVersion和gradleBuildTools


七、組件化項目Router的其餘方案-ARouter

在組件化項目中使用到的跨組件跳轉庫ActivityRouter可使用阿里巴巴的開源路由項目:阿里巴巴ARouter

ActivityRouter和ARouter的接入組件化項目的方式是同樣的,ActivityRouter提供的功能目前ARouter也所有支持,可是ARouter還支持依賴注入解耦,頁面、攔截器、服務等組件均會自動註冊到框架。對於你們來講,沒有最好的只有最適合的,你們能夠根據本身的項目選擇合適的Router。

下面將介紹ARouter的基礎使用方法,更多功能還需你們去Github本身學習;

一、首先 ARouter 這個框架是須要初始化SDK的,因此你須要在「app殼工程」中的應用Application中加入下面的代碼,注意:在 debug 模式下必定要 openDebug

if (BuildConfig.DEBUG) { //必定要在ARouter.init以前調用openDebug ARouter.openDebug(); ARouter.openLog(); } ARouter.init(this);

 

二、首先咱們依然須要在 Common 組件中的 build.gradle 將ARouter 依賴進來,方便咱們在業務組件中調用:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) //router compile 'com.alibaba:arouter-api:1.2.1.1' }

 

三、而後在每個業務組件的 build.gradle 都引入ARouter 的 Annotation處理器,代碼以下:

android {
    defaultConfig {
    ...
    javaCompileOptions {
        annotationProcessorOptions {
        arguments = [ moduleName : project.getName() ]
        }
    }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor 'com.alibaba:arouter-compiler:1.0.3' }

 

四、因爲ARouter支持自動註冊到框架,因此咱們不用像ActivityRouter那樣在各個組件中聲明組件,固然更不須要在Application中管理組件了。 咱們給 Girls組件 中的 GirlsActivity 添加註解:@Route(path = 「/girls/list」),須要注意的是這裏的路徑至少須要有兩級,/xx/xx,之因此這樣是由於ARouter使用了路徑中第一段字符串(/*/)做爲分組,好比像上面的」girls」,而分組這個概念就有點相似於ActivityRouter中的組件聲明 @Module ,代碼以下:

@Route(path = "/girls/list") public class GirlsActivity extends BaseActionBarActivity { private GirlsView mView; private GirlsContract.Presenter mPresenter; @Override protected int setTitleId() { return R.string.girls_activity_title; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mView = new GirlsView(this); setContentView(mView); mPresenter = new GirlsPresenter(mView); mPresenter.start(); } }

 

而後咱們就能夠在項目中的任何一個地方經過 URL地址 : /girls/list, 調用 GirlsActivity,方法以下:

ARouter.getInstance().build("/girls/list").navigation();

 


八、結束語

組件化相比於單一工程優點是顯而易見的:

  1. 組件模式下能夠加快編譯速度,提升開發效率;
  2. 自由選擇開發框架(MVC /MVP / MVVM /);
  3. 方便作單元測試;
  4. 代碼架構更加清晰,下降項目的維護難度;
  5. 適合於團隊開發;

最後貼出Android組件化Demo地址:Android組件化項目AndroidModulePattern

想要學習更多Android組件化知識,請查看 :Android組件化之終極方案


感謝如下文章提供的幫助
1. http://www.cnblogs.com/chenxibobo/p/6187954.html
2. https://kymjs.com/code/2016/10/18/01/
3. https://zhuanlan.zhihu.com/p/23388989
4. https://zhuanlan.zhihu.com/p/23147164?refer=moduth

感謝如下開源項目
1. https://github.com/mzule/ActivityRouter
2. https://github.com/alibaba/ARouter

相關文章
相關標籤/搜索