1. 程式人生 > >Android業務元件化開發實踐(轉載)

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 一下。 詳細配置過程可以看看這篇文章: 

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