手把手講解 Android元件化開發+自定義路由框架
前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到儘量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫原始碼的追蹤,原始碼截圖,繪製類的結構圖,儘量詳細地解釋原理的探索過程
3、提供Github 的 可執行的Demo工程,但是我所提供程式碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的執行過程中的注意事項
5、用gif圖,最直觀地展示demo執行效果
如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
引子
最近得高人指點,恰巧工作中做過一個移植其他app的某個功能模組的任務,過程簡直痛不欲生。
然後思考如何對app的各個功能模組進行
靈活拔插 配置,最大程度減少移植程式碼出錯的可能性,保證功能模組的完整移植此次手寫架構,解決的問題是:
1、讓 App內 各個功能模組能夠獨立開發單元測試,也可以 所有模組整合打包,統一測試
獨立開發
更改gradle.properties的配置,使得每個功能模組都成為application, 可以獨立打包成apk,單獨執行。單個模組,獨立測試。
整合打包
更改gradle.properties的配置,使得原先每個單獨模組,都變成library,被 主模組引用,這時候只有主模組能夠打包apk,所有功能都整合在這個apk內。
2、實現 功能模組的整體移植,靈活拔插
故事背景
當你們公司有多個安卓開發人員,開發出核心業務相同,但是UI不同,其他業務不同的一系列App時(如果核心業務是X,你們有5個開發人員,做出了A,B,C,D,E 5個app,都包含核心業務X,但是除了X之外,其他的業務模組各不相同)這時候,如果領導要把A裡面的一個非核心功能,挪到B裡面...
現狀
開發B的程式猿可能要罵娘,因為他在從移植A的程式碼中剝離程式碼 遇到了很多高耦合,低內聚 的類結構,挪過來之後,牽一髮而動全身,動一點小地方,整個程式碼滿江紅。
理想
如果這個時候,我們通過程式碼框架的配置,能夠把A裡面的一個模組, 作為一個module 移植到 工程內部,然後主module 來引用這個module,略微寫一些程式碼來使得這個功能模組在app中生效。那麼無論是多少個功能模組,都可以作為整體來 給其他app複用。這樣開發人員也不用相互罵娘了,如果挪過來的模組存在bug或者其他問題,也不用甩鍋,模組原本是誰開發的,找誰就好了。
3、保證App內 業務模組的相互隔離,但是又不妨礙業務模組之間的資料互動
我們開發app的功能模組,一個業務,可能是通過一個Activity或者 一個Fragment 作為對外的視窗,也可能是。 所謂視窗,就是這個業務,相對於其他模組,"有且只有"一個入口,沒有任何其他可以觸達到這個業務的途徑。 業務程式碼之間相互隔離,絕對不可以有相互引用。那麼,既然相互不會引用,那A模組一定要用到B模組的資料,怎麼辦呢?下文提供解決方案。
鳴謝
感謝享學課堂的老師們的公開課 https://ke.qq.com/course/341933
感謝群裡兄弟們的旁敲側擊
感謝阿里巴巴大佬們的ARouter開源框架
正文大綱
1、程式碼結構現狀以及理想狀態一覽
2、功能元件化的實現思路,實現元件移植拔插
3、參考ARouter原始碼,寫出自己的Router框架,統一通過Router來進行模組的切換 以及 元件之間資料的互動
4、使用元件api化,在模組很多的情況下優化公共模組的結構
正文
1、程式碼結構現狀以及理想狀態一覽
先來看兩張圖
現狀

image.png
程式碼有模組化的跡象,但是沒有對業務模組進行非常明顯的模組化 (不明白啥意思是吧?不明白就對了,app這個module裡面其實還有很多東西沒有展示出來,請看下圖:試想,把所有的模組集中到一個module的一個包裡面,當你要移植某一個功能的時候,想想那酸爽....當然如果你口味別緻,那當我沒說)

image
理想:

