天天看點

元件化架構設計之手寫元件化架構(五)

阿裡P7移動網際網路架構師進階視訊(每日更新中)免費學習請點選: https://space.bilibili.com/474380680 本篇文章将通過手寫元件化架構;路由架構原理與實作來闡述元件化架構設計

此次手寫架構,解決的問題是:

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子產品的資料,怎麼辦呢?下文提供解決方案。

正文大綱

1、代碼結構現狀以及理想狀态一覽

2、功能元件化的實作思路,實作元件移植拔插

3、參考ARouter源碼,寫出自己的Router架構,統一通過Router來進行子產品的切換 以及 元件之間資料的互動

4、使用元件api化,在子產品很多的情況下優化公共子產品的結構

正文

現狀;

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

理想:

理想化的話,參照:理想.png; 項目結構層次分明,脈絡清晰

按照圖中的分層,詳細解釋一下:

外殼層:app module

内部代碼隻寫 app的骨骼架構,比如說,你的app是這個樣子的結構:

下方有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檔案:

注解應該很清晰了,通過一個全局變量,就可以控制目前是要 子產品化單元測試呢?還是要內建打包apk測試。

那麼,隻寫一個isModule就完事了嗎?當然不是,還有一堆雜事 需要我們處理,我們要使用這個全局變量。

一堆雜事,分為兩類;

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配置即可。參考下圖:

這就是業務子產品元件化的秘密了。

什麼,你問我怎麼 進行功能拔插嗎?

當你不需要某一個子產品的時候,

1)在app的build.gradle裡面 把 引用該子產品的配置去掉;

2)setting.gradle 的include 去掉它

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

東西的代碼隻有這一句:
/**
     * 反射執行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 ("路由注冊"接口類 ,用于反射建立對象,進而進行路由的注冊)

上面用大量篇幅詳述了 追蹤源碼, 追查架構結構的方法,那麼下面的篇幅就直接說結論了:

路由架構的結構,可以用一張圖表示:

針對這張圖 簡單說兩句:

路由架構必然有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 注解+反射+自動生成代碼 是完全一樣的。

是以說,打蛇打七寸,看架構要看核心,拿住核心之後,其他的東西,就算代碼量再大,也是狐假虎威。

回顧一下理想中的項目結構:

背景

這裡的功能元件層 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.

上幹貨:

這個名叫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對應,也可以修改

這個api.gradle并不會在本子產品被編譯的時候起作用,但是它最終會變成 _api 新子產品的build.gradle,并保持完全一樣的代碼。 新的_api子產品隻是一個library,是以,要去掉 本子產品裡面的build.gradle裡面針對isModule的判定。

OK,感受一下元件API化的成果:

理想實作了

現在不用把所有的資料接口都放到function公共子產品内,而隻需要在本子產品之内将資料接口檔案字尾改成.api,然後在setting.gradle裡面使用自定義的方法進行include。 就可以隻引用本子產品需要的 資料接口module,而不需要引用多餘的module,而且,防止了function子產品的無意義的膨脹。簡直破費。

原文連結:

https://www.jianshu.com/p/e73bc515ab53