我的 Android 組件化之路

結(jié)構(gòu)圖

PandaMvp 組件化結(jié)構(gòu)圖

其中路由數(shù)據(jù)組件為上層業(yè)務(wù)組件必須要依賴的庫,獨立功能組件和公共 UI 組件可以根據(jù)需求選擇是否依賴。公共 UI 組件為應(yīng)用整體 UI 風(fēng)格上的公共配置和封裝,一般業(yè)務(wù)組件也都會依賴。基礎(chǔ)SDK 為最底層的 SDK 庫,所有的業(yè)務(wù)組件都基于它。頂層的業(yè)務(wù) APP 一般按功能模塊進行劃分譬如:郵件 App、IM App視頻 App

為什么要做組件化

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

組件化過程中的幾個問題

多個組件 module 怎樣共用 Application

Application 代理類生命周期接口

1、在 BaseApplication 中創(chuàng)建 AppProxy 類,(這個類是 IAppLifeCycle 的一個實現(xiàn)類)。在 BaseApplication 的生命周期方法中調(diào)用 AppProxy 的生命周期方法
2、AppProxy 構(gòu)造函數(shù)中掃描 Manifest 文件,掃描類中通過反射拿到每個組件中的實現(xiàn)類。將這些實現(xiàn)類添加到 AppProxy 中的列表中。
3、在生命周期方法中循環(huán)第二步中的列表調(diào)用列表內(nèi)各個 module 注入的生命周期代理對象的對應(yīng)方法
核心處理即在 AppProxy 類中:

AppProxy 生命周期

各個 module 的代理實現(xiàn)類一定要注冊到 manifest 中,否則會掃描不到

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

這樣配置之后我們是沒辦法手動控制 module 生命周期方法的調(diào)用順序的,因此在 LifeCycleInjector 中增加了優(yōu)先級選項,默認為 0,數(shù)字越大越延后加載

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

Activity 及 Fragment 生命周期

  • Activity:如上圖中,與 Application 生命周期注入對應(yīng),在 AppProxy 的 onCreate() 方法中將 Activity 生命周期回調(diào)注冊到 application 中。通過 Application 來管理 Activity 生命周期。
   @Override
    public void onCreate(@NonNull Application application) {
        for (IAppLifeCycle appLifeCycle : mAppLifeCycles) {
            appLifeCycle.onCreate(application);
        }
        // 注冊各個 module activity 生命周期回調(diào)方法 
        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 生命周期結(jié)束時注銷 activity 生命周期回調(diào) 
        if (mActivityLifeCycles != null) {
            for (Application.ActivityLifecycleCallbacks callbacks : mActivityLifeCycles) {
                application.unregisterActivityLifecycleCallbacks(callbacks);
            }
        }
        mAppLifeCycles = null;
        mActivityLifeCycles = null;
        mFragmentLifecycleCallbacks = null;
        AppUtils.release();
    }
  • Fragment
    各module 內(nèi)與 Activity 的生命周期注入一樣,通過 ILifecycleInjector 的實現(xiàn)類,將 Fragment 生命周期實現(xiàn)類添加到注入列表中,但在 AppProxy 中處理不再是通過注冊到 Application 來管理,而是通過一個默認的 Activity生命周期實現(xiàn)類,將這些 fragment 生命周期回調(diào)類統(tǒng)一注冊到 FragmentManager 中
    private void registerFragmentCallbacks(Activity activity) {
        //注冊框架外部, 開發(fā)者擴展的 BaseFragment 生命周期邏輯
        for (FragmentManager.FragmentLifecycleCallbacks fragmentLifecycle : mFragmentLifeCycles) {
            if (activity instanceof FragmentActivity) {
                ((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(fragmentLifecycle, true);
            }
        }
    }

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

組件間的通信

組件中的通信這里采用了 ARouter,具體使用這里不展開,直接去看 ARouter 的文檔,幾個關(guān)鍵點:
一、頁面跳轉(zhuǎn):.

@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();

二、頁面值的回傳:

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

三、Fragment 發(fā)現(xiàn):

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

四、跨組件方法調(diào)用:

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

// 實現(xiàn)接口對應(yīng)的業(yè)務(wù)組件中實現(xiàn)
@Route(path = "/email/emailservice")
public class EmailServiceImpl implements EmailService {

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

    @Override
    public void init(Context context) {

    }
}
// 調(diào)用組件中發(fā)現(xiàn)服務(wù)再調(diào)用方法
public class Test {
    @Autowired(name = "/email/emailservice")
    EmailService emailService;

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

}

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

組件間數(shù)據(jù)實體共享

獨立組件間的數(shù)據(jù)獲取傳遞都通過 Arouter 的服務(wù)來完成:

router 組件文件夾結(jié)構(gòu)

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

ORM 數(shù)據(jù)庫的增刪改查

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

資源文件重名問題

    resourcePrefix "a_"

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

module 組合運行

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

# 1、整編模式
# launchApp = app
# buildAll = true
#
# 2、單組件調(diào)試
# launchApp = xx (組件module名字)
# buildAll = false
#
# 2、多組件調(diào)試
# launchApp = app (組件 module 名字)
# buildAll = false
# loadComponents = xx1,xx2 (需要聯(lián)調(diào)的組件 module 名字)
#
# 打包容器 App module 名字
shellApp = app
# 被啟動的業(yè)務(wù)組件的名字,打包發(fā)布時一定為外殼 APP
launchApp = app_bmodule
#是否整編 App,true 的時候會殼 App 打包會依賴 allComponents。false 打包會依賴 loadComponents
buildAll = false
# 所有業(yè)務(wù)組件 App 的 module 名字
allComponents = app_amodule,app_bmodule
# 多業(yè)務(wù)組件放入 shellApp 聯(lián)合調(diào)試啟用的 module 名字
loadComponents =

公共的 build.gradle 中配置

// 根據(jù)配置是否為 launchApp 決定業(yè)務(wù)組件 module 是作為 library 還是獨立 App
boolean isShellApp = project.getName() == shellApp
boolean isLaunchApp = project.getName() == launchApp
if (isLaunchApp) { // 殼 APP 始終以 application 模式運行,其他業(yè)務(wù)組件以依賴庫模式根據(jù)配置拔插
    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 模式的清單文件是不一樣的,這里根據(jù) isLaunchApp 確定使用哪一個
                if (isLaunchApp) {
                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/release/AndroidManifest.xml'
                }
            }
        }
    }

各業(yè)務(wù)組件 module 的,build.gradle:

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

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

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

其他思考

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容