image.png
理想化的話,參照:理想.png; 專案結構層次分明,脈絡清晰
按照圖中的分層,詳細解釋一下:
外殼層:app module
內部程式碼只寫 app的骨骼框架,比如說,你的app是這個樣子的結構:

典型的app架構.png
下方有N個TAB,通過Fragment來進行切換模組。這種架構肯定不少見。
這個時候,外殼層 app module,就只需要寫上 上面這種UI架構的框架程式碼就行了,至於有多少個模組,需要程式碼去讀取配置進行顯示。神馬?你問我怎麼寫這種UI框架?網上一大把,如果實在找不到,來我的 github 專案地址
๑乛◡乛๑
業務層
我們的業務模組,對外介面可能是一個 Activity
( 比如說,登入模組,只對外提供一個 LoginActivity
,有且僅有這一個視窗) 或者 是一個 Fragment
,就像上圖(典型的app架構.png), 如果app的UI框架是通過切換 Fragment
來卻換業務模組的話。 用 business
這個目錄,將所有的業務模組包含進去,每個模組又是獨立的 module
,這樣既實現了業務程式碼隔離,又能一眼看到所有的業務模組,正所謂,一目瞭然。
功能元件層
每一個業務模組,不可避免的需要用到一些公用工具類,有的是 第三方SDK的再次封裝 ,有的是 自己的工具類, 或者自己寫的 自定義控制元件 ,還有可能是 所有業務模組都需要的 輔助模組 ,都放在這裡。
路由框架層
設計這一層,是想讓app內的所有Activity,業務模組Fragment,以及模組之間的資料互動,都由 這一層開放出去的介面來負責
gradle統一配置檔案
工程內部的一些全域性gradle變數,放在這裡,整個工程都有效
module編譯設定
setting.gradle 配置要編譯的module; 也可以做更復雜的操作,比如,寫gradle程式碼去自動生成一些module,免除人為建立的麻煩.
2. 功能元件化的實現思路,實現元件移植拔插
能夠兼顧 每個模組的單獨開發,單獨測試 和 整體打包,統一測試。 聽起來很神奇的樣子,但是其實就一個核心:gradle程式設計。
開啟gradle.properties檔案:

image
註解應該很清晰了,通過一個全域性變數,就可以控制當前是要 模組化單元測試呢?還是要整合打包apk測試。
那麼,只寫一個isModule就完事了嗎?當然不是,還有 一堆雜事 需要我們處理,我們要使用這個全域性變數。
一堆雜事 ,分為兩類:

