“終於懂了” 系列:Android元件化,全面掌握!

idaretobe發表於2020-10-21

“終於懂了” 系列:Android元件化,全面掌握!

技術最TOP 昨天

以下文章來源於胡飛洋 ,作者胡飛洋

一、背景

隨著專案逐漸擴充套件,業務功能越來越多,程式碼量越來越多,開發人員數量也越來越多。此過程中,你是否有過以下煩惱?

  1. 專案模組多且複雜,編譯一次要5分鐘甚至10分鐘?太慢不能忍?

  2. 改了一行程式碼 或只調了一點UI,就要run整個專案,再忍受一次10分鐘?

  3. 合程式碼經常發生衝突?很煩?

  4. 被人偷偷改了自己模組的程式碼?很不爽?

  5. 做一個需求,發現還要去改動很多別人模組的程式碼?

  6. 別的模組已實現的類似功能,自己要用只能去複製一份程式碼再改改?

  7. “這個不是我負責的,我不管”,程式碼責任範圍不明確?

  8. 只做了一個模組的功能,但改動點很多,所以要完整迴歸測試?

  9. 做了個需求,但不知不覺導致其他模組出現bug?

如果有這些煩惱,說明你的專案需要進行 元件化 了。

上半年,我所在專案進行了大重構,也完成了元件化改造。所以終於學習實踐了這樣一個“高階知識”,也看了一些文章,於是就有了這篇文章來作為總結和分享~

二、元件化的理解

2.1 模組化

在介紹元件化之前,先說說模組化。我們知道在Android Studio中,新建工程預設有一個App module,然後還可以通過File->New->New Module 新建module。那麼這裡的“module” 實際和我們說的“模組”基本是一個概念了。也就是說,原本一個 App模組 承載了所有的功能,而模組化就是拆分成多個模組放在不同的Module裡面,每個功能的程式碼都在自己所屬的 module 中新增。

已京東為例,大致可以分為 “首頁”、“分類”、“發現”、“購物車”、“我的”、“商品詳情” 六個模組。

專案結構如下:

這是一般專案都會採用的結構。另外通常還會有一個通用基礎模組module_common,提供BaseActivity/BaseFragment、圖片載入、網路請求等基礎能力,然後每個業務模組都會依賴這個基礎模組。那麼業務模組之間有沒有依賴呢?很顯然是有的。例如  “首頁”、“分類”、“發現”、“購物車”、“我的”,都是需要跳轉到“商品詳情” 的,必然是依賴“商品詳情” ;而“商品詳情”是需要能新增到“購物車”能力的;而“首頁”點選搜尋顯然是“分類”中的搜尋功能。所以這些模組之間存在複雜的依賴關係

模組化 在各個業務功能比較獨立的情況下是比較合理的,但多個模組中肯定會有頁面跳轉資料傳遞方法呼叫 等情況,所以必然存在以上這種依賴關係,即模組間有著高耦合度。高耦合度 加上 程式碼量大,就極易出現上面提到的那些問題了,嚴重影響了團隊的開發效率及質量。

為了 解決模組間的高耦合度問題,就要進行元件化了。

2.2 元件化介紹 — 優勢及架構

元件化去除模組間的耦合,使得每個業務模組可以獨立當做App存在,對於其他模組沒有直接的依賴關係。 此時業務模組就成為了業務元件

而除了業務元件,還有抽離出來的業務基礎元件,是提供給業務元件使用,但不是獨立的業務,例如分享元件、廣告元件;還有基礎元件,即單獨的基礎功能,與業務無關,例如 圖片載入、網路請求等。這些後面會詳細說明。

元件化帶來的好處 就顯而易見了:

  1. 加快編譯速度:每個業務功能都是一個單獨的工程,可獨立編譯執行,拆分後程式碼量較少,編譯自然變快。

  2. 提高協作效率:解耦 使得元件之間 彼此互不打擾,元件內部程式碼相關性極高。團隊中每個人有自己的責任元件,不會影響其他元件;降低團隊成員熟悉專案的成本,只需熟悉責任元件即可;對測試來說,只需重點測試改動的元件,而不是全盤迴歸測試。

  3. 功能重用:元件 類似我們引用的第三方庫,只需維護好每個元件,一建引用整合即可。業務元件可上可下,靈活多變;而基礎元件,為新業務隨時整合提供了基礎,減少重複開發和維護工作量。

下圖是我們期望的元件化架構

元件化架構

  1. 元件依賴關係是上層依賴下層,修改頻率是上層高於下層。

  2. 基礎元件是通用基礎能力,修改頻率極低,作為SDK可共公司所有專案整合使用。

  3. common元件,作為支撐業務元件、業務基礎元件的基礎(BaseActivity/BaseFragment等基礎能力),同時依賴所有的基礎元件,提供多數業務元件需要的基本功能,並且統一了基礎元件的版本號。所以 業務元件、業務基礎元件 所需的基礎能力只需要依賴common元件即可獲得。

  4. 業務元件業務基礎元件,都依賴common元件。但業務元件之間不存在依賴關係,業務基礎元件之間不存在依賴關係。而 業務元件 是依賴所需的業務基礎元件的,例如幾乎所有業務元件都會依賴廣告元件 來展示Banner廣告、彈窗廣告等。

  5. 最上層則是主工程,即所謂的“殼工程”,主要是整合所有的業務元件、提供Application唯一實現、gradle、manifest配置,整合成完備的App。

