Android:關於專案模組化的設計
隨著技術越來越成熟,這兩年,元件化開發與外掛化開發的熱度一度高漲。對於元件化,有的人也喜歡稱之為模組化開發,我也比較喜歡稱之為
模組化開發
。使用模組化開發也已經有一段時間了,特此總結一下模組化開發的心得,防止以後忘記。
什麼是模組化開發
對於模組化開發的概念,有的人可能還不是很瞭解,通俗的來講就是:
- 將專案中的具體功能模組,如登入、個人中心等,拆分成一個一個單獨的
module
, - 將其中公用的功能抽離出來,形成一個個單獨的公用
module
,方便複用 - 然後像引用一些
library
專案一樣,將這些功能module
引入到一個統一的module
中,組成一個app
- 但是與這些
library
有區別的地方在於,這些被拆分成的module
app
執行
為什麼要使用模組化開發
- 隨著專案越來越龐大,專案的體積也會越來越臃腫,每次更改一下程式碼,可能就要編譯很長時間,效率得不到保證
- 一個專案可能有多個人員開發,大家在同一個專案中開發和修改程式碼,可能就會對其他人員的程式碼帶來影響和衝突
針對以上兩個問題,模組化開發,可以讓每個人手中的專案大小得到有效控制,每個人可以更加專注於自己的任務,而不用花太多精力去關心和check別人的程式碼
模組化開發的兩種模式
模組化開發目前有兩種模式 (圖是網上截的,太懶,不想畫圖 @[email protected]
)
-
submodule: 即整個專案只有一個
project
project
中構建多個功能模組module
,每個人單獨負責一個module
, 每個module
都有自己的git
倉庫,非常直觀。但是不好的地方就是,整個project
的git
分支會很複雜,且團隊協作的時候,大家都是在project
的app
模組中做測試,可能就會發生衝突。且因為所有的module
在一個project
中,每個人都可以修改他人f負責的module
,不是很安全 -
multi-project: 即將每個功能模組都拆分成一個單獨的
project
,一個project
中一般包含自己測試使用的app
模組和功能模組。這樣,每個project
都擁有自己單獨的git
倉庫,在團隊協作時,每個人都有自己的app
module
比較安全,但是專案初期時,對於程式碼規範,命名規範都必須規定好,否則最後合併成app
時可能會有衝突。
以上兩種模式都是比較常用的模式,各有優點,submodule
更直觀,開發更快速,multi-project
更解耦,更安全。大家可以根據實際情況選擇。(因為個人比較習慣於multi-project
,所以下面的有些講解可能偏向於multi-project
)
模組化開發的設計
模組化開發大致分為以下幾層:(網上一搜一堆,沒什麼大區別,主要是對每個層級的細分)
- 殼工程
- 業務元件層
- 公共元件層
(圖是網上截的,太懶,不想畫圖
@[email protected]
,將就看吧,和講的大致也差不多)
殼工程
顧名思義,殼工程
只是一個空的app module
,本身不含任何功能頁面,不做任何業務邏輯處理,可隨時替換。主要作用就是將其他業務元件
的依賴新增到自己身上,形成完整的app
業務元件層
- 業務元件層由多個業務元件組成,每個
業務元件
代表專案的一個具體功能模組,例如登入、個人中心等。 - 當業務元件中的功能需要在多個地方使用時,可以將其在細化拆分為
基礎業務元件
和商務業務元件
。 例如大眾點評中,有兩個這樣的模組酒店
和高階酒店
,這兩個模組中都用到了對於酒店搜尋
功能,但是兩個模組又不完全相同,這時可以將酒店搜尋
做成一個單獨的基礎業務元件
,在兩個模組中都呼叫酒店搜尋
元件,這兩個模組便屬於商務業務元件
公共元件層
對於 公共元件層,以前有同事喜歡直接做成一個公用的library
,比如做成一個名為lib_common
的module
,所有的 公用util、網路訪問工具、第三方庫如分享、支付等 都放在裡面,然後被 業務元件 呼叫。在專案小的時候,這也無關緊要,但是如果專案越來越大,lib_common
也可能會更著變得越來越臃腫,這就又回到了起點,最後不得不花費精力重新將lib_common
拆分。所以個人覺得不能全都將 公用部分 集中在lib_common
中,仍需劃分。
- 基礎元件層: 存放穩定的複用程式碼,不需頻繁更改,與專案幾乎沒有什麼實際關聯,作為底層存在,例如 通用的 BaseActivity、BaseDialog、通用網路訪問封裝框架、工具類等,具體是放在一個元件中還是拆分為多個元件,根據實際狀況而定
- 第三方庫元件層: 專案中難免會用到一些第三方庫,如分享、第三方登入、支付等,這些第三方庫可以獨立成一個元件或者根據複雜程度分成多個元件方便複用
- 通用元件(Common): 針對專案實際情況,抽離出公用的程式碼例如工具類形成的元件。看起來可能與 基礎元件 的作用相似,但實際上 Common元件 與 基礎元件 是有區別的,基礎元件 是穩定的,在個人看來,基礎元件 可以不僅應用於一個專案,可以在多個專案中使用而不需要做變動(隨著不斷完善,以後很可能就成為了自己公司的專案框架
^_^
。而 Common元件 是更加針對專案的實際情況的,大都會依賴於 基礎元件,並對裡面一些功能做二次封裝以適用於專案實際情況,不適合在其他專案中不做修改就使用,且會根據專案需求的改變,Common元件 也有可能做頻繁修改
業務資料互動時的處理
場景
開發中遇到業務資料互動是有大概率遇到的事,比如說專案中有兩個模組一個是景點
,一個是美食
,當你到達一個 景點 後,會給你 推薦美食,而 推薦美食 的資料肯定是從美食模組
而來。
在上述場景中,一般常見的實現就是,將獲取推薦美食資料介面
下沉到一個 公用元件 中,在 公用元件 中實現介面,然後美食
與景點
模組都呼叫 公用元件 中的這個介面,獲取資料。看似沒什麼大問題,其實還存在一些隱患。假如,專案中這種互動的行為存在更多怎麼辦?都將這些資料介面下沉到 公用元件 ,在這裡面實現嗎?這樣但增加了 公用元件 的體積,而且是危險的。因為對於 公用元件 上文已經說過,很有可能會頻繁變動,而變動可能來自不同的業務元件,那麼可能會有多個不瞭解美食
模組的開發人員參與到 公用元件 的修改,這意味著修改人員是有許可權修改這些互動資料的獲取的,這是危險的。或者還有另一種常見的實現,就是資料獲取,仍然放在美食
模組,景點
模組直接呼叫美食
模組中的程式碼,但是這增加了兩者之間的耦合。
解決
在我看來,業務元件之間是相互獨立,沒有依賴的,每個成員專注於自己的業務,無需關注其他成員的工作,且同時對自己的業務擁有絕對的掌控權,而將業務中的核心即資料處理放在公用層,是個危險的行為。那麼該如何解決呢?下面以ARouter
路由框架為例
- 利用路由框架在一個 公用元件 中註冊服務(甚至你也可以單獨建立一個 路由元件 做這件事),這裡的服務就是
ARouter
的IProvider
,IProvider
是個interface
,本身不做具體實現,非常easy和安全
// 宣告介面,其他元件通過介面來呼叫服務
public interface FoodService extends IProvider {
List<FoodBean> getFoodData(int locationCode);
}
- 依賴公用元件,在
美食
模組中實現FoodService
// 實現介面
@Route(path = "/service/food", name = "美食服務")
public class HelloServiceImpl implements HelloService {
@Override
public List<FoodBean> getFoodData(int locationCode) {
List<FoodBean> list=new ArrayList();
// 具體實現
...
return list;
}
@Override
public void init(Context context) {
}
}
- 依賴公用元件, 在
景點
模組中獲取美食資料
// 申明服務
@Autowired(name = "/service/food")
FoodService service;
// 呼叫服務
List<FoodBean> foodList = service.getFoodData(101);
關於模組化時,元件模式的切換
在開發時,開發人員有時需要元件作為一個單獨的app
執行來除錯,有時需要將元件整合到殼app
中除錯,所以需要經常在app 和 library
間切換。下面是為了應對這種情況,常見的處理方法
- 在
project
的gradle.properties
中定義一個標記
# true: 作為一個 app 單獨執行
# false: 作為一個元件
isApp=false
- 在
module
的main
目錄下新建資料夾manifest
,再在manifest
資料夾中新建app
和module
兩個資料夾,存放兩種模式下的AndroidManifest.xml
- 對於
module
的build.gradle
的配置
// 因為在 gradle.properties 定義了,所以這邊可以直接獲取
def isApp = isApp.toBoolean()
if (isApp) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
...省略
// 作為一個元件時,是沒有 applicationId 的
if(isApp){
applicationId "com.xxxxx.xxxxxx"
}
...省略
sourceSets {
// 作為 app 和 元件時的 AndroidManifest.xml 是不同的
main {
if (isApp) {
manifest.srcFile 'src/main/manifest/app/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/manifest/module/AndroidManifest.xml'
}
}
}
小提議
- 無論是使用
submodule
還是multi-project
,引入元件時,儘量將元件打包成aar
,再引入,這樣可以編譯更快 - 開發前,團隊先規定好程式碼規範與資源命名規範,因為在最終元件合併成app時,如果元件中有資源名一致的資源,會產生衝突