MVVMHabitComponent
關於Android的元件化,相信大家並不陌生,網上談論元件化的文章,多如過江之鯽,然而一篇基於MVVM模式的元件化方案卻很少。結合自身的調研和探索,在此分享一篇基於MVVMHabit框架的一套Android-Databinding元件化開發方案。文章寫的比較簡單基礎,沒有大篇幅單向技術的討論點,目的是讓學習了此方案的開發人員都可以快速上手搭建MVVM元件化專案。
原文地址: github.com/goldze/MVVM…
整體架構
1、淺談
1.1、MVVM的優勢
想必熟悉前端的朋友對MVVM非常瞭解,這種模式在目前web前端火的一塌糊塗,比如vue、angular、react都是採用MVVM設計模式實現的前端框架。而在Android開發中,MVVM並不是唯一的架構模式,最常用的可能是MVC模式(通常不是理想的實現)。比較流行的是MVP,它在某種程度上與MVVM模式非常相似。不過MVVM在MVP的基礎上更進一步的提高了開發效率,擁有了資料繫結的能力。說到Android MVVM,很自然的聯想到谷歌出的Databinding,它提供了xml與java的完美繫結,就像html與js的繫結一樣。雖然Android端的MVVM還不是很火,但我相信它是一種趨勢。趁著web前端MVVM的熱度,移動前端也應該崛起了。
1.2、元件化開發
程式碼是死的,產品是活的。在日常開發中,各種各樣頻繁變動的需求,給開發上帶來了不小的麻煩。為了儘量把程式碼寫“活”,所以出現了設計模式。但光有設計模式,還是很難滿足產品BT的需求。
對於簡單的小專案,大多都採用的是單一工程,獨立開發。由於專案不大,編譯速度及維護成本這些也在接受範圍之內。而對於做好一個App產品,這種多人合作、單一工程的App架構勢必會影響開發效率,增加專案的維護成本。每個開發者都要熟悉如此之多的程式碼,將很難進行多人協作開發,而且Android專案在編譯程式碼的時候電腦會非常卡,又因為單一工程下程式碼耦合嚴重,每修改一處程式碼後都要重新編譯打包測試,導致非常耗時,最重要的是這樣的程式碼想要做單元測試根本無從下手,所以必須要有更靈活的架構代替過去單一的工程架構。
使用元件化方案架構,高內聚,低耦合,程式碼邊界清晰,每一個元件都可以拆分出來獨立執行。所有元件寄託於宿主App,載入分離的各個元件,各自編譯自己的模組,有利於多人團隊協作開發。
1.3、MVVM模式 + 元件化
光說理論沒用,來點實際的東西,這裡要提兩個重要的框架。
- MVVMHabit:基於谷歌最新AAC架構,MVVM設計模式的一套快速開發庫,整合Okhttp+RxJava+Retrofit+Glide等主流模組,滿足日常開發需求。使用該框架可以快速開發一個高質量、易維護的Android應用。
- ARouter:阿里出的一個用於幫助 Android App 進行元件化改造的框架 —— 支援模組間的路由、通訊、解耦。
MVVMHabit + ARouter:MVVM模式 + 元件化方案,前者是設計模式,後者是方案架構,兩者並用,相得益彰。有這兩個框架作支撐,事半功倍,可快速開發元件化應用。
2、專案搭建
2.1、建立專案
先把工程中最基本的架子建立好,再一步步將其關聯起來
2.1.1、建立宿主
搭建元件化專案與單一工程專案一樣,先通過Android Studio建立一個常規專案。
File->New->New Project...
建立的這個專案將其定義為“ 宿主 ”(大多數人都是這種叫法),也可以叫空殼專案。它沒有layout,沒有activity,它的職責是將分工開發的元件合而為一,打包成一個可用的Apk。
在宿主工程中,主要包含兩個東西,一個是AndroidManifest.xml:配置application、啟動頁面等;另一個是build.gradle:負責配置構建編譯/打包引數,依賴子模組。
2.1.2、建立元件
所謂的元件,其實也就是一個Module,不過這個Module有點特殊,在合併打包的時候它是一個library:apply plugin: ‘com.android.library’,在獨立編譯執行的時候,它是一個application:apply plugin: ‘com.android.application’。
File->New->New Module->Android Library...
一般可以取名為module-xxx(元件名)
2.1.3、建立Library
除了業務元件之外,還需要建立兩個基礎Library,library-base 和 library-res。
-
library-base:存放一些公共方法、公共常量、元件通訊的契約類等。上層被所有元件依賴,下層依賴公共資源庫、圖片選擇庫、路由庫等通用庫,通過它,避免了元件直接依賴各種通用庫,承上啟下,作為整個元件化的核心庫。
-
library-res:為了緩解base庫的壓力,專門分離出一個公共資源庫,被base庫所依賴,主要存放與res相關的公共資料,比如圖片、style、anim、color等。
2.1.4、第三方框架準備
還需要準備兩個第三方的框架,即前面說的 MVVMHabit 和 ARouter,可使用遠端依賴。
MVVMHabit:
allprojects {
repositories {
...
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
複製程式碼
dependencies {
...
implementation 'com.github.goldze:MVVMHabit:?'
}
複製程式碼
ARouter:
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
複製程式碼
dependencies {
api 'com.alibaba:arouter-api:?'
annotationProcessor 'com.alibaba:arouter-compiler:?'
}
複製程式碼
2.2、元件分離
元件化其實是一個 分離--組合 的過程,分離是分離產品原型,組合是組合程式碼模組。拿到需求後,一定不要急著開幹,首先將產品原型分離成一個個子原型,分工開發後,將編寫完成的子業務模組又打包組合成一個完整的Apk。
最常見的應屬這種底部幾個tab的設計。
通過元件化,可以按照業務大致將專案拆分為:首頁模組、工作模組、訊息模組、使用者模組,當然還可以再分細一點,比如使用者模組再分離一個身份驗證模組出來。拆分的越細,複用起來就越方便。
那麼在上面2.1.2節建立元件時,則建立以下幾個元件Module:module-home、module-work、module-msg、module-user、module-sign。
2.3、元件配置
gradle是元件化的基石,想搭建好元件化專案,gradle知識一定要紮實(Android已經留下了gradle的烙印)。
2.3.1、依賴關係
專案建立好後,需要將他們串聯起來,組合在一起。依賴關係如下圖所示:
宿主依賴業務元件
dependencies {
//主業務模組
implementation project(':module-main')
//身份驗證模組
implementation project(':module-sign')
//首頁模組
implementation project(':module-home')
//工作模組
implementation project(':module-work')
//訊息模組
implementation project(':module-msg')
//使用者模組
implementation project(':module-user')
}
複製程式碼
業務元件依賴library-base
dependencies {
//元件依賴基礎庫
api project(':library-base')
//按需依賴第三方元件
}
複製程式碼
library-base依賴公共庫
dependencies {
//support相關庫
api rootProject.ext.support["design"]
api rootProject.ext.support["appcompat-v7"]
//library-res
api project(':library-res')
//MVVMHabit框架
api rootProject.ext.dependencies.MVVMHabit
//ARouter框架
api rootProject.ext.dependencies["arouter-api"]
//其他公共庫,例如圖片選擇、分享、推送等
}
複製程式碼
2.3.2、開啟dataBinding
Android MVVM模式離不開DataBinding,每個元件中都需要開啟,包括宿主App
android {
//開啟DataBinding
dataBinding {
enabled true
}
}
複製程式碼
2.3.3、模式開關
需要一個全域性變數來控制當前執行的工程是隔離狀態還是合併狀態。在gradle.properties中定義:
isBuildModule=false
複製程式碼
isBuildModule 為 true 時可以使每個元件獨立執行,false 則可以將所有元件整合到宿主 App 中。
2.3.4、debug切換
在元件的build.gradle中動態切換library與application
if (isBuildModule.toBoolean()) {
//作為獨立App應用執行
apply plugin: 'com.android.application'
} else {
//作為元件執行
apply plugin: 'com.android.library'
}
複製程式碼
當 isBuildModule 為 true 時,它是一個application,擁有自己的包名
android {
defaultConfig {
//如果是獨立模組,則使用當前元件的包名
if (isBuildModule.toBoolean()) {
applicationId 元件的包名
}
}
}
複製程式碼
2.3.5、manifest配置
元件在自己的AndroidManifest.xml各自配置,application標籤無需新增屬性,也不需要指定activity的intent-filter。當合並打包時,gradle會將每個元件的AndroidManifest合併到宿主App中。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.goldze.main">
<application>
...
</application>
</manifest>
複製程式碼
元件獨立執行時,就需要單獨的一個AndroidManifest.xml作為除錯用。可以在src/main資料夾下建立一個alone/AndroidManifest.xml。配置application標籤屬性,並指定啟動的activity。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.goldze.main">
<application
...
>
<activity
...
>
<intent-filter>
...
</intent-filter>
</activity>
</application>
</manifest>
複製程式碼
並在build.gradle中配置
android {
sourceSets {
main {
...
if (isBuildModule.toBoolean()) {
//獨立執行
manifest.srcFile 'src/main/alone/AndroidManifest.xml'
} else {
//合併到宿主
manifest.srcFile 'src/main/AndroidManifest.xml'
resources {
//正式版本時,排除alone資料夾下所有除錯檔案
exclude 'src/main/alone/*'
}
}
}
}
}
複製程式碼
2.3.6、統一資源
在元件的build.gradle配置統一資源字首
android {
//統一資源字首,規範資源引用
resourcePrefix "元件名_"
}
複製程式碼
2.3.7、配置抽取
可以將每個元件的build.gradle公共部分抽取出一個module.build.gradle
if (isBuildModule.toBoolean()) {
//作為獨立App應用執行
apply plugin: 'com.android.application'
} else {
//作為元件執行
apply plugin: 'com.android.library'
}
android {
...
defaultConfig {
...
//阿里路由框架配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
sourceSets {
main {
if (isBuildModule.toBoolean()) {
//獨立執行
manifest.srcFile 'src/main/alone/AndroidManifest.xml'
} else {
//合併到宿主
manifest.srcFile 'src/main/AndroidManifest.xml'
resources {
//正式版本時,排除alone資料夾下所有除錯檔案
exclude 'src/main/alone/*'
}
}
}
}
buildTypes {
...
}
dataBinding {
enabled true
}
}
複製程式碼
元件中引入module.build.gradle即可
apply from: "../module.build.gradle"
android {
defaultConfig {
//如果是獨立模組,則使用當前元件的包名
if (isBuildModule.toBoolean()) {
applicationId 元件的包名
}
}
//統一資源字首,規範資源引用
resourcePrefix "元件名_"
}
dependencies {
...
}
複製程式碼
2.4、完成
執行效果如下:
到此為止,一個最基本的元件化工程搭建完畢。
3、可行性方案
3.1、元件初始化
元件在獨立執行時,也就是debug期,有單獨的manifest,當然也就可以指定Application類進行初始化。那麼當元件進行合併的時,Application只能有一個,並且存在宿主App中,元件該如何進行初始化?
3.1.1、反射
反射是一種解決元件初始化的方法。
在library-base下定義一個 ModuleLifecycleConfig 單例類,主要包含兩個公共方法:initModuleAhead(先初始化)、initModuleLow(後初始化)。
為何這裡要定義兩個初始化方法?
元件多了,必定會涉及到初始化的先後順序問題,元件中依賴的第三方庫,有些庫需要儘早初始化,有些可以稍晚一些。比如ARouter的init方法,官方要求儘可能早,那麼就可以寫在library-base初始化類的onInitAhead中,優先初始化。
@Override
public boolean onInitAhead(Application application) {
KLog.init(true);
//初始化阿里路由框架
if (BuildConfig.DEBUG) {
ARouter.openLog(); // 列印日誌
ARouter.openDebug(); // 開啟除錯模式(如果在InstantRun模式下執行,必須開啟除錯模式!線上版本需要關閉,否則有安全風險)
}
ARouter.init(application); // 儘可能早,推薦在Application中初始化
return false;
}
複製程式碼
再定義一個元件生命週期管理類 ModuleLifecycleReflexs ,在這裡註冊元件初始化的類名全路徑,通過反射動態呼叫各個元件的初始化方法。
注意:元件中初始化的Module類不能被混淆
3.1.2、初始化介面
定義一個 IModuleInit 介面,動態配置Application,需要初始化的元件實現該介面,統一在宿主app的Application中初始化
public interface IModuleInit {
//初始化優先的
boolean onInitAhead(Application application);
//初始化靠後的
boolean onInitLow(Application application);
}
複製程式碼
3.1.3、初始化實現
反射類和介面都有了,那麼在各自的元件中建立一個初始化類,實現IModuleInit介面。最後在宿主的Application中呼叫初始化方法
@Override
public void onCreate() {
super.onCreate();
//初始化元件(靠前)
ModuleLifecycleConfig.getInstance().initModuleAhead(this);
//....
//初始化元件(靠後)
ModuleLifecycleConfig.getInstance().initModuleLow(this);
}
複製程式碼
最後即實現元件的初始化效果
小優化: 當元件獨立執行時,宿主App不會執行onCreate方法,但是元件業務又需要初始化單獨除錯。常規做法是元件中單獨定義Application,但這樣每個元件都需要建立一個Application,比較繁瑣。我們有了上述的初始化方法,可以在 library-base 中定義一個 DebugApplication ,debug包下的程式碼不參與編譯,僅作為獨立模組執行時初始化資料。最後記得在元件的除錯版alone/AndroidManifest下指定為base中的 DebugApplication。
3.2、元件間通訊
元件間是完全無耦合的存在,但是在實際開發中肯定會存在業務交叉的情況,該如何實現無聯絡的元件間通訊呢?
3.2.1、ARouter
ARouter 之所以作為整個元件化的核心,是因為它擁有強大的路由機制。ARouter在library-base中依賴,所有元件又依賴於library-base,所以它可以看作為元件間通訊的橋樑。
在元件A中跳轉到元件B頁面:
ARouter.getInstance()
.build(router_url)
.withString(key, value)
.navigation();
複製程式碼
在元件B頁面中接收傳過來的引數:
@Autowired(name = key)
String value;
複製程式碼
更多ARouter用法:github.com/alibaba/ARo…
3.2.2、事件匯流排(RxBus)
MVVMHabit 中提供了RxBus,可作為全域性事件的通訊工具。
當元件B頁面需要回傳資料給元件A時,可以呼叫:
_Login _login = new _Login();
RxBus.getDefault().post(_login);
複製程式碼
在元件A中註冊接收(註冊在呼叫之前完成):
subscribe = RxBus.getDefault().toObservable(_Login.class)
.subscribe(new Consumer<_Login>() {
@Override
public void accept(_Login l) throws Exception {
//登入成功後重新重新整理資料
initData();
//解除註冊
RxSubscriptions.remove(subscribe);
}
});
RxSubscriptions.add(subscribe);
複製程式碼
3.3、base規範
library-base 有兩個主要作用:一是依賴通用基礎jar或第三方框架,二是存放一些公共的靜態屬性和方法。下面列舉一些基礎通用類的約定規範。
3.3.1、config
在base的config包下面,統一存放全域性的配置檔案,比如元件生命週期初始化類:ModuleLifecycleConfig、ModuleLifecycleReflexs,網路ROOT_URL,SD卡檔案讀寫目錄等。
3.3.2、contract
RxBus元件通訊,需要經過base層,統一規範。那麼可以在contract包下面定義RxBus的契約類,寫好註釋,便於其他元件開發人員使用。
3.3.3、global
主要存放全域性的Key,比如 IntentKeyGlobal: 存放元件間頁面跳轉傳參的Key名稱; SPKeyGlobal: 全域性SharedPreferences Key 統一存放在這裡。單個元件中內部的key可以另外在單獨元件中定義。
3.3.4、router
ARouter 路由@Route註解中Path可以單獨抽取一個或者兩個RouterPath類出來,比如定義一個RouterActivityPath:
public class RouterActivityPath {
/**
* 主業務元件
*/
public static class Main {
private static final String MAIN = "/main";
/*主業務介面*/
public static final String PAGER_MAIN = MAIN +"/Main";
}
複製程式碼
Activity的路由路徑統一在此類中定義,並使用靜態內部類分塊定義各個元件中的路徑路由。
4、總結
還是得總結一下。
專案元件化,就好比製造業,生活中的絕大多數工業產品。比如汽車,由發動機、輪子、引擎等各個重要零件拼裝而成。同樣,我們的app也是由各個元件並聯起來,形成一個完整可執行的軟體。它的精髓就是這麼3點:獨立、完整、自由組合。 而且元件化甚至都不算是人類的發明。即使放在自然界,這也是早已存在的模式。想想我們人體多麼複雜,絕對不亞於windows作業系統。但除去幾個非常重要的數器官之外,大多部分損壞或缺失,我們都能活下來。這不得不說是元件化的奇蹟。
寫軟體一定要注重架構,不要等到程式碼越寫越爛,越爛越寫,最後連自己都看不下去了才想到去重構。元件化是一個很好隔離每個業務模組的方案,即使其中一個元件出了問題,也不用像單一工程那樣整體地去除錯。配合MVVM設計模式,使得我們工作中的具體專案變得更輕、好組裝、編譯構建更快,不僅提高工作效率,同時自我對移動應用開發認知有進一步的提升。元件化框架具有通用性,特別適用於業務模組迭代多,量大的大中型專案,是一個很好的解決方案。
Android架構的演進,由 模組化 到 元件化 再到 外掛化。我們在元件化開發的道路上,儘可能的完善元件開發規範,豐富元件功能庫,有一些粒度大的業務元件可以進一步的細化,對元件功能進行更單一的內聚,同時基於現有的元件化框架,便於過度在未來打造外掛化框架。
QQ交流群:84692105
如果覺得這個方案不錯的話,麻煩點個 star,你的支援則是我前進的動力!