專案實戰之元件化架構

小劉哥可愛多發表於2018-05-23

前言

關於什麼是元件化、為什麼要進行元件化以及實施元件化的基本流程網上一搜一大把,這裡不做過多說明,不瞭解的話可以Google一下。這裡主要記錄一下元件化開發的一些心得和踩的一些坑。

先看一下專案結構圖

專案實戰之元件化架構
結構很簡單,有一個公共的基礎module類commonlibrary來處理一些公共的東西,比如第三方庫的依賴,基類封裝,工具類等。中間層是各個獨立的業務模組,各個模組之間互相隔離。最下面是app的殼,主要配置簽名打包什麼的。具體可以看一下demo

元件化實施步驟

1、設定module是否作為元件的開關

在gradle.properties檔案裡定義一個常量IsBuildApp = false,表示是否把元件module作為單獨的app執行。定義好了這個常量後,在專案的任何一個gradle檔案裡都可以讀取到這個值,那麼就用這個值來作為module元件是否需要單獨執行的開關。

// 在module元件的gradle裡配置如下,gradle.properties 中的資料型別都是String型別,這裡需要做一下轉換
if (IsBuildApp.toBoolean()){
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}
複製程式碼
2、元件module的清單檔案AndroidManifest合併問題

我們知道android的四大元件、許可權等都是需要註冊的,當module單獨執行的時候,肯定需要一個清單檔案註冊元件和申請許可權,但是當module作為app的一個子元件存在的時候,清單檔案是要合併到app的殼工程中的,這個時候如果每個module都有自己的啟動頁面和自定義application的話,就會引起衝突。

為了解決這個問題,那就需要根據module是否需要單獨執行來配置不同的清單檔案。在java同級目錄新建independent目錄,在此目錄下建立專案module需要單獨執行的清單檔案和application。然後在module的gradle檔案裡指定清單檔案路徑,程式碼如下:

// 在android領域裡指定清單檔案的路徑

sourceSets {
    main {
        if (IsBuildApp.toBoolean()) {
            // 單獨作為app執行的清單檔案,這裡可以新增啟動頁面、自定義application等。
            manifest.srcFile 'src/main/independent/AndroidManifest.xml'
        } else {
            // 作為元件的清單檔案
            manifest.srcFile 'src/main/AndroidManifest.xml'
            //release模式下排除independent資料夾中的所有Java檔案
            java {
                exclude 'independent/**'
            }
        }
    }
}
複製程式碼

這樣配置完成以後,作為元件的清單檔案是不能有自己的啟動頁面、application、appname等屬性的,下面看一下完整的配置:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.article.demos.vue">

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

        <activity android:name=".ui.VueActivity" />

    </application>
</manifest>
複製程式碼

下面看一下獨立執行模式下的清單檔案:

// 作為獨立app執行的清單檔案,注意這裡我設定了主題,不然的話會報錯。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.article.demos.main">

    <application android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
複製程式碼

獨立執行的話,就和正常的app清單檔案一樣,要有啟動頁面,application標籤可以新增label、icon、自定義application等,就不多說啦。

3、全域性Application的問題

在commonlibrary中建立自定義application,因為其他的module都依賴這個module,所以其他的module都可以獲取到這個全域性的application。另外,元件在獨立執行模式下的application,繼承我們自定義這個BaseApplication就可以了。因為我們在release模式下,排除了所有independent資料夾下的java檔案,所以作為元件執行時,並不會產生application的衝突,配置如下:

sourceSets {
    main {
        if (IsBuildApp.toBoolean()) {
            manifest.srcFile 'src/main/independent/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            //release模式下排除independent資料夾中的所有Java檔案
            java {
                exclude 'independent/**'
            }
        }
    }
}
複製程式碼
4、重複依賴三方庫的問題

為了避免重複依賴三方庫的問題,我們的三方庫依賴統一放在commonlibrary的module中,這樣既可以避免重複依賴,又方便管理。然後我們在app的module裡,如下引用即可:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    if (IsBuildApp.toBoolean()) {
        implementation project(':commonlibrary')
    } else {
        implementation project(':androidmodule')
        implementation project(':vuemodule')
        implementation project(':kotlinmodule')
        implementation project(':javamodule')
    }
}
複製程式碼
5、資源衝突問題

資源衝突主要是指各個module裡的資原始檔名衝突的問題,如果命名一樣,合併的時候便會產生衝突。

解決衝突主要有兩個解決方案,一個是約定規則,比如資源名約定都以module名開頭。

方案二是通過gradle指令碼來設定,在各個元件的gradle檔案裡新增如下程式碼:

resourcePrefix "module名稱_"
複製程式碼

但是這種配置有限制,比如只能限定xml裡的資源,所以並不推薦這種方式。

6、元件間跳轉

因為元件是相互隔離的,我們並不能顯式跳轉,這裡我們選用阿里巴巴的Arouter路由跳轉,專案的地址github.com/alibaba/ARo…

這裡需要特別說明一下,需要跳轉的目標module需要引入arouter的註解處理器,否則無法處理router註解會出現路徑不匹配的問題:

annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
複製程式碼

同時,改module的defaultconfig裡也別忘記配置moduleName

javaCompileOptions {
        annotationProcessorOptions {
            arguments = [ moduleName : project.getName() ]
        }
    }
