我的 Android 組件化之路 結構圖 爲什麼要做組件化 組件化過程中的幾個問題 其他思考

結構圖

其中路由數據組件爲上層業務組件必須要依賴的庫,獨立功能組件和公共 UI 組件可以根據需求選擇是否依賴。公共 UI 組件爲應用整體 UI 風格上的公共配置和封裝,一般業務組件也都會依賴。基礎SDK 爲最底層的 SDK 庫,所有的業務組件都基於它。頂層的業務 APP 一般按功能模塊進行劃分譬如:郵件 AppIM App視頻 App

爲什麼要做組件化

一、做組件化主要是隨着軟件的版本迭代,暴露出一個巨大的問題。同一個 module 下,各種數據跳轉之間高度的耦合了,雖然開發要求要注意代碼的耦合度,但團隊中每個人的經驗水平和編碼風格都不一樣,對這個耦合程度的理解和標準也不一樣,隨着時間推移模塊間的代碼會越寫相互依賴程度越大。畢竟有時候明明能直接拿過來用,就不會太多的去考慮設計模式。做組件化將相對獨立的模塊獨立出去,達到硬性代碼隔離,強制降低模塊耦合度的目的。
二、項目隨着開發需求的不斷迭代會變得越來越龐大,開發過程中項目整編是個很費時的事,組件化之後可以靈活配置選擇需要的組件編譯,縮短時間
三、多個項目中有的組件是可以共用的,像我經歷過的兩個項目的網盤模塊和郵件模塊。未採用組件化方案,移代碼移資源太費時費力了。採用組件化方案,直接將 module 導入新的項目,增加對應的路由和 路由Service 方法就能用(前提是項目都採用組件化方案)

組件化過程中的幾個問題

多個組件 module 怎樣共用 Application

1、在 BaseApplication 中創建 AppProxy 類,(這個類是 IAppLifeCycle 的一個實現類)。在 BaseApplication 的生命週期方法中調用 AppProxy 的生命週期方法
2、AppProxy 構造函數中掃描 Manifest 文件,掃描類中通過反射拿到每個組件中的實現類。將這些實現類添加到 AppProxy 中的列表中。
3、在生命週期方法中循環第二步中的列表調用列表內各個 module 注入的生命週期代理對象的對應方法
核心處理即在 AppProxy 類中:

各個 module 的代理實現類一定要註冊到 manifest 中,否則會掃描不到

        <!--配置 Application-->
        <meta-data
            android:name="com.pandaq.pandamvp.app.lifecycle.LifeCycleInjector"
            android:value="AppInjector"/>

這樣配置之後我們是沒辦法手動控制 module 生命週期方法的調用順序的,因此在 LifeCycleInjector 中增加了優先級選項,默認爲 0,數字越大越延後加載

    /**
     * priority for lifeCycle methods inject
     *
     * @return priority 0 for first
     */
    int priority();

