Android 業務組件化開發實踐

組件化並非新話題,其實很早很早之前咱們開始爲項目解耦的時候就討論過的。但那時候咱們說的是功能組件化。好比不少公司都常見的,網絡請求模塊、登陸註冊模塊單獨拿出來,交給一個團隊開發,而在用的時候只須要接入對應模塊的功能就能夠了。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 重複依賴 
四、資源名衝突

解決 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

相關文章
相關標籤/搜索