標籤:

Android項目組件化架構

Android項目組件化架構

4 人贊了文章

前言

用android studio開發的同學應該都知道,androidstudio的架構是project-module形式,也就可以理解為一個項目由多個模塊組成。在剛接觸android studio時,它的這種架構引出了我一個想法------我們的app也可以使用這樣的架構,一個app由多個模塊組成,各個模塊在自己的module包里。

例如我有一個資訊類app,有幾大模塊:首頁、視頻、我,那麼我們構建項目時,就可以分開三個module來構建這幾大模塊,而不是把這幾大模塊都放在一個module裡面。這個想法我很早就有,也付出實踐,但是初期的android studio效率非常差,以此想法來構建項目,在編譯時非常耗時,幾分鐘,十幾分鐘,半個小時,甚至緩不過來,一直在那轉圈,於是我就此作罷。

再到後來,有個新名詞出現了,叫「組件化架構」,翻閱了一些相關文章,正是我之前研究的東西,不過加入了一些更好更完善的思想。這裡我結合大牛們的思想和我自己的一些思想,來談一下項目的「組件化架構」。

初始模型

我以一個實際例子來一邊講解一邊搭建項目結構,就以我們上面舉的資訊類app例子,它包含三個業務模塊:首頁、視頻、我。

那麼我們的項目初始架構會是這樣的:

相信看完這個圖,你腦海里會馬上浮現兩個疑問:app外殼是什麼?為什麼又多了個main組件,這個傢伙用來幹什麼的?

這裡我們可以做一個比喻,把我們的app比喻成一個電腦主機,那麼app外殼就是主機外殼,main組件就是主板,其他各個組件就類似於網卡、顯卡之類的東西,各個組件連接到主板上,安裝在主機殼中,對外展示為一個單一的電腦主機。在我們的項目中,實際展示出來的效果應該是這樣的:

或者我們可以用另外一種目錄結構來看,可能更清晰

這裡需要注意一下,main組件的gradle文件中,apply plugin使用的是com.android.application

其他業務模塊,使用的是com.android.library

因為在最終的發布版中,其他業務組件是集成到main組件中的,main組件編譯生成最終的application,或者可以這麼理解,main組件和app外殼是我們app的必備組成部分,一起構成了可對外發布的完整app,其他組件可以以集成進來,也可以不集成進來,只會增加或者減少我們app的功能,但不影響我們app的最終發布。所以我們在新增module的時候,選擇的類型應該是library。

各個組件都建立完成之後,接下來可以把組件集成到main組件中,集成非常簡單,只需在main組件的gradle文件中,添加如下語句

dependencies{ …… compile project(:home) compile project(:personal) compile project(:video) }

接下來做些有實際效果的事情,給各個組件寫一個頁面,再集成到我們的main組件中展示。

實現非常簡單,就是在三大業務組件中,分別定義一個fragment,然後在main組件中,把這些fragment實例化載入到主頁面就行。由於我們前面已經把三大業務組件都引入到了main組件中,所以編碼和在同一個module中的編碼是一樣的,不需要做特別處理。

到此就實現了「組件化架構」了么?當然沒那麼簡單,目前只是非常簡單的集成。「組件化」不僅僅是把各個功能模塊分開,還有模塊之間如何通信,以及「組件化」的一大亮點------各個組件可以單獨開發。

各組件單獨開發

前面我們把各個組件集成到到main組件,現在我們把組件拆出來,單獨開發,開發完後,可以再把組件集成到main中,發布。這是組件化開發的最大亮點(優點)。

這裡我們把home組件單獨出來開發。第一步,需要把其library模式改為application模式,因為只有application才可以單獨運行,library必須依靠application才能運行。所以,接下來,我們就要在home所對應的gradle文件中做修改。

修改成

等要集成到main組件時,又得改回來,如果這樣子手工去改,組件一多,修改起來麻煩,也不優雅。優雅的解決辦法就是,設置一個開關,打開時,就是application模式,可以單獨開發,關閉時,就是library模式,可以集成到main組件中。

在項目根目錄下,有一個gradle.properties文件

在這文件中,我們可以添加一個常量isDebug,值設為true。

這裡設置了常量之後,我們項目中的其他build.gradle文件都可以把這個常量讀取出來,所以我們可以在home的build.gradle文件中,讀取該常量的值,動態設置applyplugin

這樣子設置之後,當我們需要切換模式時,只需要修改gradle.properties文件中isDebug的值,修改完成之後,點擊Project sync按鈕同步一下,如果有報錯,那麼還有個地方需要修改一下,就是main組件的build.gradle文件,看下圖

為什麼做這樣的修改?因為我們把module的模式改成了application了,這裡不能引入application,引入的話會報錯,所以當是debug模式時(也就是組件單獨開發時),這裡就不引入該組件,免得報錯。

