Android 組件化的概念大概從兩年前開始有人討論,到目前爲止,技術已經慢慢沉澱下來,愈來愈多團隊開源了本身組件化框架。本人所在團隊從去年開始調研組件化框架,在瞭解社區衆多組件化方案以後,決定自研組件化方案。爲何明明已經有不少輪子能夠用了,卻仍是決定要本身造個新輪子呢?java
主要的緣由是在調研了諸多組件化方案以後,發現儘管它們都有各自的優勢,可是依然有一些地方不是使人十分滿意。而其中最重要的一個因素就是引入組件化方案成本較高,對已有項目改造過大。我想這一點應該不少人都有相同的體會,不少時候 咱們對於項目的重構是須要與新需求的迭代同步進行的 ,幾乎很難停下來只作項目的組件化。android
另一點,我不太但願本身的項目和某一款組件化框架 強耦合。 Activity 的路由方案也好,跨模塊的同步或異步方法調用也好,我但願可以沿用項目已有的調用方式,而不是使用某款組件化框架本身特定的調用方式。例如某個接口已經基於 RxJava 封裝爲了 Observable
的接口,我就不太但願由於組件化的關係,這個接口位於另外一個模塊以後,我就不得不用這個組件化框架定義的方式去調用,我仍是但願以 RxJava 的方式去調用。git
我認爲目前想要進行組件化的項目應該能夠分爲兩類:github
不管是哪一種類型的項目,面臨的問題應該都是相似的,那就是項目大起來之後,編譯實在是太慢了。數據庫
除此之外,就是 跨模塊的功能調用很是不便 ,這個問題主要體如今上面列舉的第二種類型的項目。本人所在的項目在組件化以前就是上面列舉的第二種類型的項目,application 模塊最先用來承載業務邏輯代碼,隨着業務發展,大概是某位開發人員以爲, 「不行,這樣下去 application 模塊代碼數量會失控的」,因而後續新的業務模塊都會新開一個 library 模塊進行開發,就這樣斷斷續續目前有了大概 20+ 個 library 模塊(業務相關模塊,技術模塊不包含在內)。api
這種作法是符合軟件工程思想的,可是也帶來了一些棘手的問題,因爲 application 模塊裏的業務功能和 library 模塊裏的業務功能在邏輯地位上是平等的,因此不免會有互相調用的狀況,可是它們在項目依賴層次上卻不是處於相等的地位,application 調用 library 倒沒事,可是反過來調用就成了問題。另外,剩下這 20 + 個 library 模塊在依賴層次中也不全是屬於同一層次的,library 模塊之間互相依賴也很複雜。數組
因此我指望的組件化方案要求解決的問題很簡單:瀏覽器
基於上述的思想,咱們開發了 AppJoint 這個框架用來幫助咱們實現組件化。安全
AppJoint 是一個很是簡單有效的方案,引入 AppJoint 進行組件化全部的 API 只包含 3 個註解,加 1 個方法,這多是目前最簡單的組件化方案了,咱們的框架不追求功能要多麼複雜強大,只專一於框架自己實用、簡單與高效。並且總體實現也很是簡單,核心源碼 不到500行。bash
本人接觸最先的組件化方案是 DDComponentForAndroid,學習這個方案給了我不少啓發,在這個方案中,做者提出,能夠在 gradle.properties
中新增一個變量 isRunAlone=true
,用來控制某個業務模塊是 以 library 模塊集成到 App 的全量編譯中 仍是 以 application 模塊獨立編譯啓動 。不知道是否是不少人也受了相同的啓發,後面不少的組件化框架都是使用相似的方案:
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
複製代碼
根據我本人的實踐,這種方式有一些缺點。首先有一些開源框架在 library 模塊中和在 application 模塊中使用方法是不同的,例如 ButterKinfe , 在 application 中使用 R.id.xxx
,在 library 模塊中使用 R2.id.xxx
,若是想組件化,代碼必須保證在兩種狀況下均可用,因此基本只能拋棄 ButterKnife 了,這會給項目帶來巨大的改形成本。
除此之外,還有一些開源框架是隻能在 application 模塊中配置的,配置完之後對整個項目的全部 library 模塊都生效的,例如一些字節碼修改的框架(好比 AOP 一類的),這是一種狀況。還有一種狀況,若是原先項目已是多模塊的狀況下,可能多個模塊的初始化都是放在 application 模塊裏,由於 application 模塊是 上帝模塊,他能夠訪問到項目中任意一塊代碼,因此在這裏作初始化是最省事的。可是如今拆分爲模塊以後,由於每一個模塊須要獨立運行,因此模塊須要負責自身的初始化,但是有時候這個模塊的初始化是隻能在 application 模塊裏才能夠作的,咱們把這段邏輯下放到 library 以後,如何初始化就成了問題。
這兩種狀況,若是咱們使用 gradle.properties
中的變量來切換 application 和 library 的話,咱們勢必須要在這個模塊中維護兩套邏輯,一套是在 application 模式下的啓動邏輯,一套是在 library 模式下的啓動邏輯。原先這個模塊是專一本身自己的業務邏輯的,如今不得不爲了可以獨立做爲 application 啓動,而加入許多其餘代碼。一方面 build.gradle
文件中會充滿不少 if - else
,另外一方面 Java 源碼中也會加入許多判斷是否獨立運行的邏輯。
最終 Release App 打包時,這些模塊是做爲 library 存在的,可是咱們爲了組件化已經在這個模塊中加入了不少幫助該模塊獨立運行(以 application 模式)的代碼(例如模塊須要單獨運行,須要一個屬於這個模塊的 Laucher Activity),雖然這些代碼在線上不會生效,但是從潔癖的角度來說,這些代碼其實不該該被打包進去。其實說了這麼多無非就是想說明,若是咱們但願經過某個變量來控制模塊以 application 形式仍是以 library 形式存在,那麼咱們確定要在這個模塊中加入維護二者的差別的代碼,並且可能代碼量還很多,最後代碼呈現的狀態多是不太優雅的。
此外模塊中的 AndroidManifest.xml
也須要維護兩份:
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
複製代碼
可是 xml 畢竟不是代碼,沒有封裝繼承這些面向對象的特性,因此每當咱們增長、修改、刪除四大組件的時候,都須要記得要在兩個 AndroidManifest.xml
都作對應的修改。除了 AndroidManifest.xml
之外,資源文件也存在這個問題,雖然工做量不至於特別巨大,但這樣的作法其實已經違背了面向對象的設計原則。
最後還有一個問題,每當模塊在 application 模式和 library 模式之間進行切換的時候,都須要從新 Gradle Sync 一次,我想既然是須要組件化的項目那確定已是那種編譯速度極慢的項目了,即便是 Gradle Sync 也須要等待很多時間,這點也是咱們不太能接收的。
咱們最後是如何解決模塊的單獨編譯運行這個問題的呢?答案是 爲每一個模塊新建一個對應的 application 模塊 。也許你會對此表示懷疑:若是爲每一個業務模塊配一個用於獨立啓動的 application 模塊,那模塊會顯得特別多,項目看起來會很是的亂的。可是其實咱們能夠把全部用於獨立啓動業務模塊的 application 模塊收錄到一個目錄中:
projectRoot
+--app
+--module1
+--module2
+--standalone
| +--module1Standalone
| +--module2Standalone
複製代碼
在上面這個項目結構圖中,app
模塊是全量編譯的 application 模塊入口,module1
和 module2
是兩個業務 library 模塊, module1Standalone
和 module2Standalone
是分別使用來獨立啓動 module1
和 module2
的 2 個 application 模塊,這兩個模塊都被收錄在 standalone
文件夾下面。事實上,standalone
目錄下的模塊不多須要修改,因此這個目錄大多數狀況下是屬於摺疊狀態,不會影響整個項目結構的美觀。
這樣一來,在項目根目錄下的 settings.gradle
裏的代碼是這樣的:
// main app
include ':app'
// library modules
include ':module1'
include ':module2'
// for standalone modules
include ':standalone:module1Standalone'
include ':standalone:module2Standalone'
複製代碼
在主 App 模塊(app
模塊)的 build.gradle
文件裏,咱們只須要依賴 module1
和 module2
,兩個 standalone 模塊只和各自對應的業務模塊的獨立啓動有關,它們不須要被 app
模塊依賴,因此 app
模塊的 build.gradle
中的依賴部分代碼以下:
dependencies {
implementation project(':module1')
implementation project(':module1')
}
複製代碼
那些用於獨立運行的 application 模塊裏的 build.gradle
文件中,就只有一個依賴,那就是須要被獨立運行的 library 模塊。以 standalone/module1Standalone
爲例,它對應的 build.gradle
中的依賴爲:
dependencies {
implementation project(':module1')
}
複製代碼
在 Android Studio 中建立模塊,默認模塊是位於項目根目錄之下的,若是但願把模塊移動到某個文件夾下面,須要對模塊右鍵,選擇 "Refactor -- Move" 移動到指定目錄之下。
當咱們建立好這些 application 模塊以後,在 Android Studio 的運行小三角按鈕旁邊,就能夠選擇咱們須要運行哪一個模塊了:
這樣一來,咱們首先能夠感覺到的一點就是模塊再也不須要改 gradle.properties 文件切換 library 和 application 狀態了,也再也不須要忍受 Gradle Sync 浪費寶貴的開發時間,想全量編譯就全量編譯,想單獨啓動就單獨啓動。
因爲專門用於單獨啓動的 standalone 模塊 的存在,業務的 library 模塊只須要按本身是 library 模塊這一種狀況開發便可,不須要考慮本身會變成 application 模塊,因此不管是新開發一個業務模塊仍是從一個老的業務模塊改形成組件化形式的模塊,所要作的工做都會比以前更輕鬆。而以前提到的,爲了讓業務模塊單獨啓動所須要的配置、初始化工做均可以放到 standalone 模塊 裏,而且不用擔憂這些代碼被打包到最終 Release 的 App 中,前面例子中提到的用來使模塊單獨啓動的 Launcher Activity,只要把它放到 standalone 模塊 模塊便可。
AndroidManifest.xml
和資源文件的維護也變輕鬆了。四大組件的增刪改只須要在業務的 library 模塊修改便可,不須要維護兩份 AndroidManifest.xml
了,standalone 模塊 裏的 AndroidManifest.xml
只須要包含模塊獨立啓動時和 library 模塊中的 AndroidManifest.xml
不一樣的地方便可(例如 Launcher Activity 、圖標等),編譯工具會自動完成兩個文件的 merge。
推薦在 standalone 模塊 內指定一個不一樣於主 App 的 applicationId,即模塊單獨啓動的 App 與主 App 能夠在手機內共存。
咱們分析一下這個方案,和原先的比,首先缺點是,引入了不少新的 standalone 模塊,項目彷佛變複雜了。可是優勢也是明顯的,組件化的邏輯更加清晰,尤爲是在老項目改造狀況下,所須要付出的工做量更少,並且不須要在開發期間頻繁 Gradle Sync。 總的來講,改造後的組件化項目更符合軟件工程的設計原則,尤爲是開閉原則(open for extension, but closed for modification)。
介紹到這裏爲止,咱們尚未使用任何 AppJoint 的 API,咱們之因此沒有藉助任何組件化框架的 API 來實現模塊的獨立啓動,是由於本文一開始提出的,咱們不但願項目和任何組件化框架強綁定, 包括 AppJoint 框架自己,AppJoint 框架自己的設計是與項目鬆耦合的,因此使用了 AppJoint 框架進行組件化的項目,若是從此但願能夠切換到其它更優秀的組件化方案,理論上是很輕鬆的。
在組件化以前,咱們經常把項目中須要在啓動時完成的初始化行爲,放在自定義的 Application
中,根據本人的項目經驗,初始化行爲能夠分爲如下兩類:
咱們在上一步中,爲每一個業務模塊創建了獨立運行的 standalone 模塊 ,可是此時還並不能把業務模塊獨立啓動起來,由於模塊的初始化工做並無完成。咱們在前面介紹 AppJoint 的設計思想的時候,曾經說過咱們但願組件化方案最好 『不要有太多的學習成本,沿用目前已有的開發方式』,因此這裏咱們的解決方案是,在每一個業務模塊裏新建一個自定義的 Application
類,用來實現該業務模塊的初始化邏輯,這裏以在 module1
中新建自定義 Application
爲例:
@ModuleSpec
public class Module1Application extends Application {
@Override
public void onCreate() {
super.onCreate();
// do module1 initialization
Log.i("module1", "module1 init is called");
}
}
複製代碼
如上面的代碼所示,咱們在 module1
中新建一個自定義的 Application
類,名爲 Module1Application
。那咱們是否是應該把與這個模塊有關的全部初始化邏輯都放在這個類裏面呢?並不徹底是這樣。
首先,對於前面提到的當前模塊的 業務相關的初始化 ,毫無疑問應該放在這個 Module1Application
類中,可是針對前面提到的該模塊的 與業務無關的技術組件的初始化 放在這裏就不是很合適了。
首先,從邏輯上考慮,業務無關的技術組件的初始化應該放在一個統一的地方,把它們放在主 App 的自定義 Application
類中比較合適,若是每一個模塊爲了本身能夠獨立編譯運行,都要本身初始化一遍,那麼全部代碼最後一塊兒全量編譯的時候,這些初始化行爲就會在代碼中出現好幾回,這樣既不合理,也可能會形成潛在問題。
那麼,若是咱們在 Module1Application
中作判斷,若是它自身處於獨立編譯運行狀態,就執行技術組件的初始化,反之,若它處於全量編譯運行狀態中,就不執行技術組件的初始化,由主 App 的 Application
來實現這些邏輯,這樣是否能夠呢?理論上這種方案可行,可是這麼作就會遇到和前面提到的 『在 gradle.properties
中維護一個變量來控制模塊是否獨立編譯』一樣的問題,咱們不但願把和業務無關的邏輯(用於業務模塊獨立啓動的邏輯)打包進最終 Release 的 App。
那應該如何解決這個問題呢?解決方案和前面一小節相似,咱們不是爲 module1
模塊準備了一個 module1Standalone
模塊嗎?既然技術相關的組件的初始化並非 module1
模塊的核心,只和 module1
模塊的獨立啓動有關,那麼放在 module1Standalone
模塊裏是最合適的,由於這個模塊只會在 module1
的獨立編譯運行中使用到,它的任何代碼都不會被打包到最終 Release 的 App 中。咱們能夠在 module1Standalone
中定義一個 Module1StandaloneApplication
類,它從 Module1Application
繼承下來:
public class Module1StandaloneApplication extends Module1Application {
@Override
public void onCreate() {
// module1 init inside super.onCreate()
super.onCreate();
// initialization only used for running module1 standalone
Log.i("module1Standalone", "module1Standalone init is called");
}
}
複製代碼
而且咱們在 module1Standalone
模塊的 AndroidManifest.xml
中把 Module1StandaloneApplication
設置爲 Standalone App 使用的自定義 Application
類:
<application android:icon="@mipmap/module1_launcher" android:label="@string/module1_app_name" android:theme="@style/AppTheme" android:name=".Module1StandaloneApplication">
<activity android:name=".Module1MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
複製代碼
在上面的代碼中,咱們除了設置了自定義的
Application
之外,還設置了一個 Launcher Activity (Module1MainActivity
),這個Activity
即爲模塊的啓動Activity
,因爲它只存在於模塊的獨立編譯運行期間,App 全量打包時是不包含這個Module1MainActivity
的,因此咱們能夠在裏面定義一些方便模塊獨立調試的功能,例如快速前往某個頁面以及建立 Mock 數據。
這樣,只要咱們單獨運行 module1Standalone
這個模塊的時候,使用的 Application
類就是 Module1StandaloneApplication
。在開發時,咱們須要單獨調試 module1
時,咱們只須要啓動 module1Standalone
這個模塊進行調試便可;而在 App 須要全量編譯時,咱們則正常啓動原來的主 App 。不管是哪一種狀況, module1
這個模塊始終是以 library
形式存在的,這意味着,若是咱們但願把原先的業務模塊改形成組件化模塊,須要的改造量縮小不少,咱們改造的過程主要是在 增長代碼,而不是 修改代碼,這點符合軟件工程中的『開閉原則』。
寫到這裏,咱們其實還有一個問題沒有解決。Module1Application
目前除了被 Module1StandaloneApplication
繼承之外,沒有被任何其它地方引用到。您可能會有疑問:那咱們如何保證 App 全量編譯運行時,Module1Application
裏的初始化邏輯會被調用到呢?細心的您可能早就已經發現了:咱們在上面定義 Module1Application
時,同時標記了一個註解 @ModuleSpec
:
@ModuleSpec
public class Module1Application extends Application {
...
}
複製代碼
這個註解的做用是告知 AppJoint 框架,咱們須要確保當前模塊該 Application
中的初始化行爲,可以在最終全量編譯時,被主 App 的 Application
類調用到。因此對應的,咱們的主 App 模塊(app
模塊)的自定義 Application
類也須要被一個註解 -- AppSpec
標記,代碼以下所示:
@AppSpec
public class App extends Application {
...
}
複製代碼
上面代碼中的 App
爲主 App 對應的自定義 Application
類,咱們給這個類上方標記了 @AppSpec
註解,這樣系統在執行 App
自身初始化的同時會一併執行這些子模塊的 Application
裏對應聲明週期的初始化。即:
App
執行 onCreate
方法時,保證也同時執行 Module1Application
和 Module2Application
的 onCreate
方法 。App
執行 attachBaseContext
方法時,保證也同時執行 Module1Application
和 Module2Application
的 attachBaseContext
方法。App
執行某個生命週期方法時,保證子模塊的 Application
的對應的生命週期方法也會被執行。這樣,咱們經過 AppJoint 的 @ModuleSpec
和 @AppSpec
兩個註解,在主 App 的 Application
和子模塊的 Application
之間創建了聯繫,保證了在全量編譯運行時,全部業務模塊的初始化行爲都能被保證執行。
到這裏爲止,咱們已經處理好了業務模塊在 獨立編譯運行模式 和 全量編譯運行模式 這兩種狀況下的初始化問題,目前關於 Application
還有一個潛在問題,咱們的項目在組件化以前,咱們常常會在 Applictaion
類的 onCreate
週期保存當前 Appliction
的引用,而後在應用的任何地方均可以使用這個 Application
對象,例以下面這樣:
public class App extends Application {
public static App INSTANCE;
@Override
public void onCreate() {
super.onCreate();
INSTANCE = this;
}
}
複製代碼
這麼處理以後,咱們能夠在項目任意位置經過 App.INSTANCE
使用 Application Context 對象。可是,如今組件化改造之後,以 module1
爲例,在獨立運行模式時,應用的 Application
對象是 Module1StandaloneApplication
的實例,而在全量編譯運行模式時,應用的 Application
對象是主 App 模塊的 App
的實例,咱們如何能像以前同樣,作到在項目中任何一個地方都能獲取到當前使用的 Application
實例呢?
咱們能夠把項目中全部自定義 Application
內部保存的自身的 Application
實例的類型,從具體的自定義類,改成標準的 Application
類型,以 Module1Application
爲例:
@ModuleSpec
public class Module1Application extends Application {
public static Application INSTANCE;
@Override
public void onCreate() {
super.onCreate();
INSTANCE = (Application)getApplicationContext()
// do module1 initialization
Log.i("module1", "module1 init is called");
}
}
複製代碼
咱們能夠看到,若是按原來的寫法, INSTANCE
的類型通常是具體的自定義類型 Module1Application
,如今咱們改爲了 Application
。同時 onCreate
方法裏爲 INSTANCE
賦值的語句再也不是 INSTANCE = this
,而是 INSTANCE = (Application)getApplicationContext()
。這樣處理之後,就能夠保證 module1
裏面的代碼,不管是在 App 全量編譯模式下,仍是獨立編譯調試模式下,均可以經過 Module1Application.INSTANCE
訪問當前的 Application
實例。這是因爲 AppJoint 框架 保證了當主 App 的 App
對象被調用 attachBaseContext
回調時,全部組件化業務模塊的 Application
也會被調用 attachBaseContext
這個回調。
這樣,咱們在 module1
這個模塊裏的任何位置使用 Module1Application.INSTANCE
總能正確地得到 Application
的實例。對應的,咱們使用相同的方法在 module2
這個模塊裏,也能夠在任何位置使用 Module2Application.INSTANCE
正確地得到 Application
的實例,而不須要知道當前處於獨立編譯運行狀態仍是全量編譯運行狀態。
必定不要 依賴某個業務模塊自身定義的
Application
類的實例(例如Module1Application
的實例),由於在運行時真正使用的Application
實例可能不是它。
咱們已經解決業務模塊在 單獨編譯運行模式 下和在 App 全量編譯模式 下,初始化邏輯應該如何組織的問題。咱們沿用了咱們熟悉的自定義 Application
方案,來承載各個模塊的初始化行爲,同時利用 AppJoint 這個膠水,把每一個模塊的初始化邏輯集成到最終全量編譯的 App 中。而這一切和 AppJoint 有關的 API 僅僅是兩個註解,這裏很好的說明了 AppJoint 是個學習成本低的工具,咱們能夠沿用咱們已有的開發方式而不是改造咱們原有的代碼邏輯致使項目和組件化框架形成過分耦合。
雖然目前每一個模塊已經有獨立編譯運行的可能了,可是開發一個成熟的 App 咱們還有一個重要的問題沒有解決,那就是跨模塊的方法調用。由於咱們的業務模塊不管是從業務邏輯上考慮仍是從在依賴樹上的位置考慮,都應該是具備同等的地位的,體如今依賴層次上,這些業務模塊應該是平級的,且互相之間沒有依賴:
上圖是咱們比較理想狀況下的組件化的最終狀態,App
模塊不承載任何業務邏輯,它的做用僅僅是做爲一個 application
殼把 Module1
~ Module(n)
這個 n 個模塊的功能都集成在一塊兒成爲一個完整的 App。Module1
~ Module(n)
這 n 個模塊互相之間不存在任何交叉依賴,它們各自僅包含各自的業務邏輯。這種方式雖然完成了業務模塊之間的解耦,可是給咱們帶來的新的挑戰:業務模塊之間互相調用彼此的功能是很是常見且合理的需求,可是因爲這些模塊在依賴層次上位於同一層次,因此顯然是沒法直接調用的。
此外,上圖的這種形態是組件化的最終的理想狀態,若是咱們要將項目改造以達到這種狀態,毫無疑問須要付出巨大的時間成本。在業務快速迭代期間,這是咱們沒法承擔的成本,咱們只能逐漸地改造項目,也就是說,App
模塊內的業務代碼是被逐漸拆解出來造成新的獨立模塊的,這意味着在組件化過程的至關長一段時間內,App
內仍是存在業務代碼的,而被拆解出來的模塊內的業務邏輯代碼,是有可能調用到 App
模塊內的代碼的。這是一種很尷尬的狀態,在依賴層次中,位於依賴層次較低位置的代碼反而要去調用依賴層次較高位置的代碼。
針對這種狀況,咱們比較容易想到,咱們再新建一個模塊,例如 router
模塊,咱們在這個模塊內定義 全部業務模塊但願暴露給其它模塊調用的方法,以下圖:
projectRoot
+--app
+--module1
+--module2
+--standalone
+--router
| +--main
| | +--java
| | | +--com.yourPackage
| | | | +--AppRouter.java
| | | | +--Module1Router.java
| | | | +--Module2Router.java
複製代碼
在上面的項目結構層次中,咱們在新建的 router
模塊下定義了 3 個 接口:
AppRouter
接口聲明瞭 app
模塊暴露給 module1
、module2
的方法的定義。Module1Router
接口聲明瞭 module1
模塊暴露給 app
、module2
的方法的定義。Module2Router
接口聲明瞭 module2
模塊暴露給 module1
、app
的方法的定義。以 AppRouter
接口文件爲例,這個接口的定義以下:
public interface AppRouter {
/** * 普通的同步方法調用 */
String syncMethodOfApp();
/** * 以 RxJava 形式封裝的異步方法 */
Observable<String> asyncMethod1OfApp();
/** * 以 Callback 形式封裝的異步方法 */
void asyncMethod2OfApp(Callback<String> callback);
}
複製代碼
咱們在 AppRouter
這個接口內定義了 1 個同步方法,2 個異步方法,這些方法是 app
模塊須要暴露給 module1
、 module2
的方法,同時 app
模塊自身也須要提供這個接口的實現,因此首先咱們須要在 app
、module1
、module2
這三個模塊的 build.gradle
文件中依賴 router
這個模塊:
dependencies {
// Other dependencies
...
api project(":router")
}
複製代碼
這裏依賴
router
模塊的方式是使用api
而不是implementation
是爲了把router
模塊的信息暴露給依賴了這些業務模塊的 standalone 模塊,app
模塊因爲沒有別的模塊依賴它,不受上面所說的限制,能夠寫成implementation
依賴。
而後咱們回到 app
模塊,爲剛剛在 router
定義的 AppRouter
接口提供一個實現:
@ServiceProvider
public class AppRouterImpl implements AppRouter {
@Override
public String syncMethodOfApp() {
return "syncMethodResult";
}
@Override
public Observable<String> asyncMethod1OfApp() {
return Observable.just("asyncMethod1Result");
}
@Override
public void asyncMethod2OfApp(final Callback<String> callback) {
new Thread(new Runnable() {
@Override
public void run() {
callback.onResult("asyncMethod2Result");
}
}).start();
}
}
複製代碼
咱們能夠發現,咱們把 app
模塊內的方法暴露給其它模塊的方式和咱們平時寫代碼並無什麼不一樣,就是聲明一個接口提供給其它模塊,同時在本身內部編寫一個這個接口的實現類。不管是同步仍是異步,不管是 Callback 的方式,仍是 RxJava 的方式,均可以使用咱們原有的開發方式。惟一的區別就是,咱們在 AppRouterImpl
實現類上方標記了一個 @ServiceProvider
註解,這個註解的做用是用來通知 AppJoint
框架在 AppRouter
和 AppRouterImpl
之間創建聯繫,這樣其它模塊就能夠經過 AppJoint
找到一個 AppRouter
的實例並調用裏面的方法了。
假設如今 module1
中須要調用 app
模塊中的 asyncMethod1OfApp
方法,因爲 app
模塊已經把這個方法聲明在了 router
模塊的 AppRouter
接口中了,module1
因爲也依賴了 router
模塊,因此 module1
內能夠訪問到 AppRouter
這個接口,可是卻訪問不到 AppRouterImpl
這個實現類,由於這個類定義在 app
模塊內,這時候咱們可使用 AppJoint 來幫助 module1
獲取 AppRouter
的實例:
AppRouter appRouter = AppJoint.service(AppRouter.class);
// 得到同步調用的結果
String syncResult = appRouter.syncMethodOfApp();
// 發起異步調用
appRouter.asyncMethod1OfApp()
.subscribe((result) -> {
// handle asyncResult
});
// 發起異步調用
appRouter.asyncMethod2OfApp(new Callback<String>() {
@Override
public void onResult(String data) {
// handle asyncResult
}
});
複製代碼
在上面的代碼中,咱們能夠看到,除了第一步獲取 AppRouter
接口的實例咱們用到了 AppJoint 的 API AppJoint.service
之外,剩下的代碼,module1
調用 app
模塊內的方法的方式,和咱們原來的開發方式沒有任何區別。AppJoint.service
就是 AppJoint 全部 API 裏惟一的那個方法。
也就是說,若是一個模塊須要提供方法供其餘模塊調用,須要作如下步驟:
router
模塊中@ServiceProvider
註解完成這兩步之後就能夠在其它模塊中使用如下方式獲取該模塊聲明的接口的實例,並調用裏面的方法:
AppRouter appRouter = AppJoint.service(AppRouter.class);
Module1Router module1Router = AppJoint.service(Module1Router.class);
Module2Router module2Router = AppJoint.service(Module2Router.class);
複製代碼
這種方法不只僅能夠保證處於相同依賴層次的業務模塊能夠互相調用彼此的方法,還能夠支持從業務模塊中調用 app
模塊內的方法。這樣就能夠 保證咱們組件化的過程能夠是漸進的 ,咱們不須要一口氣把 app
模塊中的全部功能所有拆分到各個業務模塊中,咱們能夠逐漸地把功能拆分出來,以保證咱們的業務迭代和組件化改造同時進行。當咱們的 AppRouter
裏面的方法愈來愈少直到最後能夠把這個類從項目中安全刪除的時候,咱們的組件化改造就完成了。
上面一個小結中咱們已經介紹了使用 AppJoint 在 App 全量編譯運行期間,業務模塊之間跨模塊方法調用的解決方案。在全量編譯期間,咱們能夠經過 AppJoint.service
這個方法找到指定模塊提供的接口的實例,可是在模塊單獨編譯運行期間,其它的模塊是不參與編譯的,它們的代碼也不會打包進用於模塊獨立運行的 standalaone 模塊,咱們如何解決在模塊單獨編譯運行模式下,跨模塊調用的代碼依然有效呢?
以 module1
爲例,首先爲了便於在 module1
內部任何地方均可以調用其它模塊的方法,咱們建立一個 RouterServices
類用於存放其它模塊的接口的實例:
public class RouterServices {
// app 模塊對外暴露的接口
public static AppRouter sAppRouter = AppJoint.service(AppRouter.class);
// module2 模塊對外暴露的接口
public static Module2Router sModule2Router = AppJoint.service(Module2Router.class);
}
複製代碼
有了這個類之後,咱們在 module1
內部若是須要調用其它模塊的功能,咱們只須要使用 RouterServices.sAppRouter
和 RouterServices.sModule2Router
這兩個對象就能夠了。可是若是是剛剛提到的 module1
獨立編譯運行的狀況,即啓動的 application
模塊是 module1Standalone
, 那麼 RouterServices.sAppRouter
和 RouterServices.sModule2Router
這兩個對象的值均爲 null
,這是由於 app
和 module2
這兩個模塊此時是沒有被編譯進來的。
若是咱們須要在這種狀況下保證已有的 module1
內部的經過 RouterServices.sAppRouter
和 RouterServices.sModule2Router
進行跨模塊方法調用的代碼依然能工做,咱們就須要對這兩個引用手動賦值,即咱們須要建立 Mock 了 AppRouter
和 Module2Router
功能的類。這些類因爲只對 module1
的獨立編譯運行有意義,因此這些類最合適的位置是放在 module1Standalone
這個模塊內,以 AppRouter
的 Mock 類 AppRouterMock
爲例:
public class AppRouterMock implements AppRouter {
@Override
public String syncMethodOfApp() {
return "mockSyncMethodOfApp";
}
@Override
public Observable<String> asyncMethod1OfApp() {
return Observable.just("mockAsyncMethod1OfApp");
}
@Override
public void asyncMethod2OfApp(final Callback<String> callback) {
new Thread(new Runnable() {
@Override
public void run() {
callback.onResult("mockAsyncMethod2Result");
}
}).start();
}
}
複製代碼
已經建立好了 Mock 類,接下來咱們要作的是,在 module1
獨立編譯運行的模式下,用 Mock 類的對象,去替換 RouterServices
裏面的對應的引用,因爲這些邏輯只和 module1
的獨立編譯運行有關,咱們不但願這些邏輯被打包進真正 Release 的 App 中,那麼最合適的地方就是 Module1StandaloneApplication
裏了:
public class Module1StandaloneApplication extends Module1Application {
@Override
public void onCreate() {
// module1 init inside super.onCreate()
super.onCreate();
// initialization only used for running module1 standalone
Log.i("module1Standalone", "module1Standalone init is called");
// Replace instances inside RouterServices
RouterServices.sAppRouter = new AppRouterMock();
RouterServices.sModule2Router = new Module2RouterMock();
}
}
複製代碼
有了上面的初始化動做之後,咱們就能夠在 module1
內部安全地使用 RouterServices.sAppRouter
和 RouterServices.sModule2Router
這兩個對象進行跨模塊的方法調用了,不管當前是處於 App 全量編譯模式仍是 modul1Standalone
獨立編譯運行模式。
在組件化改造過程當中,除了跨模塊的方法調用以外,跨模塊啓動 Activity 和跨模塊引用 Fragment 也是咱們常常遇到的需求。目前社區中大多數組件化方案都是使用自定義私有協議,使用 URL-Scheme 的方式來實現跨模塊 Activity 的啓動,這一塊已經有不少成熟的方案了,有的組件化方案直接推薦使用 ARouter 來實現這塊功能。可是 AppJoint 沒有使用這類方案。
本文開頭曾經介紹過,AppJoint 全部的 API 只包含 3 個註解加 1 個方法,而這些 API 咱們在前文中已經都介紹完了,也就是說,咱們沒有提供專門的 API 來實現跨模塊的 Activity / Fragment 調用。
咱們回想一下,在沒有實現組件化時,咱們啓動 Activity 的推薦寫法以下,首先在被啓動的 Activity 內實現一個靜態 start
方法:
public class MyActivity extends AppCompatActivity {
public static void start(Context context, String param1, Integer param2) {
Intent intent = new Intent(context, MyActivity.class);
intent.putExtra("param1", param1);
intent.putExtra("param2", param2);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
}
複製代碼
而後咱們若是在其它 Activity 中啓動這個 MyActivity
的話,寫法以下:
MyActivity.start(param1, param2);
複製代碼
這裏的思想是,服務的提供者應該把複雜的邏輯放在本身這裏,而只提供給調用者一個簡單的接口,用這個簡單的接口隔離具體實現的複雜性,這是符合軟件工程思想的。
那麼若是目前 module1
模塊中有一個 Module1Activity
,如今這個 Activity 但願可以從 module2
啓動,應該如何寫呢?首先,在 router
模塊的 Module1Router
內聲明啓動 Module1Activity
的方法:
public interface Module1Router {
...
// 啓動 Module1Activity
void startModule1Activity(Context context);
}
複製代碼
而後在 module1
模塊裏 Module1Router
對應的實現類 Module1RouterImpl
中實現剛剛定義的方法:
@ServiceProvider
public class Module1RouterImpl implements Module1Router {
...
@Override
public void startModule1Activity(Context context) {
Intent intent = new Intent(context, Module1Activity.class);
context.startActivity(intent);
}
}
複製代碼
這樣, module2
中就能夠經過下面的方式啓動 module1
中的 Module1Activity
了。
RouterServices.sModule1Router.startModule1Activity(context);
複製代碼
跨模塊獲取 Fragment
實例也是相似的方法,咱們在 Module1Router
裏繼續聲明方法:
public interface Module1Router {
...
// 啓動 Module1Activity
void startModule1Activity(Context context);
// 獲取 Module1Fragment
Fragment obtainModule1Fragment();
}
複製代碼
差很少的寫法,咱們只要在 Module1RouterImpl
裏接着實現方法便可:
@ServiceProvider
public class Module1RouterImpl implements Module1Router {
@Override
public void startModule1Activity(Context context) {
Intent intent = new Intent(context, Module1Activity.class);
context.startActivity(intent);
}
@Override
public Fragment obtainModule1Fragment() {
Fragment fragment = new Module1Fragment();
Bundle bundle = new Bundle();
bundle.putString("param1", "value1");
bundle.putString("param2", "value2");
fragment.setArguments(bundle);
return fragment;
}
}
複製代碼
前面提到過,目前社區大多數組件化方案都是使用 自定義私有協議,利用 URL-Scheme 的方式來實現跨模塊頁面跳轉 的,即相似 ARouter 的那種方案,爲何 AppJoint 不採用這種方案呢?
緣由其實很簡單,假設項目中沒有組件化的需求,咱們在同一個模塊內進行 Activity 的跳轉,確定不會採用 URL-Scheme 方式進行跳轉,咱們確定是本身建立 Intent
進行跳轉的。其實說到底,使用 URL-Scheme 進行跳轉是 不得已而爲之,它只是手段,不是目的,由於在組件化以後,模塊之間彼此的 Activity 變得不可見了,因此咱們轉而使用 URL-Scheme 的方式進行跳轉。
如今 AppJoint 從新支持了使用代碼進行跳轉,只須要把跳轉的邏輯抽象爲接口中的方法暴露給其它模塊,其它模塊就能夠調用這個方法實現跳轉邏輯。除此之外,使用接口提供跳轉邏輯相比 URL-Scheme 方式還有什麼優點呢?
類型安全。充分利用 Java 這種靜態類型語言的編譯器檢查功能,經過接口暴露的跳轉方法,不管是傳參仍是返回值,若是類型錯誤,在編譯期間就能發現錯誤,而使用 URL-Scheme 進行跳轉,若是發生類型上的錯誤,只能在運行期間才能發現錯誤。
效率高。即便是使用 URL-Scheme 進行跳轉,底層仍然是構造 Intent
進行跳轉,可是卻額外引入了對跳轉 URL 進行構造和解析的過程,涉及到額外的序列化和反序列化邏輯,下降了代碼的執行效率。而使用接口提供的跳轉邏輯,咱們直接構造 Intent
進行跳轉,不涉及到任何額外的序列化和反序列化操做,和咱們平常的 Activity 跳轉邏輯執行效率相同。
IDE 友好。使用 URL-Scheme 進行跳轉,IDE 沒法提供任何智能提示,只能依靠完善的文檔或者開發者自身檢查來確保跳轉邏輯的正確性,而經過接口提供跳轉邏輯能夠最大限度發揮 IDE 的智能提示功能,確保咱們的跳轉邏輯是正確的。
易於重構。使用 URL-Scheme 進行跳轉,若是遇到跳轉邏輯須要重構的狀況,例如 Activity 名字的修改,參數名稱的修改,參數數量的增刪,只能依靠開發者對使用到跳轉邏輯的地方一個一個修改,並且沒法確保所有都修改正確了,由於編譯器沒法幫咱們檢查。而經過接口提供的跳轉邏輯代碼須要重構時,編譯器能夠自動幫助咱們檢查,一旦有地方沒有改對,直接在編譯期報錯,並且 IDE 都提供了智能重構的功能,咱們能夠方便地對接口中定義的方法進行重構。
學習成本低。咱們能夠沿用咱們熟悉的開發方式,不須要去學習 URL-Scheme 跳轉框架的 API。這樣還能夠保證咱們的跳轉邏輯不與具體的框架強綁定,咱們經過接口隔離了跳轉邏輯的真正實現,即便使用 AppJoint 進行跳轉,咱們也能夠在隨時把跳轉邏輯切換到其餘方案,包括 URL-Scheme 方式。
我我的的實踐,目前項目中同一進程內的頁面跳轉已經所有由 AppJoint 的方式實現,目前只有跨進程的頁面啓動交給了 URL-Scheme 這種方式(例如從瀏覽器喚醒 App 某個頁面)。
最後再提一點,因爲跨模塊啓動 Activity 沿用了跨模塊方法調用的開發方式,在業務模塊單獨編譯運行模式下,咱們也須要 Mock 這些啓動方法。既然咱們是在獨立調試某個業務模塊,咱們確定不是真的但願跳轉到那些頁面,咱們在 Mock 方法裏直接輸出 Log 或者 Toast 便可。
到這裏爲止,使用 AppJoint 進行組件化的介紹就已經結束了。AppJoint 的 Github 地址爲:github.com/PrototypeZ/… 。核心代碼不超過 500 行,您徹底能夠快速掌握這個工具加速您的組件化開發,只要 Fork 一份代碼便可。若是您不想本身引入工程,咱們也提供了一個開箱即用的版本,您能夠直接經過 Gradle 引入。
build.gradle
文件中添加 AppJoint插件 依賴:buildscript {
...
dependencies {
...
classpath 'io.github.prototypez:app-joint:{latest_version}'
}
}
複製代碼
dependencies {
...
implementation "io.github.prototypez:app-joint-core:{latest_version}"
}
複製代碼
apply plugin: 'com.android.application'
apply plugin: 'app-joint'
複製代碼
經過本文的介紹,咱們其實能夠發現 AppJoint 是個思想很簡單的組件化方案。雖然簡單,可是卻直接並且夠用,儘管沒有像其它的組件化方案那樣提供了各類各樣強大的 API,可是卻足以勝任大多數中小型項目,這是咱們一以貫之的設計理念。
若是您感受這個項目對您有幫助,但願能夠點一個 Star ,謝謝 : ) 。文章很長,感謝您耐心讀完。因爲本人能力有限,文章可能存在紕漏的地方,歡迎各位指正,再次謝謝你們!
若是您對個人技術分享感興趣,歡迎關注個人我的公衆號:麻瓜日記,不按期更新原創技術分享,謝謝!:)