Activity 及 Fragment 生命週期

  • Activity:如上圖中,與 Application 生命週期注入對應,在 AppProxy 的 onCreate() 方法中將 Activity 生命週期回調註冊到 application 中。通過 Application 來管理 Activity 生命週期。
   @Override
    public void onCreate(@NonNull Application application) {
        for (IAppLifeCycle appLifeCycle : mAppLifeCycles) {
            appLifeCycle.onCreate(application);
        }
        // 註冊各個 module activity 生命週期回調方法 
        for (Application.ActivityLifecycleCallbacks callbacks : mActivityLifeCycles) {
            application.registerActivityLifecycleCallbacks(callbacks);
        }

    }
    
    @Override
    public void onTerminate(@NonNull Application application) {
        if (mAppLifeCycles != null) {
            for (IAppLifeCycle appLifeCycle : mAppLifeCycles) {
                appLifeCycle.onTerminate(application);
            }
        }
        // app 生命週期結束時註銷 activity 生命週期回調 
        if (mActivityLifeCycles != null) {
            for (Application.ActivityLifecycleCallbacks callbacks : mActivityLifeCycles) {
                application.unregisterActivityLifecycleCallbacks(callbacks);
            }
        }
        mAppLifeCycles = null;
        mActivityLifeCycles = null;
        mFragmentLifecycleCallbacks = null;
        AppUtils.release();
    }
  • Fragment
    各module 內與 Activity 的生命週期注入一樣,通過 ILifecycleInjector 的實現類,將 Fragment 生命週期實現類添加到注入列表中,但在 AppProxy 中處理不再是通過註冊到 Application 來管理,而是通過一個默認的 Activity生命週期實現類,將這些 fragment 生命週期回調類統一註冊到 FragmentManager 中
    private void registerFragmentCallbacks(Activity activity) {
        //註冊框架外部, 開發者擴展的 BaseFragment 生命週期邏輯
        for (FragmentManager.FragmentLifecycleCallbacks fragmentLifecycle : mFragmentLifeCycles) {
            if (activity instanceof FragmentActivity) {
                ((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(fragmentLifecycle, true);
            }
        }
    }

描述可能不太容易看懂,具體的代碼可以參考 PandaMvp

組件間的通信

組件中的通信這裏採用了 ARouter,具體使用這裏不展開,直接去看 ARouter 的文檔,幾個關鍵點:
一、頁面跳轉:.

@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
ARouter.getInstance().build("/test/activity")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();

二、頁面值的回傳:

// 構建標準的路由請求,startActivityForResult
// navigation的第一個參數必須是Activity,第二個參數則是RequestCode
ARouter.getInstance().build("/home/main").navigation(this, 5);

三、Fragment 發現:

// 獲取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();

四、跨組件方法調用:

// 聲明接口,其他組件通過接口來調用服務, router 組件中定義
public interface EmailService extends IProvider {
    EmailAccount getAccount();
}

// 實現接口對應的業務組件中實現
@Route(path = "/email/emailservice")
public class EmailServiceImpl implements EmailService {

    @Override
    public EmailAccount getAccount() {
    return new EmailAccount();
    }

    @Override
    public void init(Context context) {

    }
}
// 調用組件中發現服務再調用方法
public class Test {
    @Autowired(name = "/email/emailservice")
    EmailService emailService;

    public Test() {
        ARouter.getInstance().inject(this);
        EmailAccount account = emailService.getAccount();
    }

}

爲避免書寫錯誤等問題,最好定義常量類統一管理路由的 path 併爲每個組件使用不同的父路徑分組

組件間數據實體共享

獨立組件間的數據獲取傳遞都通過 Arouter 的服務來完成:

如圖所示,每一個獨立的業務 module 在 router module 下都有一個自己的文件夾。以 email 組件爲例將,其他的業務組件需要獲取郵件組件中用戶郵件的賬號信息和郵件簽名,則 email 將自己的賬號信息和簽名信息類 MailAccountMailSign 註冊到 router module 中,這樣其他組件就能通過對 router module 的依賴識別這兩個數據類。A 組件需要從 Email 組件獲取 MailAccount。首先 Email 組件要在 router 中的服務 EmailService 接口中註冊對外暴露 getAccount() 方法,如果獲取爲異步的,則還需要再 callbacks 中註冊一個回調方法。A 中通過接口回調異步拿到 EmailService 給他的數據。

ORM 數據庫的增刪改查

ORM 數據庫,項目採用的是 GreenDao3.0。因爲 GreenDao 的初始化和 Tab生成不能跨 module,所以在存儲數據時有兩種方案:
1、每個業務 module 自己維護數據庫。業務組件間需要通信的數據再單獨創建類下沉到一個公共庫中去。這種方式能保證業務數據的完全獨立,但需要多寫數據實體類
2、直接把數據實體類都放在一個公共庫中,GreenDao 的初始化也放在這個庫中。我在項目的實際操作中是將要存入數據庫的實體類放入 Router module 中按文件夾分開存放的。
數據庫的操作工具類定義在對應的組件內,如 Email 組件,其中的緩存表操作工具類叫 EmailTb,通過 EmailTb 的方法對數據庫進行增刪改查。Email 組件內部增刪改查沒有任何的阻礙隔離,如果 A組件需要對 Email 表進行增刪改查,則需要通過 EmailService 中註冊暴露的方法間接的增刪改查。如果 Email 未註冊暴露對應方法則其他組件不能對 Email 數據庫操作

資源文件重名問題

    resourcePrefix "a_"

通過在 gradle 中配置 resourcePrefix 統一爲資源文件添加前綴限制,在編譯時命名不符合規範編譯器將會提示錯誤。進行組件化改造時這是個體力活,說多了都是淚

module 組合運行

module 自由組合運行,則需要 module 既要有成爲 application 的能力又要有作爲 library 的能力。我們通過 gradle.propertiesbuild.gradle 文件配置,通過腳本在編譯時決定打包哪些業務組件 App 組成應用。

# 1、整編模式
# launchApp = app
# buildAll = true
#
# 2、單組件調試
# launchApp = xx (組件module名字)
# buildAll = false
#
# 2、多組件調試
# launchApp = app (組件 module 名字)
# buildAll = false
# loadComponents = xx1,xx2 (需要聯調的組件 module 名字)
#
# 打包容器 App module 名字
shellApp = app
# 被啓動的業務組件的名字,打包發佈時一定爲外殼 APP
launchApp = app_bmodule
#是否整編 App,true 的時候會殼 App 打包會依賴 allComponents。false 打包會依賴 loadComponents
buildAll = false
# 所有業務組件 App 的 module 名字
allComponents = app_amodule,app_bmodule
# 多業務組件放入 shellApp 聯合調試啓用的 module 名字
loadComponents =

公共的 build.gradle 中配置

// 根據配置是否爲 launchApp 決定業務組件 module 是作爲 library 還是獨立 App
boolean isShellApp = project.getName() == shellApp
boolean isLaunchApp = project.getName() == launchApp
if (isLaunchApp) { // 殼 APP 始終以 application 模式運行,其他業務組件以依賴庫模式根據配置拔插
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
·
·
·
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isShellApp){ // 容器 App 只會以 App模式運行或者不運行
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }else {
            // application 模式和 library 模式的清單文件是不一樣的,這裏根據 isLaunchApp 確定使用哪一個
                if (isLaunchApp) {
                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/release/AndroidManifest.xml'
                }
            }
        }
    }

