1. 程式人生 > >Android 業務組件化開發實踐

Android 業務組件化開發實踐

公司 跳轉 not apk 優勢 避免 項目結構 行為 分享

組件化並不是新話題,其實很早很早以前我們開始為項目解耦的時候就討論過的。但那時候我們說的是功能組件化。比如很多公司都常見的,網絡請求模塊、登錄註冊模塊單獨拿出來,交給一個團隊開發,而在用的時候只需要接入對應模塊的功能就可以了。

百牛信息技術bainiu.ltd整理發布於博客園

今天我們來討論一下業務組件化,拿出手機,打開淘寶或者大眾點評來看看,裏面的美食電影酒店外賣就是一個一個的業務。如果我們在一個項目裏面去寫的時候,總會出現或多或少的代碼耦合,最典型的有時為了趕上線時間而先復制粘貼一段類似的代碼過來,結果這段代碼引用的資源可能是另一個模塊獨立的資源或代碼。但是如果將一個項目作為獨立的工程來運行,就完全可以避免這種情況了。但是這並不是業務組件化最大的優勢,我認為最大的優勢是它大大縮減了工程結構直接降低了編譯時間。

代碼實現

註意,組件化不是插件化,插件化是在[運行時],而組件化是在[編譯時]。換句話說,插件化是基於多 APK 的,而組件化本質上還是只有一個 APK。

代碼實現上核心思路要緊記一句話:開發時是 application,發版時是 library。
來看一段 gradle 代碼:

if (isDebug.toBoolean()) {
    apply plugin: ‘com.android.application‘
} else {
    apply plugin: ‘com.android.library‘
}

非常好理解,我們在開發的時候,module 如果是一個庫,會使用com.android.library

插件,如果是一個應用,則使用com.android.application插件,我們通過一個開關來控制這個狀態的切換。
然後因為我們需要在 library 和 application 之間切換,manifest文件也需要提供兩套。
技術分享

舉個栗子?

你可以根據這個項目一起看:https://github.com/kymjs/Modularity

假設有一個項目,這個項目包含一個叫 explorer 的文件瀏覽器的模塊和一個叫 memory-box 的筆記的模塊。因為這兩個功能相對獨立,我們將這兩個功能拆分成兩個 module,再加上原本項目的 app module,總共三個。
在 explorer 的根目錄建立一個作為開關的 properties 文件(寫一個全局變量也可以,怎麽簡單怎麽來),方便用來改變當前是開發狀態還是發版狀態(debug & release)。 從gradle中讀取這個文件中的值,來切換不同狀態所需要調用的配置。順便一提,當你修改了 properties 文件中的值時,必須要重新 sync 一下。 詳細配置過程可以看看這篇文章:http://www.zjutkz.net/

遇到的問題

阿布他們的項目大量的用了 databinding 和 dagger,然而我們項目並沒有用這些,用了這兩個庫的可以看看他是怎麽爬坑的:魔都三帥

當你采用了組件化開發的時候,一定會遇到這幾個問題,這幾個問題除了第三個都只能規避,沒有好的處理辦法:

1、module 中 Application 調用的問題
2、跨 module 的 Activity 或 Fragment 跳轉問題
3、AAR 或 library project 重復依賴
4、資源名沖突

解決 Application 沖突

由於 module 在開發過程中是以 application 的形式存在的,如果這個 module 調用了類似 ((XXXApplication)getApplication()).xxx()這種代碼的話,最終 release 項目時一定會發生類轉換異常。因為在 debug 狀態下的 module 是一個 application,而在 release 狀態下它只是一個 lib。所以也就是在 debug 和 release 時獲取到的 Application 不是同一個類的對象。
這個問題還好,我們只要在 application 裏面盡量不要寫方法實現,不要做強轉操作就好。
如果確實要區分,業務模塊在 debug 狀態和 release 狀態有不同的行為,可以通過擴展 BuildConfig 這個類,在代碼中通過 boolean 值來執行不同的邏輯。只需要在 gradle 中加入(具體代碼用法可查看【line:48】):