2.3 元件化開發的問題點

我們瞭解了元件化的概念、優點及架構特點,那麼要想實施元件化,首先要搞清楚 要解決問題點有哪些?

核心問題是 業務元件去耦合。那麼存在哪些耦合的情況呢?前面有提到過,頁面跳轉、方法呼叫、事件通知。而基礎元件、業務基礎元件,不存在耦合的問題,所以只需要抽離封裝成庫即可。所以針對業務元件有以下問題:

  1. 業務元件,如何實現單獨執行除錯?

  2. 業務元件間 沒有依賴,如何實現頁面的跳轉?

  3. 業務元件間 沒有依賴,如何實現元件間通訊/方法呼叫?

  4. 業務元件間 沒有依賴,如何獲取fragment例項?

  5. 業務元件不能反向依賴殼工程,如何獲取Application例項、如何獲取Application onCreate()回撥(用於任務初始化)?

下面就來看看如何解決這些問題。

三、元件獨立除錯

每個 業務元件 都是一個完整的整體,可以當做獨立的App,需要滿足單獨執行及除錯的要求,這樣可以提升編譯速度提高效率。

如何做到元件獨立除錯呢?有兩種方案:

  1. 單工程方案,元件以module形式存在,動態配置元件的工程型別;

  2. 多工程方案,業務元件以library module形式存在於獨立的工程,且只有這一個library module。

3.1 單工程方案

3.1.1 動態配置元件工程型別

單工程模式,整個專案只有一個工程,它包含:App module 加上各個業務元件module,就是所有的程式碼,這就是單工程模式。如何做到元件單獨除錯呢?

我們知道,在 AndroidStudio 開發 Android 專案時,使用的是 Gradle 來構建,Android Gradle 中提供了三種外掛,在開發中可以通過配置不同的外掛來配置不同的module型別。

  • Application 外掛,id: com.android.application

  • Library 外掛,id: com.android.library

區別比較簡單, App 外掛來配置一個 Android App 工程,專案構建後輸出一個 APK 安裝包,Library 外掛來配置一個 Android Library 工程,構建後輸出 ARR 包。

顯然我們的 App module配置的就是Application 外掛,業務元件module 配置的是 Library 外掛。想要實現 業務元件的獨立除錯,這就需要把配置改為 Application 外掛;而獨立開發除錯完成後,又需要變回Library 外掛進行整合除錯

如何讓元件在這兩種除錯模式之間自動轉換呢?手動修改元件的 gralde 檔案,切換 Application 和 library ?如果專案只有兩三個元件那麼是可行的,但在大型專案中可能會有十幾個業務元件,一個個手動修改顯得費力笨拙。

我們知道用AndroidStudio建立一個Android專案後,會在根目錄中生成一個gradle.properties檔案。在這個檔案定義的常量,可以被任何一個build.gradle讀取。所以我們可以在gradle.properties中定義一個常量值 isModule,true為即獨立除錯;false為整合除錯。然後在業務元件的build.gradle中讀取 isModule,設定成對應的外掛即可。程式碼如下:

//gradle.properties
#元件獨立除錯開關, 每次更改值後要同步工程
isModule = false
//build.gradle
//注意gradle.properties中的資料型別都是String型別,使用其他資料型別需要自行轉換
if (isModule.toBoolean()){
    apply plugin: 'com.android.application'
}else {
    apply plugin: 'com.android.library'
}

3.1.2 動態配置ApplicationId 和 AndroidManifest

我們知道一個 App 是需要一個 ApplicationId的 ,而元件在獨立除錯時也是一個App,所以也需要一個 ApplicationId,整合除錯時元件是不需要ApplicationId的;另外一個 APP 也只有一個啟動頁, 而元件在獨立除錯時也需要一個啟動頁,在整合除錯時就不需要了。所以ApplicationId、AndroidManifest也是需要 isModule 來進行配置的。

