Android 實(shí)戰(zhàn)之組件化框架

問題場景:
筆者一直想使用組件化開發(fā)框架來進(jìn)行實(shí)現(xiàn)模塊解耦辉川,這一點(diǎn)在協(xié)作開發(fā)的時候很有用芦岂,比如ABC三人開發(fā)一款app李破,其中A需要用到BC的功能,B需要用到AC的功能壹将,C需要用到A功能嗤攻,那么問題來了,如果ABC同時開發(fā)各自模塊诽俯,那么誰都不想等待屯曹,怎么解決這個問題?

解決方法:
很多人會想到ABC定義業(yè)務(wù)接口惊畏,先提供出去恶耽,然后給需要的調(diào)用,這個方案是好的颜启,我們在提供接口的時候偷俭,需要定義各種模塊,下面我們開始編寫一個組件化開發(fā)架構(gòu)(目前筆者項(xiàng)目正在切換的架構(gòu)缰盏,筆者認(rèn)為不錯涌萤,打算模擬源碼實(shí)現(xiàn)開源,這樣的結(jié)構(gòu)夠我玩幾年了~)

下面我們就開始組件化開發(fā)吧:
首先創(chuàng)建一個app項(xiàng)目口猜,同時創(chuàng)建moudleA,moduleB,moduleC模塊负溪,app模塊依賴moudleA,moduleB,moduleC模塊,同時創(chuàng)建一個common作為base能力庫济炎,這個模塊用于我們后面編寫組件化核心代碼
項(xiàng)目結(jié)構(gòu)如下:


image.png

image.png

其中moudleA,moduleB,moduleC, common模塊作為lib存在(apply plugin: 'com.android.library'),圖解如下:


image.png

現(xiàn)在創(chuàng)建了ModuleA,B,C模塊川抡,但是目前了結(jié)構(gòu)無法滿足我們的要求,ABC之間需要定義接口
下面開始我們的編碼:
在moudleA里面定義外部接口A须尚,moudleB里面定義外部接口B崖堤,moudleC里面定義外部接口C,如果ModuleB需要用到A或者C的接口耐床,需要引用A,C項(xiàng)目
implementation project(path: ':moduleA') 
implementation project(path: ':modulec')

那么B的結(jié)構(gòu)看上去如下:


image.png

這樣MoudleB就可以用到AC的接口密幔,但是有心的讀者可以發(fā)現(xiàn)一個問題,那這樣我還要區(qū)分moudleA撩轰,moudleB胯甩,moudleC干什么?他們互相依賴堪嫂,并且處于同一層那就相當(dāng)于合在了一個Lib里面偎箫,彼此相互依賴無法拆分,這樣的定義模塊就沒有意義了
筆者也認(rèn)為確實(shí)是這樣的溉苛,我們還需要進(jìn)行進(jìn)一步改造镜廉,爭取讓B只能訪問AC的接口,對于moduleA愚战,moduleC的具體實(shí)現(xiàn)不關(guān)心

實(shí)現(xiàn)方法:
我們將MoudleA與MoudleC再次拆分娇唯,分成ModuleA-API與ModuleA-IMPL,將ModuleA-API提供給B使用寂玲,MoudleC同樣的方法拆分
這里我們需要創(chuàng)建文件夾了塔插,
首先在我們的工程目錄下面創(chuàng)建moudleA,moduleB,moduleC文件夾,通知將之前的moudle分別嵌入對應(yīng)目錄拓哟,創(chuàng)建APImoudule與Implmoudle最后修改settings.gradle想许,看上去像這樣:
Project這樣:


image.png

Android顯示如下:


image.png

Setting配置如下:
include ':app', ':common',
        ":MoudleA:Api",":MoudleA:Impl",
        ":MoudleB:Api",":MoudleB:Impl",
        ":MoudleC:Api",":MoudleC:Impl"
rootProject.name='ComponentApp'

意思就是我們在每個模塊加入一個父目錄
結(jié)構(gòu)看上去如下:


image.png

其中Folder只是父目錄,不是工程断序,其他都是工程
這樣的話流纹,如果ModuleB需要使用A與C的接口,只需要依賴MoudleA/C – Api模塊就可以违诗,不需要依賴Impl的具體實(shí)現(xiàn)漱凝,而MoudleA/C-Impl代表具體實(shí)現(xiàn)其Api接口
那么結(jié)構(gòu)就變成這樣了:


image.png