複製程式碼
7、跨module互動

跨moduel互動一般是指module間通訊和module間的相互呼叫。module間通訊這裡選用eventbus,很簡單,就不過多說明了。

下面說一下同級module直接的通訊,比如我在任何一個頁面要呼叫loginModule裡的微信登入方法,因為各個module是互相獨立的,互不依賴,想要直接呼叫基本不可能。目前網上發現有兩種解決方案,一個是寫一個反射工具類,通過反射獲取到要呼叫的類,然後呼叫相應的方法。另一個是通過commonModule做一下橋接,瞭解更多可以參考這裡。不過感覺用Arouter能更優雅的實現,下面具體講一下利用arouter來實現。

首先,在公共module裡建立一個介面IService

public interface IService extends IProvider{
    String wxLogin();
}
複製程式碼

介面裡定義一個微信登入的虛擬碼,然後在我們的登入元件裡,實現該介面並新增route註解

@Route(path = Constant.WX_LOGIN)
public class WxTest implements IService{

    @Override
    public void init(Context context) {

    }

    @Override
    public String wxLogin() {

        return "wxlogin";
    }
}
複製程式碼

其中 Constant.WX_LOGIN是我定義的一個字串常量

public static final String WX_LOGIN = "/wx/login";
複製程式碼

以上兩步就把工作做完了,下面只需要在需要呼叫的頁面呼叫登入就行了。首先,我們獲取到IService

/**
     * 推薦使用方式二來獲取IService
     */
    // IService iService = (IService) ARouter.getInstance().build(Constant.WX_LOGIN).navigation();
    IService iService = ARouter.getInstance().navigation(IService.class);
複製程式碼

拿到IService後,就可以放心大膽的呼叫登入方法就行了。

mBinding.btLogin.setOnClickListener(v -> {
        String s = iService.wxLogin();
        Toast.makeText(getContext(), s, Toast.LENGTH_SHORT).show();
    });
複製程式碼
8、fragment的元件化

一般的專案首頁都是一個activity和多個fragment組成。由於元件間的隔離,我們在首頁裡怎麼獲取到其他元件裡的fragment呢?開篇的兩個參考文章分別使用了兩種不同的方式,有興趣的朋友可以看看。各有利弊吧,一個是查詢所有,太耗時。一個是直接反射獲取,但是好像有點違背元件隔離,需要知道fragment的全路徑。

這裡我參考了《Android元件化架構》一書,使用arouter來獲取。其實三種方式獲取的原理一樣,都是通過反射。我們看一下arouter的註解的原始碼就知道:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {……}
複製程式碼

可以看到Route註解的retention是CLASS,也是通過反射來獲取。

9、遇到的一些坑

(1)使用dataBinding的話,每個module的gradle檔案裡都要加上dataBinding的支援,否則無法生成相應的binding類

// 每個module都加上dataBinding的支援,否則無法生成相應的binding類
    dataBinding {
        enabled = true
    }
複製程式碼

(2)java8的支援一樣要每個module都要單獨配置

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
複製程式碼

(3)升級到as 3.1.2後,出現無法訪問TaskStackBuilder的問題

檢查一下你的support包,將你的support包更新到27或以上即可。

(4)如果使用有自定義註解annotation的話,如果編譯報錯 Annotation processors must be explicitly declared now...,那麼在commonlibrary的gradle檔案的defaultConfig裡新增如下程式碼:

// Annotation processors must be explicitly declared now
    javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
複製程式碼

(5)如果你元件化開發,子module中無法使用butterknife的話,網上自行搜解決方案吧(?‍♀️)

關於為何出現這個問題,推薦一篇博文R.java、R2.java是時候懂了

(6)其他問題本篇部落格會持續更新……

2018年6.15更新………………………………………………………… 編譯報錯

 Multiple dex files define Lcom/alibaba/android/arouter/routes/ARouter$$Group$$module
複製程式碼

一般網上說是依賴版本衝突,其實這個問題是不同module之間有相同分組導致的問題,比如a模組 path = "/message/a",b模組 path = "/message/b",有相同的message分組,修改成不一樣的就可以了。

2018.8.20更新…………………………………………………………

最近在用kotlin和java混合開發,發現原有java頁面跳轉新寫的kotlin頁面 arouter 頁面跳轉的時候報異常提示 There is no route match the path……,此時參考官方文件即可解決,

// 在kotlin的module中新增外掛
apply plugin: 'kotlin-kapt'
// 依賴裡 使用kapt 引用
dependencies {
    compile 'com.alibaba:arouter-api:x.x.x'
    kapt 'com.alibaba:arouter-compiler:x.x.x'
    ...
}
複製程式碼
2018.8.22更新…………………………………………………………

遇到了一個很蛋疼的問題,在純java寫的module裡通過arouter跳轉到另一個module裡的kotlin頁面的時候,發現setcontentview方法無效,頁面什麼都不顯示。除錯了半天,發現是頁面的xml佈局檔案和一個空的xml佈局檔案重名了,導致kotlin頁面載入了空頁面的佈局,在此記錄一下,好尷尬。

最後附上完整的demo地址,如果對你有幫助麻煩start鼓勵一下,你的鼓勵是我前進的動力。

相關文章