//build.gradle (module_cart)
android {
...
    defaultConfig {
...
        if (isModule.toBoolean()) {
            // 獨立除錯時新增 applicationId ,整合除錯時移除
            applicationId "com.hfy.componentlearning.cart"
        }
...
    }

    sourceSets {
        main {
            // 獨立除錯與整合除錯時使用不同的 AndroidManifest.xml 檔案
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/moduleManifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
...
}

可見也是使用isModule分別設定applicationId、AndroidManifest。其中獨立除錯的AndroidManifest是新建於目錄moduleManifest,使用 manifest.srcFile 即可指定兩種除錯模式的AndroidManifest檔案路徑。moduleManifest中新建的manifest檔案 指定了Application、啟動activity:

//moduleManifest/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart" >
    <application android:name=".CartApplication"
        android:allowBackup="true"
        android:label="Cart"
        android:theme="@style/Theme.AppCompat">
        <activity android:name=".CartActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

原本自動生成的manifest,未指定Application、啟動activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_cart">
    <application>
        <activity android:name=".CartActivity"></activity>
    </application>
</manifest>

獨立除錯、整合除錯 ,分別使用“assembleDebug”構建結果如下:

3.2 多工程方案

3.2.1 方案概述

多工程方案,業務元件以library module形式存在於獨立的工程。獨立工程 自然就可以獨立除錯了,不再需要進行上面那些配置了。

例如,購物車元件 就是 新建的工程Cart 的 module_cart模組,業務程式碼就寫在module_cart中即可。app模組是依賴module_cart。app模組只是一個元件的入口,或者是一些demo測試程式碼。

那麼當所有業務元件都拆分成獨立元件時,原本的工程就變成一個只有app模組的殼工程了,殼工程就是用來整合所有業務元件的。

3.2.1 maven引用元件

那麼如何進行整合除錯呢?使用maven引用元件:1、釋出元件的arr包 到公司的maven倉庫,2、然後在殼工程中就使用implemention依賴就可以了,和使用第三方庫一毛一樣。另外arr包 分為 快照版本(SNAPSHOT) 和 正(Realease)式版本,快照版本是開發階段除錯使用,正式版本是正式發版使用。具體如下:

首先,在module_cart模組中新建maven_push.gradle檔案,和build.gradle同級目錄

apply plugin: 'maven'

configurations {
    deployerJars
}

repositories {
    mavenCentral()
}

//上傳到Maven倉庫的task
uploadArchives {
    repositories {
        mavenDeployer {
            pom.version = '1.0.0' // 版本號
            pom.artifactId = 'cart' // 專案名稱(通常為類庫模組名稱,也可以任意)
            pom.groupId = 'com.hfy.cart' // 唯一標識(通常為模組包名,也可以任意)

            //指定快照版本 maven倉庫url, todo 請改為自己的maven伺服器地址、賬號密碼
            snapshotRepository(url: 'http://xxx/maven-snapshots/') {
                authentication(userName: '***', password: '***')
            }
            //指定正式版本 maven倉庫url, todo 請改為自己的maven伺服器地址、賬號密碼
            repository(url: 'http://xxx/maven-releases/') {
                authentication(userName: '***', password: '***')
            }
        }
    }
}

// type顯示指定任務型別或任務, 這裡指定要執行Javadoc這個task,這個task在gradle中已經定義
task androidJavadocs(type: Javadoc) {
    // 設定原始碼所在的位置
    source = android.sourceSets.main.java.sourceFiles
}

// 生成javadoc.jar
task androidJavadocsJar(type: Jar) {
    // 指定文件名稱
    classifier = 'javadoc'
    from androidJavadocs.destinationDir
}

// 打包main目錄下程式碼和資源的task,生成sources.jar
task androidSourcesJar(type: Jar) {
    classifier = 'sources'
    from android.sourceSets.main.java.sourceFiles
}

//配置需要上傳到maven倉庫的檔案
artifacts {
    archives androidSourcesJar
    archives androidJavadocsJar
}

maven_push.gradle主要就是釋出元件ARR的配置:ARR的版本號、名稱、maven倉地址賬號等。

然後,再build.gradle中引用:

//build.gradle
apply from: 'maven_push.gradle'

接著,點選Sync後,點選Gradle任務uploadArchives,即可打包併發布arr到maven倉。

最後,殼工程要引用元件ARR,需要先在殼工程的根目錄下build.gradle中新增maven倉庫地址:

allprojects {
    repositories {
        google()
        jcenter()
        //私有伺服器倉庫地址
        maven {
            url 'http://xxx'
        }
    }
}

接著在app的build.gradle中新增依賴即可:

dependencies {
    ...
    implementation 'com.hfy.cart:cart:1.0.0'
    //以及其他業務元件
}

可見,多工程方案 和我們平時使用第三方庫是一樣的,只是我們把元件ARR釋出到公司的私有maven倉而已。

實際上,我個人比較建議 使用多工程方案的。

  • 單工程方案沒法做到程式碼許可權管控,也不能做到開發人員職責劃分明確,每個開發人員都可以對任意的元件進行修改,顯然還是會造成混亂。

  • 多工程把每個元件都分割成單獨的工程,程式碼許可權可以明確管控。整合測試時,通過maven引用來整合即可。並且業務元件和業務基礎元件也可以 和 基礎元件一樣,可以給公司其他專案複用。

注意,我在Demo裡 使用的是多工程方案,並且是 把ARR發到JitPack倉,這樣是為了演示方便,和發到公司私有maven倉是一個意思。

1、需要根目錄下build.gradle中新增JitPack倉地址:maven { url 'https://jitpack.io' } 

2、JitPack是自定義的Maven倉庫,不過它的流程極度簡化,只需要輸入Github專案地址就可釋出專案。

四、頁面跳轉

4.1 方案—ARouter

前面說到,元件化的核心就是解耦,所以元件間是不能有依賴的,那麼如何實現元件間的頁面跳轉呢?

例如 在首頁模組 點選 購物車按鈕 需要跳轉到 購物車模組的購物車頁面,兩個模組之間沒有依賴,也就說不能直接使用 顯示啟動 來開啟購物車Activity,那麼隱式啟動呢?隱式啟動是可以實現跳轉的,但是隱式 Intent 需要通過 AndroidManifest 配置和管理,協作開發顯得比較麻煩。這裡我們採用業界通用的方式—路由

比較著名的路由框架 有阿里的ARouter、美團的WMRouter,它們原理基本是一致的。

這裡我們採用使用更廣泛的ARouter:“一個用於幫助 Android App 進行元件化改造的框架 —— 支援模組間的路由、通訊、解耦”。

4.2 ARouter實現路由跳轉

前面提到,所有的業務元件都依賴了 Common 元件,所以我們在 Common 元件中使用關鍵字**“api”**新增的依賴,業務元件都能訪問。我們要使用 ARouter 進行介面跳轉,需要Common 元件新增 Arouter 的依賴(另外,其它元件共同依賴的庫也要都放到 Common 中統一依賴)。

4.2.1 引入依賴

因為ARouter比較特殊,“arouter-compiler ” 的annotationProcessor依賴 需要所有使用到 ARouter 的元件中都單獨新增,不然無法在 apt 中生成索引檔案,就無法跳轉成功。並且在每個使用到 ARouter 的元件的 build.gradle 檔案中,其 android{} 中的 javaCompileOptions 中也需要新增特定配置。然後殼工程需要依賴業務元件。如下所示:

//common元件的build.gradle
dependencies {
    ...
    api 'com.alibaba:arouter-api:1.4.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    //業務元件、業務基礎元件 共同依賴的庫(網路庫、圖片庫等)都寫在這裡~
}
//業務元件的build.gradle
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
...
}
dependencies {
...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'//業務元件依賴common元件
}
//殼工程app module的build.gradle
dependencies {
    ...
    //這裡沒有使用私有maven倉,而是發到JitPack倉,一樣的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang:Cart:1.0.1' //依賴購物車元件
    implementation 'com.github.hufeiyang:HomePage:1.0.2' //依賴首頁元件

    //殼工程內 也需要依賴Common元件,因為需要初始化ARouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}

4.2.2 初始化

依賴完了,先要對ARouter初始化,需要在Application內完成:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
        if (BuildConfig.DEBUG) {
            // 列印日誌
            ARouter.openLog();
            // 開啟除錯模式(如果在InstantRun模式下執行,必須開啟除錯模式!線上版本需要關閉,否則有安全風險)
            ARouter.openDebug();
        }
        // 儘可能早,推薦在Application中初始化
        ARouter.init(this);
    }
}