模塊B需要用到moduleA,moduleC的接口,用到基礎(chǔ)庫Common诸迟,而真正具體實(shí)現(xiàn)ModuleA/C由協(xié)助者同步開發(fā)茸炒,這樣就不產(chǎn)生過度耦合,注意:圖中ModuleA/C-Impl只是具體實(shí)現(xiàn)阵苇,不會被其他模塊直接使用壁公,而我們最后將所有組件的Api與Impl將注冊到總組件Common中,這樣每個模塊Impl可以拿到Api接口的具體實(shí)現(xiàn)

下面我們開始編寫模擬代碼
1.創(chuàng)建ModuleABC的接口InterfaceABC
2.MoudleB-Impl項(xiàng)目添加ModuleA-Api與ModuleC-Api依賴绅项,用于調(diào)用其他模塊功能紊册,添加ModuleB-Api,用于具體實(shí)現(xiàn)自己外部提供的接口

implementation project(path: ':MoudleB:Api')
implementation project(path: ':MoudleC:Api')
implementation project(path: ':MoudleA:Api')

3.1 先實(shí)現(xiàn)自己模塊功能快耿,ModuleB-Api湿硝,我們定義InterfaceB,提供一個printModuleB接口方法:

/**
 * 用于其他模塊調(diào)用
 */
public interface InterfaceB {
    void printModuleB();
}

3.2 ModuleB-Impl定義Impl實(shí)現(xiàn)類

public class ModuleBImpl implements InterfaceB {
    @Override
    public void printModuleB() {
        Log.i("MoudleImpl", "print ModuleB");
    }
}
  1. 那么MoudleAC-Impl引用了ModuleB-Api润努,如何獲取其具體實(shí)現(xiàn)呢关斜?
    下面就是重點(diǎn)了,我們需要將所有Api與Impl鏈接起來铺浇,并且注冊到一個組件存儲器里面痢畜,而外部只能拿到接口
    4.1 步驟4需要定義在Common庫里面,并且所有Impl都有依賴Common庫鳍侣,用于獲取Api服務(wù)丁稀,定義一個存儲器HashMap<String,Object>存儲api名與impl實(shí)現(xiàn),并且提供注冊方法registerApiAndImpl倚聚,有了注冊线衫,還需要提供獲取接口方法。定義ComponentServiceStore類惑折,代碼看起來如下:
public class ComponentServiceStore implements IComponentService {

    private Map<String, Object> apiStores = new HashMap<>();

    @Override
    public <T> void registerApiAndImpl(Class<T> api, Class<? extends T> impl) {
        if (api.isInterface() && !impl.isInterface()) {
            Object o = NewInstanceFactory.create(impl);
            apiStores.put(api.getCanonicalName(), o);
        } else {
            throw new IllegalStateException("impl is not api subclass");
        }
    }

    public void init(ApiService.IRegisterCallBack callBack) {
        callBack.onInit(this);
    }

    <T> T getServiceImpl(Class<T> apiClazz) {
        String canonicalName = apiClazz.getCanonicalName();
        Object o = apiStores.containsKey(canonicalName) ? apiStores.get(canonicalName) : null;
        return o != null ? (T) o : null;
    }

    public static class NewInstanceFactory {
        @SuppressWarnings("ClassNewInstance")
        @NonNull
        public static <T> T create(@NonNull Class<T> modelClass) {
            // noinspection TryWithIdenticalCatches
            try {
                return modelClass.newInstance();
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            }
        }
    }
}

代碼就是一個簡單的map存取功能授账,這就是核心實(shí)現(xiàn)枯跑,對此我們還需要擴(kuò)展包裝一下,提供一個初始化方法:

public class ApiService {

    private static ComponentServiceStore componentService;

    public interface IRegisterCallBack {
        void onInit(IComponentService componentService);
    }

    public static void initService(IRegisterCallBack callBack) {
        // init componentService
        if (componentService == null) {
            componentService = new ComponentServiceStore();
        }
        componentService.init(callBack);
    }

    public static <T> T getServiceImpl(Class<T> apiClazz) {
        if (componentService == null) {
            throw new IllegalStateException("you must call ApiComponentService#initService first");
        }
        return componentService.getServiceImpl(apiClazz);
    }
}

上面就是注冊組件化的核心代碼白热,我們可以在自定義Application#onCreate里面進(jìn)行初始化敛助,使用方法如下:(注意,App模塊目前需要將所有模塊都依賴進(jìn)去屋确,這里后續(xù)我們還需要改造纳击,如果app刪除某個模塊,最好我們只需要刪除gradle里面依賴不改動代碼就好)