接下來,還得修改 AndroidManifest.xml。因為當我們把一個module設置為application時,其AndroidManifest.xml需要包含一個app所需要的屬性,例如app的icon、theme、launch Activity這些屬性設置,而當module為library時,上面所提的這些屬性都不需要用到,所以當我們處於不同模式時,AndroidManifest.xml文件也得不同。我們在home的src文件夾中新創建一個目錄為debug目錄,再把我們用於debug時的AndroidManifest.xml文件放進去。由於在Android目錄模式下比較難創建這個目錄,所以我們可以選另外一種目錄模式(Project目錄模式),然後再創建debug目錄(很繞,直接看下圖就會明白了)。

AndroidManifest.xml文件內容可以這樣寫

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.zhuang.home"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> </application> </manifest>

以上內容會有很多錯誤提示,其實提示的無非就是資源找不到,我們只需要把這些資源給加上就好了。可以把main組件中相應的資源拷貝過來,放在對應的目錄下就可以了。

接下來在home組件的build.gradle文件中,指定不同模式下的AndroidManifest.xml文件。

sourceSets { main { if (isDebug.toBoolean()) { manifest.srcFile src/main/AndroidManifest.xml } } }

以上設置完成,並且sync project之後,home組件會是這樣的目錄

並且android studio的運行app裡面,多了一個可運行的app,就是home。

由於我們home組件集成到main組件中時,是以一個fragment界面集成上去的,所以我們的home組件中,沒有一個MainActivity可以作為LaunchActivity,我們可以創建一個,然後把fragment在MainActivity的layout xml文件中放進去就可以了。

所對應的activity_main.xml布局文件如下

<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.zhuang.home.HomeMainFragment" android:layout_width_="match_parent" android:layout_height="match_parent"> </fragment>

以上步驟完成之後,home就可以單獨作為一個app來開發了,我們可以直接運行該組件。

效果圖

再來嘗試一下把home組件集成到main組件中,我們只需把isDebug的值修改為false,然後syncproject,此時可以看到我們的home作為app時被打了個叉叉,表示不能用了。

選擇一下main組件,運行。

運行成功,又集成到我們的完整項目裡面了。

至此,我們已經可以隨意的集成或者拆分組件,這裡總結一下「組件化」的優點:

1、 業務模塊分開,解耦的同時也降低了項目的複雜度。

2、 開發調試時不需要對整個項目進行編譯。

3、 多人合作時可以只關注自己的業務模塊,把某一業務當成單一項目來開發。

4、 可以靈活的對業務模塊進行組裝和拆分。

總而言之,言而總之,就是把一個項目分開成多個項目所具有的優點。

共用資源的引用

實際項目中,總有一些資源需要共用,例如第三方庫、自定義的工具類、自定義的view等等,甚至我們的theme。那麼這些資源在「組件化」中,應該如何處理?每個組件都各自引入?

如果每個組件都各自引入這些共用資源,一方面效率差,如果一個類是所有組件都用到的,那麼就得每個組件中都新增這個類,一旦這個類修改了,還得一個組件一個組件的來修改這個類;另一方面,第三方類庫也有可能會衝突,例如不同組件引用了相同的第三方庫,但是版本不一樣,就衝突了。還有諸多問題,就不一一列舉,所以,我們得用另外的方式來處理。

解決的辦法就是,也把共用庫組件化,然後其他組件引用它,所以我們的架構可以演化為這樣:

在項目裡面把這個共用庫加上

然後可以把其他第三方庫、自定義view、工具類、公用資源放進該組件。例如網路請求框架retrofit,在common的build.gradle文件中:

然後其他組件,都分別引用common組件,在其他組件對應的build.gradle文件中

這樣就可以使用common組件的資源了。

看到這裡,不知道大家發現沒,其實我們還可以更簡化,每一個組件的build.gradle所引入的資源,其實有很多是重複的,我們一樣可以把這些一併加到common組件中去,例如下面這一段,就是所有組件都有的

所以這一部分,也可以放到common裡面去。所以三大業務組件裡面的build.gradle文件就變成這樣:

除了第三方庫,還有自定義的工具類、自定義的view等等這些,就直接在common中編寫java文件就行啊,其他組件能夠引入這些類的。

還有就是圖片、xml這些(value目錄下的各種xml文件),也是可以一樣被其他組件引用,這裡需要特別注意的是style.xml文件,對於全局共用的style,我們應該把它也放在common中。例如我們的項目theme,本來是放在main組件的style裡面,我們可以把它移到common中,這樣其他組件調試時,作為一個單獨的項目,也能和主項目有一樣的主題。

總之就是,所有你認為可以被各個組件共享的資源,都可以放在common組件中。

資源衝突

多組件集成時,其資源文件會被歸檔到一起,所以如果命名重複,那麼就會發生衝突,導致界面混亂。為了解決這個問題,我們可以讓各個組件中的資源都有一個屬於自己的前綴,例如home組件中的資源,我們可以以home_開通,video組件中的資源,我們可以以video_開頭,這樣就防止了資源衝突。在這裡gradle可以幫我們做一點事情,就是讓我們在命名資源文件時,幫我們先加上前綴。例如在home組件的build.gradle文件中,加入

resourcePrefix"home_"