4.2.3 路由跳轉

好了,準備工作都完成了。並且知道 首頁元件是沒有依賴購物車元件的,下面就來實現前面提到的 首頁元件 無依賴 跳轉到 購物車元件頁面

而使用ARouter進行簡單路由跳轉,只有兩步:新增註解路徑、通過路徑路由跳轉。

1、在支援路由的頁面上新增註解@Route(path = "/xx/xx"),路徑需要注意的是至少需要有兩級,/xx/xx。這裡就是購物車元件的CartActivity:

@Route(path = "/cart/cartActivity")
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}

2、然後在首頁元件的HomeActivity 發起路由操作—點選按鈕跳轉到購物車,呼叫ARouter.getInstance().build("/xx/xx").navigation()即可:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通過路由跳轉到 購物車元件的購物車頁面(但沒有依賴購物車元件)
                ARouter.getInstance()
                        .build("/cart/cartActivity")
                        .withString("key1","value1")//攜帶引數1
                        .withString("key2","value2")//攜帶引數2
                        .navigation();
            }
        });
    }
}

另外,注意在HomeActivity上新增了註解和路徑,這是為了殼工程的啟動頁中直接開啟首頁。還看到路由跳轉可以像startActivity一樣待引數。

最後,殼工程的啟動頁中 通過路由開啟首頁(當然這裡也可以用startActivity(),畢竟殼工程依賴了首頁元件):

//啟動頁
public class SplashActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //通過路由直接開啟home元件的HomeActivity,
        ARouter.getInstance().build("/homepage/homeActivity").navigation();
        finish();
    }
}

我們run殼工程 最後看下效果:到這裡,元件間頁面跳轉的問題也解決了。

五、元件間通訊

元件間沒有依賴,又如何進行通訊呢?

例如,首頁需要展示購物車中商品的數量,而查詢購物車中商品數量 這個能力是購物車元件內部的,這咋辦呢?

