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

Android 業務元件化開發實踐

本文原創,轉載請以連結形式註明地址:http://kymjs.com/code/2016/10/18/01

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

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

程式碼實現

注意,元件化不是外掛化,外掛化是在[執行時],而元件化是在[編譯時]。換句話說,外掛化是基於多 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 中被引入。