這樣之後我們的xml文件如果沒有以home_為前綴的話,就會報錯。但是這個功能其實很弱,例如xml文件報錯,但是我們運行的時候,依然可以運行,圖片文件不已home_為前綴,也不會報錯,所以,資源衝突的問題,還需要開發者自己多多注意。

組件之間的通信

組件雖然可以拆分了,但是當他們集成到main組件中,一起工作時,還是需要一定的通信,例如業務A需要調用到業務B的一個頁面,甚至進行傳參。但是在「組件化」模式下,業務A和業務B是完全分開的,在業務A的認知里,根本就不存在業務B,也無從直接調用。當然,如果要A與B可以直接通信,也是可以的,互相引入就可以,但是這樣的話,項目耦合性太高,架構也混亂,會把「組件化」的所有優點都一一撇掉。我們應該用另外一個方式來處理。

這裡需要引入一個概念----「路由」,就如我們實際訪問網路一樣,我們電腦發送的請求都經過路由器轉發,在「組件化」中,我們也可以設置這麼一個中轉站,來統一處理不同組件之間的調用關係。支持我們這一思想有一些比較有名的開源項目,如ARouter、ActivityRouter,通過這些開源項目,我們可以用一個url,由其轉發,幫我們調用其他組件。具體使用方法大家自行去看,這裡我並不打算用這兩個項目,因為我覺得我們暫且沒需要用到這麼強大的工具,對其他組件的調用,我們可以簡單的這麼寫

try { Class clazz = Class.forName("com.zhuang.personal.MainActivity"); Intent intent = new Intent(context,clazz); startActivity(intent);} catch (ClassNotFoundException e) { Log.e("zhuang","未集成,無法跳轉");}

通過完整的類名來進行跳轉。在debug模式下,調用其他組件時,找不到對應組件,不會直接報錯,只是提示「未集成,無法跳轉」。我們可以把這個方法寫成一個工具類,放在common組件中。

publicclass EventUtil{ /** * 頁面跳轉 * @param context * @param className */ public static void open(Context context,String className){ try { Class clazz = Class.forName(className); Intent intent = new Intent(context,clazz); context.startActivity(intent); } catch (ClassNotFoundException e) { Log.e("zhuang","未集成,無法跳轉"); } } /** * 頁面跳轉,可以傳參,參數放在intent中,所以需要傳入一個intent * @param context * @param className * @param intent */ public static void open(Context context,String className,Intentintent){ try { Class clazz = Class.forName(className); intent.setClass(context,clazz); context.startActivity(intent); } catch (ClassNotFoundException e) { Log.e("zhuang","未集成,無法跳轉"); } } }

這樣跳轉的方法就可以寫成

EventUtil.open(context,"com.zhuang.personal.ShareActivity");

還有一種通信情況是組件A的改變會影響到組件B的改變。以我們這個例子來講,personal組件中,用戶的登陸情況,會影響到home組件中一些控制項的顯示情況,這個時候我們可以用android的廣播機制,或者用EventBus來解決。不過,實際上,如果是像這種會影響全局的改變,應該放在common組件之中。例如用戶,是影響全局的存在,那麼就可以在common中,定義一個用戶單例,其他組件分別拿這個單例去控制其界面的顯示,特別是引入databingding之後,操作起來更為方便。所以,概念我們放在這裡,實際操作可以根據實際情況來做不同的實現。

統一版本號

各個組件的build.gradle文件中,有很多版本號。

我們可以把這些版本號統一管理起來,免得每次修改都得同時修改多份build.gradle文件,也避免不同的組件使用的版本不一樣,導致衝突。

在項目build.gradle文件中(項目根目錄下的build.gradle文件),定義版本號常量

ext { compileSdkVersion = 25 buildToolsVersion = "25.0.2" minSdkVersion = 14 targetSdkVersion = 25 versionCode = 1 versionName = "1.0" }

然後在各個組件的build.gradle文件中,做這樣的修改

相信大家很容易看得明白,就是把版本號用我們定義的常量來表示。

未解決的問題

「組件化」之後,每一個業務組件中,都會有兩份AndroidManifetst.xml文件,當我們用android studio自帶的功能創建Activity或者Service等android組件時,會自動的在AndroidManifetst.xml文件中為我們註冊。但是我們這裡有兩份AndroidManifetst.xml文件,android studio只會自動的在main目錄下的AndroidManifetst.xml文件自動註冊,所以如果是業務組件單獨開發時,就得手動把那些註冊的android組件複製到debug目錄下的AndroidManifetst.xml文件。不知道是否有什麼方法可以另其兩份AndroidManifetst.xml文件都能夠自動註冊?

後話

由於本項目並沒有太多業務邏輯,實際情況中要比這更複雜,如果大家有什麼問題,歡迎留言討論,一起解決。

項目地址 : github.com/likeadog/Com


推薦閱讀:

Google 的 Android One 計劃能不能改變 Android 系統碎片化的生態?
關於Flutter iOS安裝包大小的解讀
簡單提升編譯速度的一個方法
輕量版的原生 Android 好用嗎?我用自己的手機體驗了 Android Go

TAG:Android |