5.1 服務暴露元件

平時開發中 我們常用 介面 進行解耦,對介面的實現不用關心,避免介面呼叫與業務邏輯實現緊密關聯。這裡元件間的解耦也是相同的思路,僅依賴和呼叫服務介面,不會依賴介面的實現。

可能你會有疑問了:既然首頁元件可以訪問購物車元件介面了,那就需要依賴購物車元件啊,這倆元件還是耦合了啊,那咋辦啊?答案是元件拆分出可暴露服務。見下圖:左側是元件間可以呼叫對方服務 但是有依賴耦合。右側,發現多了export_homeexport_cart,這是對應拆分出來的專門用於提供服務的暴露元件。操作說明如下:

  • 暴露元件 只存放 服務介面、服務介面相關的實體類、路由資訊、便於服務呼叫的util等

  • 服務呼叫方 只依賴 服務提供方的 露元件,如module_home依賴export_cart,而不依賴module_cart

  • 元件 需要依賴 自己的暴露元件,並實現服務介面,如module_cart依賴export_cart 並實現其中的服務介面

  • 介面的實現注入 依然是由ARouter完成,和頁面跳轉一樣使用路由資訊

下面按照此方案 來實施 首頁呼叫購物車服務 來獲取商品數量,更好地說明和理解。

5.2 實施

5.2.1 新建export_cart

首先,在購物車工程中新建module即export_cart,在其中新建介面類ICartService並定義獲取購物車商品數量方法,注意介面必須繼承IProvider,是為了使用ARouter的實現注入:

/**
 * 購物車元件對外暴露的服務
 * 必須繼承IProvider
 * @author hufeiyang
 */
public interface ICartService extends IProvider {

    /**
     * 獲取購物車中商品數量
     * @return
     */
    CartInfo getProductCountInCart();
}

CartInfo是購物車資訊,包含商品數量:

/**
 * 購物車資訊

 * @author hufeiyang
 */
public class CartInfo {

    /**
     * 商品數量
     */
    public int productCount;
}

接著,建立路由表資訊,存放購物車元件對外提供跳轉的頁面、服務的路由地址:

/**
 * 購物車元件路由表
 * 即 購物車元件中 所有可以從外部跳轉的頁面 的路由資訊
 * @author hufeiyang
 */
public interface CartRouterTable {

    /**
     * 購物車頁面
     */
    String PATH_PAGE_CART = "/cart/cartActivity";

    /**
     * 購物車服務
     */
    String PATH_SERVICE_CART = "/cart/service";

}

前面說頁面跳轉時是直接使用 路徑字串 進行路由跳轉,這裡是和服務路由都放在這裡統一管理。

然後,為了外部元件使用方便新建CartServiceUtil:

/**
 * 購物車元件服務工具類
 * 其他元件直接使用此類即可:頁面跳轉、獲取服務。
 * @author hufeiyang
 */
public class CartServiceUtil {

    /**
     * 跳轉到購物車頁面
     * @param param1
     * @param param2
     */
    public static void navigateCartPage(String param1, String param2){
        ARouter.getInstance()
                .build(CartRouterTable.PATH_PAGE_CART)
                .withString("key1",param1)
                .withString("key2",param2)
                .navigation();
    }

    /**
     * 獲取服務
     * @return
     */
    public static ICartService getService(){
        //return ARouter.getInstance().navigation(ICartService.class);//如果只有一個實現,這種方式也可以
        return (ICartService) ARouter.getInstance().build(CartRouterTable.PATH_SERVICE_CART).navigation();
    }

    /**
     * 獲取購物車中商品數量
     * @return
     */
    public static CartInfo getCartProductCount(){
        return getService().getProductCountInCart();
    }
}

注意到,這裡使用靜態方法 分別提供了頁面跳轉、服務獲取、服務具體方法獲取。 其中服務獲取 和頁面跳轉 同樣是使用路由,並且服務介面實現類 也是需要新增@Route註解指定路徑的。

到這裡,export_cart就已經準備完畢,我們同樣釋出一個export_cart的ARR(“com.github.hufeiyang.Cart:export_cart:xxx”)。

再來看看module_cart對服務介面的實現。

5.2.2 module_cart的實現

首先,module_cart需要依賴export_cart:

//module_cart的Build.gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //依賴export_cart
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}

點選sync後,接著CartActivity的path改為路由表提供:

@Route(path = CartRouterTable.PATH_PAGE_CART)
public class CartActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cart);
    }
}

然後,新建服務介面的實現類來實現ICartService,新增@Route註解指定CartRouterTable中定義的服務路由

/**
 * 購物車元件服務的實現
 * 需要@Route註解、指定CartRouterTable中定義的服務路由
 * @author hufeiyang
 */
@Route(path = CartRouterTable.PATH_SERVICE_CART)
public class CartServiceImpl implements ICartService {

