1. 程式人生 > >Android元件化專案詳細實施方案

Android元件化專案詳細實施方案

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>
  1. debug資料夾中登錄檔的標籤中指定了具體application類,而release資料夾中的則沒有,
  2. debug資料夾中登錄檔的標籤中新增一些application屬性,而release資料夾中的則什麼都沒有新增;
  3. 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 tools
buildToolsVersion = localBuildToolsVersion
compileSdkVersion = 23
minSdkVersion = 16
targetSdkVersion = 23
//時間:2017.2.13;每次修改版本號都要新增修改時間
versionCode = 1
versionName = "1.0"
javaVersion = JavaVersion.VERSION_1_8

// App dependencies version
supportLibraryVersion = "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地址:請用滑鼠猛擊這裡