各業務組件 module 的,build.gradle:

·
·
·
    defaultConfig { //根據是否爲 launchApp 決定添加 applicationId 和版本號 
        if (isLaunchApp) {
            applicationId "com.pandaq.app_amodule"
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
·
·
·

容器 App module(主 module),相較於一般業務組件 module 的 build.gradle,還需要配置多組件打包和整編打包時動態依賴業務組件庫:

·
·
·
    defaultConfig { //根據是否爲 launchApp 決定添加 applicationId 和版本號 
        if (isLaunchApp) {
            applicationId "com.pandaq.app_amodule"
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
·
·
·
dependencies{
        ·
        ·
        ·
    // 按需加載依賴業務 APP
    if (buildAll.toBoolean()) { //整編時將業務組件全部添加依賴
        for (String name : allComponents.split(",")) {
            implementation(project(":$name")) {
                exclude group: 'com.android.support'
                exclude module: 'appcompat-v7'
                exclude module: 'support-v4'
            }
        }
    } else { //非整編時可以選擇業務組價加入容器 App
        for (String name : loadComponents.split(",")) {
            if (!name.isEmpty()) {
                implementation(project(":$name")) {
                    exclude group: 'com.android.support'
                    exclude module: 'appcompat-v7'
                    exclude module: 'support-v4'
                }
            }
        }
    }
}

其他思考

組件化有風險,推進需謹慎。一個非組件化的大型項目要對其進行組件化改造這個過程是漫長而艱鉅的,項目中各個模塊不可避免的會有各種耦合關係,往往牽一髮而動全身,要對它進行組件化改造。首先要對項目進行封裝解耦,獨立的功能該下沉的下沉,該重寫的重寫。有時候代碼的複用對組件化改造簡直是災難,尤其是本來不屬於一個功能模塊的界面進行了複用這種。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章