    @Override
    public CartInfo getProductCountInCart() {
     //這裡實際專案中 應該是 請求介面 或查詢資料庫
        CartInfo cartInfo = new CartInfo();
        cartInfo.productCount = 666;
        return cartInfo;
    }

    @Override
    public void init(Context context) {
        //初始化工作,服務注入時會呼叫,可忽略
    }
}

這裡的實現是直接例項化了CartInfo,數量賦值666。然後釋出一個ARR(“com.github.hufeiyang.Cart:module_cart:xxx”)。

5.2.3 module_home中的使用和除錯

module_home需要依賴export_cart:

//module_home的Build.gradle
dependencies {
    ...
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation 'com.github.hufeiyang:Common:1.0.0'

    //注意這裡只依賴export_cart(module_cart由殼工程引入)
    implementation 'com.github.hufeiyang.Cart:export_cart:1.0.5'
}

在HomeActivity中新增TextView,呼叫CartServiceUtil獲取並展示購物車商品數量:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        //跳轉到購物車頁面
        findViewById(R.id.btn_go_cart).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通過路由跳轉到 購物車元件的購物車頁面(但沒有依賴購物車元件)
//                ARouter.getInstance()
//                        .build("/cart/cartActivity")
//                        .withString("key1","param1")//攜帶引數1
//                        .withString("key2","param2")//攜帶引數2
//                        .navigation();

                CartServiceUtil.navigateCartPage("param1", "param1");
            }
        });

        //呼叫購物車元件服務:獲取購物車商品數量
        TextView tvCartProductCount = findViewById(R.id.tv_cart_product_count);
        tvCartProductCount.setText("購物車商品數量:"+ CartServiceUtil.getCartProductCount().productCount);
    }
}

看到 使用CartServiceUtil.getCartProductCount()獲取購物車資訊並展示,跳轉頁面也改為了CartServiceUtil.navigateCartPage()方法。

到這裡home元件的就可以獨立除錯了:頁面跳轉和服務呼叫,獨立除錯ok後 再整合到殼工程。先讓HomePage工程的app模組依賴Common元件、module_cart 以及本地的module_home

//HomePage工程,app模組的Build.gradle
dependencies {
    ...
    //引入本地Common元件、module_cart、module_home,在app module中獨立除錯使用
    implementation 'com.github.hufeiyang:Common:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'

    implementation project(path: ':module_home')
}

然後新建MyApplication初始化ARouter、在app的MainActivity中使用ARouter.getInstance().build("/homepage/homeActivity").navigation()開啟首頁,這樣就可以除錯了。

除錯ok後接著就是整合到殼工程。

5.2.4 整合到殼工程

殼工程中的操作和獨立除錯類似,區別是對首頁元件引入的是ARR:

dependencies {
    ...
    //這裡沒有使用私有maven倉,而是發到JitPack倉,一樣的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.6'
    implementation 'com.github.hufeiyang:HomePage:1.0.4'

    //殼工程內 也需要依賴Common元件,因為需要初始化ARouter
    implementation 'com.github.hufeiyang:Common:1.0.0'
}

最後run殼工程來看下效果:獲取數量是666、跳轉頁面成功。

另外,除了export_xxx這種方式,還可以新增一個 ComponentBase 元件,這個元件被所有的Common元件依賴,在這個元件中分別新增定義了業務元件可以對外提供訪問自身資料的抽象方法的 Service。相當於把各業務元件的export整合到ComponentBase中,這樣就只新增了一個元件而已。但是這樣就不好管理了,每個元件對外能力的變更都要改ComponentBase。

另外,除了元件間方法呼叫,使用EventBus在元件間傳遞資訊也是ok的(注意Event實體類要定義在export_xxx中)。

好了,到這裡元件間通訊問題也解決了。

六、fragment例項獲取

上面介紹了Activity 的跳轉,我們也會經常使用 Fragment。例如常見的應用主頁HomeActivity 中包含了多個屬於不同元件的 Fragment、或者有一個Fragment多個元件都需要用到。通常我們直接訪問具體 Fragment 類來new一個Fragment 例項,但這裡元件間沒有直接依賴,那咋辦呢?答案依然是ARouter

先在module_cart中建立CartFragment:

//新增註解@Route,指定路徑
@Route(path = CartRouterTable.PATH_FRAGMENT_CART)
public class CartFragment extends Fragment {
    ...
    public CartFragment() {
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        //顯示“cart_fragment"
        return inflater.inflate(R.layout.fragment_cart, container, false);
    }
}

同時是fragment新增註解@Route,指定路由路徑,路由還是定義在export_cart的CartRouterTable中,所以export_cart需要先發一個ARR,module_cart來依賴,然後module_cart釋出ARR。

然後再module_home中依賴export_cart,使用ARouter獲取Fragment例項:

@Route(path = "/homepage/homeActivity")
public class HomeActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        ...
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction transaction= manager.beginTransaction();

  //使用ARouter獲取Fragment例項 並新增
        Fragment userFragment = (Fragment) ARouter.getInstance().build(CartRouterTable.PATH_FRAGMENT_CART).navigation();
        transaction.add(R.id.fl_test_fragment, userFragment, "tag");
        transaction.commit();
    }
}

