(轉)Android組件化項目詳細實施方案

轉載請標明出處: http://blog.csdn.net/guiying712/article/details/55213884 ,本文出自:【張華洋的博客】

1、Android組件化項目
Android項目組件化之前,我們的項目都是像下圖那樣,一個單一工程下,根據不同的業務分幾個文件夾,把需要的第三方庫依賴下就開始開發了,這樣的代碼耦合嚴重,牽一發而動全身,刪除某處代碼就會到處報錯,如果不解決掉報錯的地方,就沒法編譯打包,而且這樣的代碼只適合于個人開發,尤其團隊開發合并代碼的時候那真是一個麻煩,相信大家都會深有體會,如果項目很大的話,修改一點簡單的頁面都要重新編譯,Android編譯速度大家也都見識過,每次打包都很耗時,并且這樣的代碼想做單元測試也是無從下手。

這里寫圖片描述

所以Android項目組件化就迫在眉睫了,組件化的方向就是由一個項目工程拆分成若干個模塊工程,由App主工程提供統一的入口,每個業務獨立的模塊共享項目的Common依賴庫。
這里寫圖片描述

2、Android組件化項目實施步驟
1)第一步:配置可自動將組件在Application和Library屬性之間切換的方法
我們都知道Android Studio中的Module主要有兩種屬性,分別為 :
application屬性,可以獨立運行的Android程序,也就是我們的APP;

apply plugin: ‘com.android.application’

library屬性,不可以獨立運行,一般是Android程序依賴的庫文件;

apply plugin: ‘com.android.library’

當我們在開發單獨組件的時候,這個組件應該處于application模式,而當我們要將單獨組件合并到主工程的時候,就需要將單獨組從application模式改為library模式,也許你可以每次切換的時候都去build.gradle文件中去修改,但是你的項目要是有十幾個組件的時候,你確定一個個去改?所以我們必須有一種能夠動態切換組件模式的方法,做到一次修改,全局組件生效,這個問題就需要通過配置Gradle來解決了。
在Android Studio項目的根目錄下有一個gradle.properties 文件,這個文件主要用來配置Gradle settings的,例如JVM參數等,想要了解這個文件的更多作用請查看http://www.gradle.org/docs/current/userguide/build_environment.html ,我們今天需要關注的是這個文件的一個特點:我們在gradle.properties 中配置的字段都可以在build.gradle文件中直接讀取出來,不用任何多余的代碼。
現在我們在gradle.properties添加了一行代碼,定義一個屬性isModule(是否是組件開發模式,true為是,false為否):
每次更改“isModule”的值后,需要點擊 "Sync Project" 按鈕isModule=true

然后我們在組件的build.gradle文件中讀出這行代碼:

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

因為gradle.properties中的數據類型都是String類型,而這里我們需要的是boolean值,所以這里要將String轉換為boolean值,如果是‘組件開發模式”就將這個組件應用為application模式,如果不是就將這個組件應用為library模式,也就是一個庫。 這樣我們的第一個問題就解決了,首先我們在gradle.properties中定義一個屬性isModule,然后在每個組件的build.gradle中把這個屬性讀取出來,每當我們需要從組件開發模式和APP整體開發模式轉換時,只需要修改“isModule”的值即可,當然注釋中也說了修改為這個屬性值后,要點擊AndroidStudio上的 “Sync Project”按鈕同步下整個項目才能生效。
2)第二步:解決組件AndroidManifest和主工程AndroidManifest合并的問題
每個組件是由不同的成員單獨開發的,這個時候組件就是一個獨立的APP,那么這個組件就會有自己的“AndroidManifest.xml”,但是Android程序只有一個“AndroidManifest.xml”,當我們要把組件作為Library合并到主工程的時候,組件的“AndroidManifest.xml”和主工程的“AndroidManifest.xml”就會產生沖突,因為他們都有自己實現application類以及一些屬性,還有自己的MAIN Activity,如果直接把張表合并到一起勢必產生沖突。
解決思路就是:每個組件維護兩張表,一張用于組件單獨開發時使用,另一張用于合并到主工程的注冊表中,每當增加一個Android系統的四大組件時都要同時給兩張表中添加。
我們在上一節講了可自動在組件的Application和Library屬性之間切換的方法,有了這種方法,維護兩張表就很方便了,首先在組件的main文件夾(和Java文件夾平級)下創建兩個文件夾,如下圖:

這里寫圖片描述

然后在每個組件的*build.gradle中添加如下的代碼:

sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                //release模式下排除debug文件夾中的所有Java文件
                java {
                    exclude 'debug/**'
                }
            }
        }
    }

這些代碼的意思是:當在組件開發模式下,組件的注冊表文件使用debug文件夾下的,其他情況使用release文件夾下的注冊表文件;那么這兩張表的區別在哪里呢?
下面的表示debug文件夾中的:

    <application android:name="debug.CarApplication"
        android:icon="@mipmap/ic_car_launcher"
        android:label="@string/car_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".query.QueryActivity" android:configChanges="orientation|screenSize|keyboard"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustPan|stateHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter> </activity>
        <activity android:name=".scan.ScanActivity" android:screenOrientation="portrait" />
    </application>

下面的表是release文件夾中的:

    <application android:theme="@style/AppTheme">
        <activity
            android:name=".query.QueryActivity"
            android:configChanges="orientation|screenSize|keyboard"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme"
            android:windowSoftInputMode="adjustPan|stateHidden" />
        <activity
            android:name=".scan.ScanActivity"
            android:screenOrientation="portrait" />
    </application>

debug文件夾中注冊表的標簽中指定了具體application類,而release文件夾中的則沒有,
debug文件夾中注冊表的標簽中添加一些application屬性,而release文件夾中的則什么都沒有添加;
debug文件夾中的注冊表指定QueryActivity為MAIN Activity,也就是要啟動的 Activity,而release文件夾中的則沒有;

3)第三步:解決組件和主工程的Application沖突問題以及組件單獨開發初始化(共享)數據問題
當android程序啟動時,android系統會為每個程序創建一個Application類的對象,并且只創建一個,application對象的生命周期是整個程序中最長的,它的生命周期就等于這個程序的生命周期。在默認情況下應用系統會自動生成Application 對象,但是如果我們自定義了Application,那就需要告知系統,實例化的時候,是實例化我們自定義的,而非默認的。但是我們在組件化開發的時候每一個組件可能都會有一個自己的Application類的對象,如果我們在自己的組件中開發時需要獲取全局的Context,一般都會直接獲取application對象,但是當所有組件要打包合并在一起的時候就會出現問題,因為最后程序只有一個Application,我們組件中自己定義的Application肯定沒法使用,總不能每次打包的時候都把全局的application改一遍吧?
解決思路:首先創建一個叫做Common的Library,這個Common庫中主要包含整個項目用到公共基類、工具類、自定義View等,例如BaseActivity、BaseFragment、BaseApplication等,并且我們的每一個組件都要依賴這個Common庫,現在主要講Common庫中的BaseApplication怎么定義,下面是BaseApplication中的部分代碼:

public class BaseApplication extends Application {
    private static BaseApplication sInstance;
    public static Context context;

    public static BaseApplication getIns() {
        return sInstance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
        context = this.getApplicationContext();
        if (isAppDebug(context)) { //只有debug模式才會打印日志 Logger.init("Demo").logLevel(LogLevel.FULL); 

        } else {
            Logger.init("Demo").logLevel(LogLevel.NONE);
        }
    }
}

因為每個組件都依賴了Common庫,所以每個組件都能夠獲取到BaseApplication.context,但是Android程序默認的是系統自己的Application這個類,要想使用自己的就要繼承Application并且在AndroidManifest.xml中聲明,因此我們先在自己的組件中創建一個組件Application并且繼承于BaseApplication,然后在debug文件中的AndroidManifest.xml中聲明:


public class CarApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        login();
    }
}

這樣我們就可以在組件中使用全局的Context:BaseApplication.context了,但是還有一個問題,我們在自己的組件中定義了CarApplication,那么組件合并到主工程后,主工程也有自己的Application,這樣又沖突了,其實這個問題第二節的代碼就已經寫出來了,我們只是在組件開發時才使用CarApplication,那么我們在合并到主工程的時候把這個代碼排除掉不就行了嘛,直接上圖:

這里寫圖片描述