image
1- app 外殼層module 的build.gradle(注意:寫在dependencies)
if (isModule.toBoolean()) { implementation project(":business:activity_XXX") //...在這裡引用更多業務模組 }
2- 每個業務module的build.gradle
第一處:判定 isModule,決定當前module是要當成library還是application
if (isModule.toBoolean()) { apply plugin:'com.android.library' } else { apply plugin:'com.android.application'* }
第二處:更改defaultConfig裡面的部分程式碼,為什麼要改?因為噹噹前module作為library的時候,不能有applicationId "XXXX"這一句
defaultConfig { if (!isModule.toBoolean()) { applicationId"study.hank.com.XXXX"* } .... }
第三處:當業務模組module作為library的時候,不可以在 AndroidManifest.xml中寫 Launcher Activity,否則,你打包app module的時候,安裝完畢, 手機桌面上將會出現不止一個icon 。而,當業務模組module 作為application單獨執行的時候,必須有一個Launcher Activity ,不然...launcher都沒有,你測個球 ``` 所以這裡針對manifest檔案進行區分對待。
sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } }
由於要區分對待,我們就需要另外建立一個manifest檔案,移除launcher配置即可。參考下圖:

image

image
這就是業務模組元件化的祕密了。
什麼,你問我怎麼 進行功能拔插?
當你不需要某一個模組的時候,
1)在app的build.gradle裡面 把 引用該模組的配置去掉;

image.png
2)setting.gradle 的include 去掉它

image.png
3)app module 裡面,改動程式碼,不再使用這個模組。(這個我就不截圖了,因為app module的UI框架程式碼不是一句話說得清的。請執行我的 demo原始碼 自己看吧)
功能的插入,同理,上面的過程倒過來走一遍,就不浪費篇幅了。
3. 參考ARouter原始碼,寫出自己的Router框架,統一通過Router來進行模組的切換 以及元件之間資料的互動
說到路由框架的使用價值,兩點:
1、在app實現了元件化之後,由於元件之間由於程式碼隔離,不允許相互引用,導致 相互不能直接溝通,那麼,就需要一個 “中間人角色” 來幫忙 " 帶話" 了.
2、app內部,不可避免地要進行Activity跳轉,Fragment切換。把這些重複性的程式碼,都統一讓路由來做吧。省了不少程式碼行數。
閱讀了阿里巴巴ARouter的原始碼,參照阿里大神的主要思路,簡化了一些流程,去掉了一些我不需要的功能,增加了一些我獨有的功能,加入了一些自己的想法,寫出了自己的 ZRouter 路由 框架。那就不羅嗦了,上乾貨。
基礎知識
如果以下基礎知識不具備,建議先去學習基礎知識。 或者 也可以跟著筆者的思路來看程式碼,慢慢理解這些知識的實用價值。
java反射機制(路由框架裡大量地使用了 class反射建立 物件)
APT 註解,註解解析機制(註解解析機制貫穿了整個路由框架)
javapoet , java類的元素結構(一些人為寫起來很麻煩的程式碼,一些髒活累活,就通過自動生成程式碼來解決)
如何使用
1- 在app module的自定義Application類裡面,進行初始化, ZRouter準備就緒
public class FTApplication extends Application { @Override public void onCreate() { super.onCreate(); ZRouter.getInstance().initRegister(this); } }
2- 就緒之後才可以直接使用( RouterPathConst 裡面都是我自己定義的String常量 **):
切換Fragment
ZRouter.getInstance().build(RouterPathConst.PATH_FRAGMENT_MINE).navigation();
跳轉Activity
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
元件之間的通訊,取得 Mine 模組的 accountNo 然後 toast 出來
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo(); Toast.makeText(getActivity(), accountNo, Toast.LENGTH_LONG).show();
如我們之前所設想的,切換Fragment,跳轉Activity,元件之間的通訊 全部只能通過 ZRouter框架來執行。
3-退出app時,要釋放ARouer的資源(主要是靜態變數)
ZRouter.getInstance().release();
4- 每個業務模組,在將要暴露出去的Fragment或者Activity上,要加上註解
@ZRoute(RouterPathConst.PATH_ACTIVITY_CHART)//註冊Activity public class ChartActivity extends AppCompatActivity {···}
或者
@ZRoute(RouterPathConst.PATH_FRAGMENT_HOME)//註冊Fragment public class HomeFragment extends Fragment {···}
或者
@ZRoute(RouterPathConst.PATH_PROVIDER_MINE) // 註冊資料介面 public class MineServiceImpl implements MineOpenServiceApi{···}
設計思路
講解設計思路,必須用原始碼進行參照,請務必參照原始碼 。
原始碼地址為: https://github.com/18598925736/EvolutionPro
說明一下本人 閱讀原始碼的方法。也許很多人都和曾經的我一樣,看到一份第三方SDK的原始碼,不知道從何下手,要麼看了半天還在原地打轉轉,要麼就是這裡看一點,那裡看一點,沒有中心思想,看了半天毫無收穫。
乾貨:看原始碼要思路清晰,目的明確。一切技術的價值都只有一個,那就是解決實際問題。既然是解決實際問題,那我們就從這個SDK暴露出來的最外圍介面為起點,看這個介面的作用是什麼,解決了什麼問題,順藤摸瓜,找找它解決問題的核心方法,至於順藤摸瓜道路上遇到的枝枝脈脈,要分清哪些是輔助類(每個人寫輔助類的習慣可能都不同,所以不必太在意),哪些是核心類(核心思想一般都是大同小異)。找到了核心思想,再從頭重新過幾遍,SDK的設計思路就瞭然於胸了.
按照我的上面提供的“乾貨”,如果你現在下載了我的 Demo原始碼 ,那麼我們繼續:
如果把看原始碼的結構,理解為 警察查案。那麼就要從最表層的現象開始著手,慢慢查詢根源。
HomeFragment.java的54行, 這裡要進行Activity跳轉。
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
這裡有getInstance()方法,build()方法,還有navigation()方法,一個一個看
-
getInstance()
是處在ZRouter類內部,是ZRouter的單例模式的get方法,單例模式就不贅述了,我寫了註釋 -
build()
方法也是在ZRouter類內部,邏輯很簡單,就是new Postcard(path)
引數path
是一個string
,方法返回值是一個Postcard
物件 -
navigation()
方法是在Postcard類內部,但是,具體的執行邏輯,依然是在ZRouter
類裡面
getInstance()
和build()
方法都很簡單,不需要花太多精力。下面繼續跟隨ZRouter
的navigation()
方法“追查”
ZRouter
的 navigation()
方法內容如下:
Object navigation(Postcard postcard) { LogisticsCenter.complete(postcard); switch (postcard.getRouteType()) { case ACTIVITY://如果是Activity,那就跳吧 return startActivity(postcard); case FRAGMENT://如果是Fragment,那就切換吧 return switchFragment(postcard); case PROVIDER://如果是Provider,那就執行業務邏輯 return postcard.getProvider();//那就直接返回provider物件 default: break; } return null; }
發現一個可疑的程式碼: LogisticsCenter.complete(postcard);
看方法名,應該是對postcard物件進行完善。
進去追查
/** * Postcard欄位補全 * * @param postcard */ public static void complete(Postcard postcard) { if (null == postcard) { throw new RuntimeException("err:postcard 是空的,怎麼搞的?"); } RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());// if (null == routeMeta) {//如果路由meta是空,說明可能這個路由沒註冊,也有可能路由表沒有去載入到記憶體中 throw new RuntimeException("err:路由定址失敗,請檢查是否path寫錯了"); } else { postcard.setDestination(routeMeta.getDestination()); postcard.setRouteType(routeMeta.getRouteType()); ··· } }
這段程式碼,從一個 map
中,用 path
作為 key
, get
出了一個 RouteMeat
物件,然後用這個物件的欄位值,對引數 postcard
的屬性進行賦值。好像有點莫名其妙。看不太懂。不著急,繼續。
剛才的 navigation()
方法這裡存在 switch
分支,分支設計到 ACTIVITY,FRAGMENT,PROVIDER
,由於我們這次追查的只是 activity
相關,所以,忽略掉其他分支,只追查 startActivity(postcard);
下面是該方法的程式碼:
private Object startActivity(Postcard postcard) { Class<?> cls = postcard.getDestination(); if (cls == null) { if (cls == null) throw new RuntimeException("沒找到對應的activity,請檢查路由定址標識是否寫錯"); } final Intent intent = new Intent(mContext, cls); if (Postcard.FLAG_DEFAULT != postcard.getFlag()) {//如果不是初始值,也就是說,flag值被更改過,那就用更改後的值 intent.setFlags(postcard.getFlag()); } else {//如果沒有設定啟動模式,即 flag值沒有被更改,就用常規模式啟動 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//常規模式啟動Activity } //跳轉只能在主執行緒中進行 runInMainThread(new Runnable() { @Override public void run() { mContext.startActivity(intent); } }); return null; }
這裡只是一個簡單的跳轉操作,但是,發現了一個關鍵點,跳轉的“目的地” class
是來自 postcard
的 destination
. 發現規律了,原來剛才在 LogisticsCenter.complete(postcard);
裡面進行 postcard
“完善”的時候, set
進去的 destination
原來在這裡被使用到。
那麼問題的關鍵點就發生了轉移了, 這個 destination
Class
是從 map
裡面 get
出來的,那麼,又是什麼時候被 put
進去的呢?
開始追蹤這個 map
: Warehouse.routeMap
,通過程式碼追蹤,可以發現,唯一可能往 map
裡面 put
東西的程式碼只有這一句:

image.png
/** * 反射執行APT註冊檔案的註冊方法 */ private static void registerComm() { try { Set<String> classNames = ClassUtils.getFileNameByPackageName(mContext, RouterConst.GENERATION_PACKAGE_NAME);//找到包名下的所有class for (String className : classNames) { Class<?> clz = Class.forName(className); if (IRouterZ.class.isAssignableFrom(clz)) { IRouterZ iRouterComm = (IRouterZ) clz.getConstructor().newInstance(); iRouterComm.onLoad(Warehouse.routeMap); } } } catch (Exception e) { e.printStackTrace(); } finally { Warehouse.traversalCommMap(); } }
利用java反射機制,反射建立類的例項,然後執行 onLoad
方法,引數,正是這個 map
OK,關於檢視原始碼的詳細步驟,就寫到這麼多,再羅嗦,大佬們要打人啦。
ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();
我們一路上遭遇了這些類或介面:
核心類 :
ZRouter(提供Activity跳轉的介面);
輔助類或介面
Postcard (“明信片”,封裝我們要執行操作,這次的操作是 跳Activity)
RouteMeta (“路由引數”,Postcard的基類)
RouteType *(“路由型別”,我們要執行的操作,用列舉來進行區分) *
LogisticsCenter ("物流中心",主要封裝ZRouter類的一些特殊邏輯,比如對Postcard物件進行完善補充 )
Warehouse (“貨艙”,用hashMap來儲存“路由”物件)
IRouterZ ("路由註冊"介面類 ,用於反射建立物件,從而進行路由的註冊)
上面用大量篇幅詳述了 追蹤原始碼, 追查框架結構的方法,那麼下面的篇幅就直接說結論了:
路由框架的結構,可以用一張圖表示:

image.png
針對這張圖 簡單說兩句:
路由框架必然有3個部分,註解定義,註解解析,以及路由對外介面。
demo
中我把這3個部分定義成了3個 module
.
其中,每個部分的核心程式碼是:
zrouter-annotation
模組的 ZRoute @interface,IRouterZ
介面
zrouter-api
模組的 ZRouter
類
zrouter-compiler
模組的 RouterProcessor
類
具體的程式碼,不加以說明了。
如何用路由進行 Activity
跳轉,我寫了詳細步驟,相信沒人看不懂了。那麼 Fragment
的切換,是我自定義的方法,可能有點粗糙,但是也是通俗易懂,就點到為止。但是,我們元件化的思想,就是要隔離所有的業務模組,彼此之間不能進行直接通訊,如果A模組一定要使用B模組的一些資料,通過路由框架也能實現。
HomeFragment類的第72行程式碼:
String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();
這句程式碼的意義是:在Home模組中,通過路由框架,呼叫Mine模組對外開放的介面accountNo();
追蹤這句程式碼的 navigation()
方法,找到真正的執行邏輯 ZRouter類
141行起:
public <T> T navigation(String serviceName) { Postcard postcard = LogisticsCenter.buildProvider(serviceName); if (null == postcard) return null; LogisticsCenter.complete(postcard);//補全postcard欄位值 return (T) postcard.getProvider(); }
這裡:最終返回了一個Provider物件.
LogisticsCenter
類又有了戲份: LogisticsCenter.buildProvider(serviceName)
和 LogisticsCenter.complete(postcard);
分別點進去看:
buildProvider(String)
方法,其實就是從 map
中找出 RouteMeta
物件,然後返回一個 Postcard
.
public static Postcard buildProvider(String name) { RouteMeta routeMeta = Warehouse.routeMap.get(name); if (null == routeMeta) { return null; } else { return new Postcard(routeMeta.getPath()); } }
complete(Postcard)
方法,其實就是完善 postcard
的欄位,且,針對 Provider
,進行特別處理,反射建立 Provider
物件,並建立 Provider
的快取機制,防止多次進行資料互動時進行無意義的反射建立物件。
/** * Postcard欄位補全 * * @param postcard */ public static void complete(Postcard postcard) { if (null == postcard) { throw new RuntimeException("err:postcard 是空的,怎麼搞的?"); } RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());// if (null == routeMeta) {//如果路由meta是空,說明可能這個路由沒註冊,也有可能路由表沒有去載入到記憶體中 throw new RuntimeException("err:路由定址失敗,請檢查是否path寫錯了"); } else { postcard.setDestination(routeMeta.getDestination()); postcard.setRouteType(routeMeta.getRouteType()); switch (routeMeta.getRouteType()) { case PROVIDER://如果是資料介面Provider的話 Class<? extends IProvider> clz = (Class<? extends IProvider>) routeMeta.getDestination(); //從map中找找看 IProvider provider = Warehouse.providerMap.get(clz); //如果沒找到 if (null == provider) { //執行反射方法建立,並且存入到map try { provider = clz.getConstructor().newInstance(); provider.init(mContext); Warehouse.providerMap.put(clz, provider); } catch (Exception e) { e.printStackTrace(); } } postcard.setProvider(provider); break; default: break; } } }
看到這裡,整個路由框架,包括模組間的通訊,就講解完畢了。
做個結論:
使用路由框架的目的,是 在專案程式碼元件化的背景之下,優化Activity跳轉,Fragment切換的重複程式碼的編寫,而統一使用路由框架的對外介面執行跳轉或者切換。同時,通過路由框架的對外介面,實現元件之間的無障礙通訊,保證元件的獨立性。
在探索框架的過程中,我們遇到了很多輔助類,但是輔助類怎麼寫,完全看個人習慣,我是看了阿里巴巴的ARtouer框架之後得到啟發,按照它的思路來寫自己的路由框架,但是很多輔助類的寫法,我並完全按它的意思來。但是,核心思想,APT 註解+反射+自動生成程式碼 是完全一樣的。
所以說,打蛇打七寸,看框架要看核心,拿住核心之後,其他的東西,就算程式碼量再大,也是狐假虎威。
4、使用元件api化,在模組很多的情況下優化公共模組的結構
回顧一下理想中的專案結構:

image
背景
這裡的功能元件層 function,是存放各個業務模組都需要的公共類或者介面。這裡說的公共類,也包含了剛才所提及的 業務模組之間進行通訊所需要的介面。
舉例說明:A模組,需要呼叫B模組的test()介面,由於A不能直接引用B模組,那這個test介面,只能放在function這個公共模組內,然後A,B同時引用,B對test介面進行實現並通過註解進行路由註冊,A通過路由對外介面呼叫B的test方法。
現狀
誠然,這種做法沒毛病,能夠實現功能。但是隨著專案模組的增多,function 裡面會存在很多的業務模組資料介面。有一種情況:如果存在A,B,C,D,E 5個模組,它們都在function記憶體放了 資料介面,並且5個模組都引用了function模組。那麼,當A需要,並且只需要B的資料介面,而不需要C,D,E的介面時,它還是不得不去引用這些用不著的介面。A不需要這些介面,但是,還不得不引用!這顯然會不合邏輯。並且這種 全部業務資料介面都塞到function模組裡面的做法,會導致function出現不必要的臃腫。
理想
每個業務模組的資料介面,只和本模組的業務有關,所以最好是放在本模組之內,但是,如果放在本模組之內,又會導致元件之間不能通訊. 那麼就建立一個專門的 Module來存放每個業務模組的介面。想法可行,但是每個業務模組的module數量一下子加倍了,又會造成維護困難的問題。那麼有沒有方法可以自動生成這些資料介面模組呢? 還真有~ 神奇的gradle程式設計 >_<
關鍵詞
元件API化技術 使用gradle配置,對module內的特殊字尾檔案進行檢索,並以當前module為基礎,自動生成新的module.
不羅嗦,直接上乾貨:

image.png
這個名叫MineOpenServiceApi的介面,原本是.java字尾,現在改成.api
開啟 demo 的setting.gradle檔案:
找到下面的程式碼:
include_with_api(':business:fragment_mine') def include_with_api(String moduleName) { include(moduleName) //獲得工程根目錄 String originDir = project(moduleName).projectDir //製作的 SDK 工程的目錄 String targetDir = "${originDir}_api" //製作的 SDK 工程的名字 String sdkName = "${project(moduleName).name}_api" System.out.println("-------------------------------------SDK name:" + sdkName) //刪除掉 SDK 工程目錄 除了 iml FileTree targetFiles = fileTree(targetDir) targetFiles.exclude "*.iml" targetFiles.each { File file -> file.delete() } //從待制作SDK工程拷貝目錄到 SDK工程 只拷貝目錄 copy { from originDir into targetDir //拷貝檔案 include '**/*.api' include '**/AndroidManifest.xml' include 'api.gradle' } //讀取實現模組的manifest並將package的值後加 .api 作為API工程的manifest package FileTree manifests = fileTree(targetDir).include("**/AndroidManifest.xml") manifests.each { File file -> def parser = new XmlParser().parse(file) def node = parser.attribute('package') parser.attributes().replace('package', "${node}.api") new XmlNodePrinter(new PrintWriter(file)).print(parser) } //將api.gradle改為build.gradle File build = new File(targetDir + "/api.gradle") if (build.exists()) { build.renameTo(new File(targetDir + "/build.gradle")) } // 將.api 檔案改為 .java FileTree files = fileTree(targetDir).include("**/*.api") files.each { File file -> file.renameTo(new File(file.absolutePath.replace(".api", ".java"))) } //加入 SDK工程 include ":business:" + "$sdkName" }
這段程式碼來自一位”真“大神,它的作用是,檢索指定模組裡面,有沒有指定字尾名(.api)的檔案,有的話,找出來,經過一系列處理(註解很詳細,應該能看懂),自動生成一個module. 生成的module名字比原來的module多一個_api. 表示這個模組,包含原模組的所有對外資料介面
有幾處細節需要注意:
- 資料介面的.java字尾需要改成.api(整個.api完全和setting.gradle程式碼裡的.api對應,你可以都換成其他字尾,比如.apixxxxx)
- 原模組裡面,會多出一個api.gradle,這個檔案的名字也和 setting.gradle裡的api.gradle對應,也可以修改
image.png
OK,感受一下元件API化的成果:

image.png
理想實現了
現在不用把所有的資料介面都放到function公共模組內,而只需要在本模組之內將資料介面檔案字尾改成.api,然後在setting.gradle裡面使用自定義的方法進行include。 就可以只引用本模組需要的 資料介面module,而不需要引用多餘的module,而且,防止了function模組的無意義的膨脹。簡直破費。
結語
元件化的全攻略+ Demo 已經全部放送完畢。
特別說明: Demo只是提供一種元件化的全攻略,可能demo的程式碼並沒有十分完善,比如:原ARouter原始碼內的帶引數的跳轉,或者startActivityForResult,由於時間關係我都去除了。一些輔助向的設計思路,我也並沒有完全遵照ARouter原始碼。
這篇部落格的技術來自 享學課堂的公開課,建議大家可以去聽聽 https://ke.qq.com/course/341933
如果有大佬看了文章之後有問題,或者意見建議,歡迎留言討論。 >_<!