可以先獨立除錯,然後整合到殼工程——依賴最新的module_cart 、HomePage,結果如下:

通訊成功,展示666

綠色部分就是引用自cart元件的fragment。

七、Application生命週期分發

我們通常會在Application的onCreate中做一些初始化任務,例如前面提到的ARouter初始化。而業務元件有時也需要獲取應用的Application,也要在應用啟動時進行一些初始化任務。

你可能會說,直接在殼工程Application的onCreate操作就可以啊。但是這樣做會帶來問題:因為我們希望殼工程和業務元件 程式碼隔離(雖然有依賴),並且 我們希望元件內部的任務要在業務元件內部完成。

那麼如何做到 各業務元件 無侵入地獲取 Application生命週期 呢?——答案是 使用AppLifeCycle外掛,它專門用於在Android元件化開發中,Application生命週期主動分發到元件。具體使用如下:

  1. common元件依賴 applifecycle-api首先,common元件通過 api 新增 applifecycle-api 依賴 併發布ARR:

//common元件 build.gradle
dependencies {
    ...
    //AppLifecycle
    api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
  1. 業務元件依賴applifecycle-compiler、實現介面+註解各業務元件都要 依賴最新common元件,並新增 applifecycle-compiler 的依賴:

//業務元件 build.gradle
...
 //這裡Common:1.0.2內依賴了applifecycle-api
    implementation 'com.github.hufeiyang:Common:1.0.2'
    annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'

sync後,新建類來實現介面IApplicationLifecycleCallbacks用於接收Application生命週期,且新增@AppLifecycle註解。

例如 Cart元件的實現:

/**
 * 元件的AppLifecycle
 * 1、@AppLifecycle
 * 2、實現IApplicationLifecycleCallbacks
 * @author hufeiyang
 */
@AppLifecycle
public class CartApplication implements IApplicationLifecycleCallbacks {

    public  Context context;

    /**
      * 用於設定優先順序,即多個元件onCreate方法呼叫的優先順序
      * @return
     */
    @Override
    public int getPriority() {
        return NORM_PRIORITY;
    }

    @Override
    public void onCreate(Context context) {
        //可在此處做初始化任務,相當於Application的onCreate方法
        this.context = context;

        Log.i("CartApplication", "onCreate");
    }

    @Override
    public void onTerminate() {
    }

    @Override
    public void onLowMemory() {
    }

    @Override
    public void onTrimMemory(int level) {
    }
}

實現的方法 有onCreate、onTerminate、onLowMemory、onTrimMemory。最重要的就是onCreate方法了,相當於Application的onCreate方法,可在此處做初始化任務。並且還可以通過getPriority()方法設定回撥 多個元件onCreate方法呼叫的優先順序,無特殊要求設定NORM_PRIORITY即可。

  1. 殼工程引入AppLifecycle外掛、觸發回撥

殼工程引入新的common元件、業務元件,以及 引入AppLifecycle外掛:

//殼工程根目錄的 build.gradle

buildscript {
   
    repositories {
        google()
        jcenter()

        //applifecycle外掛倉也是jitpack
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'

        //載入外掛applifecycle
        classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3'
    }
}
//app module 的build.gradle

apply plugin: 'com.android.application'
//使用外掛applifecycle
apply plugin: 'com.hm.plugin.lifecycle'
...
dependencies {
    ...
    //這裡沒有使用私有maven倉,而是發到JitPack倉,一樣的意思~
//    implementation 'com.hfy.cart:cart:1.0.0'
    implementation 'com.github.hufeiyang.Cart:module_cart:1.0.11'
    implementation 'com.github.hufeiyang:HomePage:1.0.5'

    //殼工程內 也需要依賴Common元件,因為要 觸發生命週期分發
    implementation 'com.github.hufeiyang:Common:1.0.2'
}

最後需要在Application中觸發生命週期的分發:

//殼工程 MyApplication
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...

        ApplicationLifecycleManager.init();
        ApplicationLifecycleManager.onCreate(this);
    }

    @Override
    public void onTerminate() {
        super.onTerminate();

        ApplicationLifecycleManager.onTerminate();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();

        ApplicationLifecycleManager.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        ApplicationLifecycleManager.onTrimMemory(level);
    }
}

首先在inCreate方法中呼叫 ApplicationLifecycleManager的init()方法,用於收集元件內實現了IApplicationLifecycleCallbacks且新增了@AppLifecycle註解的類。然後在各生命週期方法內呼叫對應的ApplicationLifecycleManager的方法,來分發到所有元件。

這樣 元件 就能接收到Application的生命週期了。新增元件的話,只需要 實現IApplicationLifecycleCallbacks並新增了@AppLifecycle註解 即可,無需修改殼工程,也不用關心