if (isDebug.toBoolean()) {
    buildConfigField ‘boolean‘, ‘ISAPP‘, ‘true‘
} else {
    buildConfigField ‘boolean‘, ‘ISAPP‘, ‘false‘
}

有些人喜歡將 application 單例,寫一個靜態的對象,然後在代碼裏面需要context的時候用這個全局單例。這樣的情況我送大家一個工具類(其實是從馮老師代碼裏偷來的):Common

public class App {
    public static final Application INSTANCE;
    
    static {
        Application app = null;
        try {
            app = (Application) Class.forName("android.app.AppGlobals").getMethod("getInitialApplication").invoke(null);
            if (app == null)
                throw new IllegalStateException("Static initialization of Applications must be on main thread.");
        } catch (final Exception e) {
            LogUtils.e("Failed to get current application from AppGlobals." + e.getMessage());
            try {
                app = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null);
            } catch (final Exception ex) {
                LogUtils.e("Failed to get current application from ActivityThread." + e.getMessage());
            }
        } finally {
            INSTANCE = app;
        }
    }
}

跨 module 跳轉

如果單獨是 Activity 跳轉,常見的做法是:隱式啟動 Activity、或者定義 scheme 跳轉。
但是如果界面是一個 Fragment 就比較麻煩了,我推薦的是直接通過類名跳轉。

首先創建一個所有界面類名的列表

public class RList {
    public static final String ACTIVITY_MEMORYBOX_MAIN = "com.kymjs.app.memory.module.main.MainActivity";
    
    public static final String FRAGMENT_MEMORYBOX_MAIN = "com.kymjs.app.memory.module.list.MainFragment";
}

在獲取 Fragment 的時候就可以根據列表中的類名來讀取指定的 Fragment 了。

public class FragmentRouter {

    public static Fragment getFragment(String name) {
        Fragment fragment;
        try {
            Class fragmentClass = Class.forName(name);
            fragment = (Fragment) fragmentClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return fragment;
    }
}

同理,Activity 其實也可以用這種方法來跳轉:

public static void startActivityForName(Context context, String name) {
    try {
        Class clazz = Class.forName(name);
        startActivity(context, clazz);
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

最後,對於這個RList類,我們還可以通過 Gradle 腳本來生成,就像 R 文件一樣,這樣子開發就要方便很多了。

重復依賴

重復依賴問題其實在開發中經常會遇到,比如你 compile 了一個A,然後在這個庫裏面又 compile 了一個B,然後你的工程中又 compile 了一個同樣的B,就依賴了兩次。
默認情況下,如果是 aar 依賴,gradle 會自動幫我們找出新版本的庫而拋棄舊版本的重復依賴。但是如果你使用的是 project 依賴,gradle 並不會去去重,最後打包就會出現代碼中有重復的類了。
一種是 將 compile 改為 provided,只在最終的項目中 compile 對應的代碼;
還可以使用這種方案:
技術分享
可以將所有的依賴寫在 shell 層的 module,這個 shell 並不做事情,他只用來將所有的依賴統一成一個入口交給上層的 app 去引入,而項目所有的依賴都可以寫在 shell module 裏面。

資源名沖突

因為分了多個 module,在合並工程的時候總會出現資源引用沖突,比如兩個 module 定義了同一個資源名。
這個問題也不是新問題了,做 SDK 基本都會遇到,可以通過設置 resourcePrefix 來避免。設置了這個值後,你所有的資源名必須以指定的字符串做前綴,否則會報錯。
但是 resourcePrefix 這個值只能限定 xml 裏面的資源,並不能限定圖片資源,所有圖片資源仍然需要你手動去修改資源名。

項目結構

技術分享

app 是最終工程的目錄
explorer 和 memory-box 是兩個功能模塊,他們在開發階段是以獨立的 application,在 release 時才會作為 library 引入工程。
router 有兩個功能,一個是作為路由,用於提供界面跳轉功能。另一個功能是前面講的 shell ,作為依賴集合,讓各業務 module 接入。 base-res 是一些通用的代碼,即每個業務模塊都會接入的部分,它會在 router 中被引入。

最終代碼可以查看:https://github.com/kymjs/Modularity

Android 業務組件化開發實踐