我們在java文件夾下再建一個debug文件夾,把組件自己的application放在這個文件夾中,然后在build.gradle添加這行代碼:
這里寫圖片描述

這樣在合并到主項目時debug文件夾下的java文件就全部被排除了。并且你可以在組件的Application中做一些初始化的操作,比如登陸,然后把數據保存下來,供組件使用。
4)第四步:解決library重復依賴以及Sdk和依賴的第三方庫版本號控制問題
重復依賴問題其實在開發中經常會遇到,比如你 compile 了一個A,然后在這個庫里面又 compile 了一個B,然后你的工程中又 compile 了一個同樣的B,就依賴了兩次。 默認情況下,如果是 aar 依賴,gradle 會自動幫我們找出新版本的庫而拋棄舊版本的重復依賴。但是如果你使用的是 project 依賴,gradle 并不會去去重,最后打包就會出現代碼中有重復的類了。
Library重復依賴的解決辦法就是給整個工程提供統一的依賴第三方庫的入口,在上一節講解決Application沖突問題時我們建了一個Common庫,這個庫還有一個作用就是用來為整個項目提供統一的依賴第三方庫的入口,我們把項目常用或者必須用到的庫全部在Common庫的build.gradle中依賴進來,例如Android support Library、網絡庫、圖片加載庫等,又因為每個組件都要依賴這個Common庫,所以的build.gradle中就不在需要依賴任何其他庫了,這樣我們就有了統一的依賴第三方庫的入口,添加、刪除和升級庫文件都只需要在Common庫中去處理就好了。
下面是組件build.gradle的依賴配置:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':common')
}

當組件合并到主項目的時候,其實就是將組件打包成arr包,所以主工程中在組件開發模式下是還是要單獨依賴Common庫,等到合并的時候在去依賴其他組件,Common庫就不用依賴了,下面是主工程build.gradle的依賴配置:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    if (!isModule.toBoolean()) {
        compile project(':alert')
        compile project(':car')
    } else {
        compile project(':common')
    }
}

另外一個問題就是我們每個組件的build.gradle中都要配置一些屬性,例如compileSdkVersion、buildToolsVersion還有defaultConfig等,如果我們需要修改項目的compileSdkVersion版本號,那就麻煩了,那么多組的build.gradle,每個都要去找到修改一遍,想想都頭疼,所以我們要把這些build.gradle中都要配置的屬性統一起來,類似于java中的靜態常量,一處修改到處生效。首先我們在項目(不是組件的)build.gradle中定義如下代碼:


// Define versions in a single place
ext {
// Sdk and toolsbuildToolsVersion = local
    BuildToolsVersion
    compileSdkVersion = 23
    minSdkVersion = 16
    targetSdkVersion = 23
//時間:2017.2.13;每次修改版本號都要添加修改時間
    versionCode = 1
    versionName = "1.0"
    javaVersion = JavaVersion.VERSION_1_8
// App dependencies
    versionsupportLibraryVersion = "23.2.1"
    retrofitVersion = "2.1.0"
    glideVersion = "3.7.0"
    loggerVersion = "1.15"
    eventbusVersion = "3.0.0"
    gsonVersion = "2.8.0"
}

然后在組件build.gradle中引用這些值,下面貼出的是Common庫的build.gradle代碼會和組件的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
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
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.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
}

這樣我們修改compileSdkVersion、buildToolsVersion、defaultConfig的值或者依賴庫文件的版本號都可以直接在項目build.gradle文件中直接修改了,修改完后整個項目也就都改過來了。
5)第五步:跨Module跳轉問題,也是我們最重要的一步了
在組件化開發的時候,我們不能在使用顯示調用來跳轉頁面了,因為我們組件化的目的之一就是解決模塊間的強依賴問題,組件跟組件之間完全沒有任何依賴,假如現在我從A組件跳轉到B組件,并且要攜帶參數跳轉,這時候怎么辦呢?而且組件這么多怎么管理也是個問題,這時候就需要引入“路由”的概念了。
我在項目中使用了一個開源的“路由”庫,github地址請點擊:ActivityRouter,主頁里會有詳細的介紹,大家可以去了解一下。另外阿里巴巴也開源了一個組件路由,github地址請點擊:ARouter;這兩個都是現成拿來就能用的,當然有人可能比較好奇組件Router是什么原理,自己怎么開發,這里有一位作者寫出了詳細的教程,大家可以去學習下:Android路由實現
接下來我們就講怎么將路由應用到我們的組件化項目中,首先我們要在項目(不是組件的)build.gradle中依賴下面的代碼:

buildscript { dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }}

為什么要使用android-apt呢?大家可以看下面的解釋,或者自己去搜索:

這里寫圖片描述

然后在每個組件build.gradle中加入下面的代碼:

apply plugin: 'com.neenbedankt.android-apt'dependencies { 
compile 'com.github.mzule.activityrouter:activityrouter:1.2.2'
 apt 'com.github.mzule.activityrouter:compiler:1.1.7'
}

接下來是在主工程的AndroidManifest.xml配置

   <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="demo" /><!--改成自己的scheme-->
            </intent-filter>
        </activity>

接下來我們需要在每個組件的java目錄下,聲明這個組件,向下面的代碼那樣(聲明了兩個組件):

@Module("App")
public class AppModule {}
@Module("Car")
public class Car { }

然后在主工程的Application 中聲明需要添加到主工程中的所有組件:

@Modules({"App", "Car"})
public class DemoApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

到這里我們的組件和主工程之間的關系就建立起來了,組件的聲明以及添加和刪除就都已經解決了。接下來就是組件之間Activity的跳轉嗎,前面我們做了那么多都是在為Activity的跳轉做準備。
首先我們在需要跳轉的目標Activity上添加注解:

@Router("main")
public class MainActivity extends Activity { ...}

這樣就可以通過 demo://main來打開MainActivity了。
這一步就算講完了,至于Router更多進階功能就要靠大家自己去:ActivityRouter 學習了。
6)Module之間的通信問題
如果在B組件中要通知A組件刷新列表,就要想辦法解決組件間的通信問題,這個只要使用EventBus就能解決,并不是什么復雜問題。
7)資源名沖突問題
因為我們拆分出了很多組件,在合并到主工程的時候就有可能會出現資源名沖突問題,比如A組件和B組件都定義了同一個資源名。這個問題一般很很好解決,我們只需要在組件的build.gradle中添加這樣的代碼:

resourcePrefix "組件名_"

但是設置了這個屬性后有個問題,所有的資源名必須以指定的字符串做前綴,否則會報錯,而且resourcePrefix這個值只能限定xml里面的資源,并不能限定圖片資源,所有圖片資源仍然需要手動去修改資源名。所以我并不推薦使用這種方法來解決資源名沖突,我們項目中解決辦法是增加資源命名規約,只要遵守這個命名規約就能規避資源名沖突問題。
3、Android組件化項目結語
到這里一個簡單的組件化項目就搭建出來了,組件化相比于單一工程優勢是顯而易見的: 1. 加快編譯速度,提高開發效率 2. 自由選擇開發框架(MVC /MVP / MVVM /) 3. 方便做單元測試 4. 代碼架構更加清晰,降低項目的維護難度 5. 適合于團隊開發
最后貼出Android組件化Demo地址:請用鼠標猛擊這里
參考文章:

  1. http://www.cnblogs.com/chenxibobo/p/6187954.html
  2. https://kymjs.com/code/2016/10/18/01/
  3. http://www.open-open.com/lib/view/open1481772233714.html
  4. https://zhuanlan.zhihu.com/p/23388989
  5. https://zhuanlan.zhihu.com/p/23147164?refer=moduth

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,603評論 25 707
  • 1. Android組件化開發 在Android項目組件化之前,我們的項目都是像下圖那樣,一個單一工程下,根據不同...
    CHSmile閱讀 4,552評論 1 34
  • 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網絡層、UI層、通信層或其他單一功能的框架 混合開發框架:...
    偉子男閱讀 5,250評論 0 161
  • 我寫盡年華,終換不回一個童年 在霓虹滿街,人情漸漸冷淡的當今,我想聊聊關于我的童年。 小時候,我住在浦東新區一個...
    十九號小仙女閱讀 229評論 2 2
  • 有一個巨商,為躲避動蕩,把所有的家財置換成金銀票,特制了一把油紙傘,將金銀票小心地藏進傘柄之內,然后把自己裝扮成普...
    遂心天涯閱讀 203評論 0 0