AppLifecycle外掛是使用了 APT技術、gradle外掛技術+ASM動態生成位元組碼,在編譯階段就已經完成了大部分工作,無效能問題、且使用方便。

到這裡,元件化開發的5個問題點 都已經解決了。下面來看看針對老專案如何實現元件化改造。

八、 老專案元件化

通常情況 我們去做元件化,都是為了改造 已有老專案。可能老專案內部的模組之間耦合嚴重,沒有嚴格的業務模組劃分,並且元件化改造是大工作量的事情,且要全量回歸測試,總體來說,是需要全員參與、有較大難度的事情。

8.1 方案

8.1.1 元件劃分

根據前面介紹的元件化架構圖,元件分為 基礎元件、業務基礎元件、業務元件。

  • 基礎元件,不用多說,就是基礎功能,例如網路請求、日誌框架、圖片載入,這些與業務毫無關聯,可用於公司所有專案,是底層最穩定的元件。這裡就比較容易識別和拆分。

  • 業務基礎元件,主要是供業務元件依賴使用,例如 分享、支付元件,通常是一個完整的功能,是較為最穩定的元件。這部分通常也是比較容易識別的。

  • 業務元件,完整的業務塊,例如前面提到京東的  “首頁”、“分類”、“發現”、“購物車”、“我的”。業務元件是日常需求開發的主戰場。

8.1.2 元件拆分:基礎元件、Common元件

基礎元件最容易拆分,它依賴最少,功能單一純粹。把基礎元件依賴的東西,從老工程中抽取出來,放在單獨的工程,做成單獨的元件,釋出ARR到公司maven倉。注意不能存在任何業務相關程式碼。

新建Common元件,使用 “api” 依賴 所有基礎元件,這樣依賴 Common元件的元件 就能使用所有基礎元件的功能了。接著,就是前面提到的 ARouter、AppLifeCycle、以及其他第三方庫的依賴。

另外,Common元件,還有一個重要部分:提供BaseActivity、BaseFragment,這裡Base需要完成基礎能力的新增,例如頁面進入、退出的埋點上報、統一頁面標題樣式、開啟關閉EventBus等等。

8.1.3  元件拆分:業務基礎元件、業務元件

業務基礎元件 基本上只依賴common,功能也是單一純粹。同樣是把依賴的東西抽取出來,放在單獨的工程,做成單獨的元件,釋出ARR到公司maven倉。

業務元件,首先要識別元件的邊界,可以按照頁面入口和出口作為判斷。然後,需要識別對 業務基礎元件的依賴;以及 最重要的,對其他 業務元件的依賴。可以先把程式碼抽離到單獨的工程,然後依賴common元件、需要的業務基礎元件,此時依然報錯的地方就是 對其他 業務元件的依賴了。這時就可以給對應元件負責人提需求,在export_xxx中提供跳轉和服務。然後你只需要依賴export_xxx使用即可

老專案元件化改造需要循序漸進,除非有專門的時間。一般是需求開發和改造並行。要先完成一個元件,之後有了經驗,後面其他業務元件陸續實施,這樣就會比較簡單。

8.2 常見問題

8.2.1 元件中butterknife報錯—R2

在Library中,ButterKnife註解中使用R.id會報錯,例如common元件 module_common 中新建Activity,並依賴butterknife:

android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.3'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}

報錯如下所示:解決方法:需要新增ButterKnife外掛,然後使用R2:

buildscript {
  repositories {
    mavenCentral()
    google()
  }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
  }
}
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

然後ButterKnife註解中使用 R2 就ok了:

到這裡,Android元件化的知識全部講完。

八、總結

本文介紹了 元件化開發的背景、架構、優勢、要解決的問題 以及詳細解決方案,獨立除錯、頁面跳轉、元件通訊等,最後介紹的老專案元件化方案。

其中涉及的最重要的工具是ARouter,專門用於Android元件化解耦。ARouter還有很多進階用法,有機會我也針對ARouter寫一篇全面分析。還有一個重要知識點AppLifecycle外掛,它的原理涉及APT、ASM插入位元組碼、gradle外掛等技術,後續也會專門去分析這塊知識。

Android開發元件化,是在專案發展到一定規模後 必定要使用的技術,學習至完全掌握非常必要。

好了,今天就到這裡,歡迎留言討論~

Demo的GitHub地址:

殼工程ComponentLearning:

https://github.com/hufeiyang/ComponentLearning

AppLifecycle外掛:

https://github.com/hufeiyang/Android-AppLifecycleMgr

 

 

---END---

 

推薦閱讀:

Java 中的 AQS 到底是什麼?高階面試必問!

Java 中的 AQS 到底是什麼?高階面試必問!

Android Studio 4.1重磅釋出:支援內嵌安卓模擬器!

12種Flutter開發工具推薦

最受歡迎的女友職業排行榜Top10!

使用 Paging 3 實現分頁載入

一位40歲“老程式設計師”的經歷,給你們說一些我的真實想法!

全球最大的色情網站,保留著西方媒體最後的良心

 

相關文章