Android業務組件化開發實踐

hytking 8年前發布 | 12K 次閱讀 Gradle 安卓開發 Android開發 移動開發

寫在前面

借用阿布倪盟博的一句話:“在MDCC中馮森林老師的《回歸初心,從容器化到組件化》,為我們這些沒有那么多精力折騰黑科技開發者們打開了另一扇門” 。

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

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

代碼實現

注意,組件化不是插件化,插件化是在[運行時],而組件化是在[編譯時]。換句話說,插件化是基于多 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文件也需要提供兩套。

舉個栗子?

假設有一個項目,這個項目包含一個叫 explorer 的文件瀏覽器的模塊和一個叫 memory-box 的筆記的模塊。因為這兩個功能相對獨立,我們將這兩個功能拆分成兩個 module,再加上原本項目的 app module,總共三個。

在 explorer 的根目錄建立一個作為開關的 properties 文件(寫一個全局變量也可以,怎么簡單怎么來),方便用來改變當前是開發狀態還是發版狀態(debug & release)。 從gradle中讀取這個文件中的值,來切換不同狀態所需要調用的配置。順便一提,當你修改了 properties 文件中的值時,必須要重新 sync 一下。 

遇到的問題

阿布他們的項目大量的用了 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;
    }
}

}</code></pre>

跨 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";

}</code></pre>

在獲取 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;
}

}</code></pre>

同理,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 中被引入。

 

 

來自:http://www.kymjs.com/code/2016/10/18/01

 

 本文由用戶 hytking 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!