組件化並非新話題,其實很早很早之前咱們開始爲項目解耦的時候就討論過的。但那時候咱們說的是功能組件化。好比不少公司都常見的,網絡請求模塊、登陸註冊模塊單獨拿出來,交給一個團隊開發,而在用的時候只須要接入對應模塊的功能就能夠了。java
百牛信息技術bainiu.ltd整理髮佈於博客園android
今天咱們來討論一下業務組件化,拿出手機,打開淘寶或者大衆點評來看看,裏面的美食電影酒店外賣就是一個一個的業務。若是咱們在一個項目裏面去寫的時候,總會出現或多或少的代碼耦合,最典型的有時爲了遇上線時間而先複製粘貼一段相似的代碼過來,結果這段代碼引用的資源多是另外一個模塊獨立的資源或代碼。可是若是將一個項目做爲獨立的工程來運行,就徹底能夠避免這種狀況了。可是這並非業務組件化最大的優點,我認爲最大的優點是它大大縮減了工程結構直接下降了編譯時間。nginx
注意,組件化不是插件化,插件化是在[運行時],而組件化是在[編譯時]。換句話說,插件化是基於多 APK 的,而組件化本質上仍是隻有一個 APK。git
代碼實現上核心思路要緊記一句話:開發時是 application,發版時是 library。
來看一段 gradle 代碼:github
if (isDebug.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
很是好理解,咱們在開發的時候,module 若是是一個庫,會使用com.android.library
插件,若是是一個應用,則使用com.android.application
插件,咱們經過一個開關來控制這個狀態的切換。
而後由於咱們須要在 library 和 application 之間切換,manifest文件也須要提供兩套。
shell
你能夠根據這個項目一塊兒看:https://github.com/kymjs/Modularity瀏覽器
假設有一個項目,這個項目包含一個叫 explorer 的文件瀏覽器的模塊和一個叫 memory-box 的筆記的模塊。由於這兩個功能相對獨立,咱們將這兩個功能拆分紅兩個 module,再加上本來項目的 app module,總共三個。
在 explorer 的根目錄創建一個做爲開關的 properties 文件(寫一個全局變量也能夠,怎麼簡單怎麼來),方便用來改變當前是開發狀態仍是發版狀態(debug & release)。 從gradle中讀取這個文件中的值,來切換不一樣狀態所須要調用的配置。順便一提,當你修改了 properties 文件中的值時,必需要從新 sync 一下。 詳細配置過程能夠看看這篇文章:http://www.zjutkz.net/網絡
阿布他們的項目大量的用了 databinding 和 dagger,然而咱們項目並無用這些,用了這兩個庫的能夠看看他是怎麼爬坑的:魔都三帥架構
當你採用了組件化開發的時候,必定會遇到這幾個問題,這幾個問題除了第三個都只能規避,沒有好的處理辦法:app
一、module 中 Application 調用的問題
二、跨 module 的 Activity 或 Fragment 跳轉問題
三、AAR 或 library project 重複依賴
四、資源名衝突
因爲 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; } } }
若是單獨是 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