public class XApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initService();
    }
    private void initService() {
        ApiService.initService(new ApiService.IRegisterCallBack() {
            @Override
            public void onInit(IComponentService componentService) {
                componentService.registerApiAndImpl(InterfaceA.class, ModuleAImpl.class);
                componentService.registerApiAndImpl(InterfaceB.class, ModuleBImpl.class);
                componentService.registerApiAndImpl(InterfaceC.class, ModuleCImpl.class);
            }
        });
    }
}

所有組件都在App模塊里面注冊好了攻臀,那么我們?nèi)绻贛oudleB-Impl模塊使用其他某塊呢
1. MoudleB-Impl已經(jīng)依賴ModuleA-Api與ModuleC-Api

implementation project(path: ':MoudleC:Api')
implementation project(path: ':MoudleA:Api')
  1. 獲取其他模塊接口:
public class ModuleBImpl implements InterfaceB {
    @Override
    public void printModuleB() {
        Log.i("MoudleImpl", "print ModuleB");

        //這里我們調(diào)用一下模塊C的接口實(shí)現(xiàn)
        new TestMocker().testModuleC();
    }
}
public class TestMocker {
    public void testModuleC() {
        InterfaceC implC = ApiService.getServiceImpl(InterfaceC.class);
        if (implC != null) {
            implC.printModuleC();
        }
    }
}

我們在組件B的Impl實(shí)現(xiàn)里面去獲取C的接口并調(diào)用C的接口方法焕数,看看C-Impl是否實(shí)現(xiàn)了接口


image.png

好了,上面就是組件化解耦刨啸,筆者目前項(xiàng)目正在遷移的框架簡介(很有用哦~)
這里遺留一個小問題堡赔,有興趣的讀者可以研究一下:
前面我們提到的,app模塊如果注冊所有api與impl需要將模塊全部引入呜投,如果刪除某個模塊還需要手動修改報錯代碼加匈,這里我們是否可以能夠在動態(tài)添加刪除模塊的時候,不修改代碼仑荐,讓組件注冊的時候自動加載實(shí)例化雕拼,如果加載不到那就不加載呢?

注意
我們使用的組件化Impl實(shí)現(xiàn)都是存儲在ApiService里面的粘招,所以獲取出來的對象是同一個啥寇,但是筆者在項(xiàng)目里面使用的使用,在ModuleBImpl中存儲了一個變量(比如private String userId)洒扎,然后調(diào)用異步操作方法傳入修改了userId辑甜,后面callback回來的使用使用了該userId,但是心細(xì)的小伙伴可以發(fā)現(xiàn)袍冷,異步操作如果該方法調(diào)用多次就會出現(xiàn)回調(diào)獲取的userId與之前的不一致磷醋,所以這里建議Impl實(shí)現(xiàn)不能保存變量,可以使用新的實(shí)例來修改保存胡诗,不能存儲在這里類型單例里面
感謝閱讀邓线,有不當(dāng)之處或者建議,評論回復(fù)煌恢,筆者定努力完善骇陈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瑰抵,隨后出現(xiàn)的幾起案子你雌,更是在濱河造成了極大的恐慌,老刑警劉巖二汛,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件婿崭,死亡現(xiàn)場離奇詭異拨拓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)逛球,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門千元,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苫昌,“玉大人颤绕,你說我怎么就攤上這事∷钌恚” “怎么了奥务?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長袜硫。 經(jīng)常有香客問我氯葬,道長,這世上最難降的妖魔是什么婉陷? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任帚称,我火速辦了婚禮,結(jié)果婚禮上秽澳,老公的妹妹穿的比我還像新娘闯睹。我一直安慰自己,他們只是感情好担神,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布楼吃。 她就那樣靜靜地躺著,像睡著了一般妄讯。 火紅的嫁衣襯著肌膚如雪孩锡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天亥贸,我揣著相機(jī)與錄音躬窜,去河邊找鬼。 笑死炕置,一個胖子當(dāng)著我的面吹牛荣挨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讹俊,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼垦沉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了仍劈?” 一聲冷哼從身側(cè)響起厕倍,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贩疙,沒想到半個月后讹弯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體况既,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年组民,在試婚紗的時候發(fā)現(xiàn)自己被綠了棒仍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡臭胜,死狀恐怖莫其,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耸三,我是刑警寧澤乱陡,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站仪壮,受9級特大地震影響憨颠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜积锅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一爽彤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缚陷,春花似錦适篙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蝶缀,卻和暖如春丹喻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翁都。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工碍论, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柄慰。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓鳍悠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坐搔。 傳聞我的和親對象是個殘疾皇子藏研,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容