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