Android應(yīng)用模板之項(xiàng)目架構(gòu)和路由

應(yīng)用模板代碼地址:https://github.com/thfhongfeng/AndroidAppTemplate

項(xiàng)目架構(gòu)和路由介紹

Android項(xiàng)目架構(gòu)主要目的就是實(shí)現(xiàn)業(yè)務(wù)的模塊化,使之成為可插拔可配置的組件:
業(yè)務(wù)模塊化:這點(diǎn)Gradle的模塊化編譯實(shí)際上已經(jīng)實(shí)現(xiàn)憔鬼。
模塊間的通信:模塊化后寇蚊,模塊間的通信成為重點(diǎn)。這個(gè)有很多現(xiàn)成的庫幫我們實(shí)現(xiàn)了,比如Arouter。


Arouter的路由功能的實(shí)現(xiàn)主要分三個(gè)階段:
編譯階段:Arouter在編譯階段通過注解處理器將標(biāo)記的通信類收集起來生成處理這些信息的類文件(類似ARouter$$Group$$XXX類文件),并放在指定的包中(com.alibaba.android.arouter.routes )。
初始化階段:Arouter初始化時(shí)通過掃描指定的包(com.alibaba.android.arouter.routes )握牧,將里面的類進(jìn)行初始化容诬。然后調(diào)用指定的方法(loadInto)將通信類放到幾個(gè)靜態(tài)的Map中。
使用階段:在使用過程中通過標(biāo)記在Map中找到對(duì)應(yīng)的通信類了沿腰,然后就是實(shí)例化這些通信類览徒,根據(jù)通信類的類別做出相應(yīng)的處理和返回相應(yīng)的結(jié)果了。


而筆者應(yīng)用模板的架構(gòu)正是基于Arouter颂龙。
Arouter的路由功能雖然很強(qiáng)大习蓬,但仍然對(duì)業(yè)務(wù)代碼有入侵。為了盡量減少對(duì)業(yè)務(wù)代碼的入侵措嵌,應(yīng)用模板搭架了一個(gè)路由模塊router躲叼。router通信模塊是一個(gè)通信規(guī)范化的模塊,Arouter只是通信的一種實(shí)現(xiàn)方式企巢。
router模塊的目錄結(jié)構(gòu):

router模塊目錄結(jié)構(gòu)

應(yīng)用模板模塊間通信的主要分兩塊(新增模塊時(shí)模塊間通信的搭建):
router模塊的command集:新增一個(gè)模塊時(shí)枫慷,如果此模塊需要給其它模塊提供模塊間服務(wù),就需要在router模塊中添加該模塊提供給其他模塊使用的服務(wù)命令集RouterXxxCommand。
router模塊的command集

業(yè)務(wù)模塊的提供的remote通信服務(wù):新增一個(gè)模塊時(shí)或听,如果此模塊需要給其它模塊提供模塊間服務(wù)探孝,就需要在本模塊中添加提供具體服務(wù)的類XxxRemoteService和服務(wù)協(xié)議類XxxArouterService。XxxRouterClient為本模塊調(diào)用其它模塊的統(tǒng)一服務(wù)出口類誉裆。
業(yè)務(wù)模塊的提供的remote通信服務(wù)目錄結(jié)構(gòu)

以上兩塊都是通過注解的方式來實(shí)現(xiàn)的顿颅,下面進(jìn)行解析說明。

本應(yīng)用架構(gòu)模塊間通信的原理

從使用出發(fā)一步一步解析
上面已經(jīng)展示了Login模塊的模塊間通信設(shè)施的搭架足丢。那么我們來看看Welcome模塊是如何使用Login模塊提供的服務(wù)的粱腻。


Welcome模塊目錄結(jié)構(gòu)

Welcome模塊在合適的位置會(huì)通過調(diào)用WelcomeRouterClient里的autoLogin嘗試自動(dòng)登錄。

public class WelcomeRouterClient {
    public static void callCommand(Context context, String bundleKey,
                                   String command, Bundle args, IRouterCallback callback) {
        RouterManager.getInstance(bundleKey).callUiCommand(context,
                command, args, callback);
    }

    public static void autoLogin(Context context, Bundle args, IRouterCallback callback) {
        RouterManager.getInstance(ConfigKey.BUNDLE_LOGIN_KEY).callOpCommand(context,
                RouterLoginCommand.autoLogin, args, callback);
    }

    public static void goMainHomeActivity(Context context, Bundle args, IRouterCallback callback) {
        RouterManager.getInstance(ConfigKey.BUNDLE_MAIN_KEY).callUiCommand(context,
                RouterMainCommand.goMainHomeActivity, args, callback);
    }
}

autoLogin方法先根據(jù)Key獲取RouterManager實(shí)例霎桅∑芤桑看看如何獲取實(shí)例的

public class RouterManager {
    public static IRouterManager getInstance(String bundleKey) {
        switch (BuildConfig.APP_THIRD_ROUTER_PROVIDER) {
            case "arouter":
                return ARouterManager.getInstance(bundleKey);
            default:
                return ARouterManager.getInstance(bundleKey);
        }
    }
}

RouterManager的規(guī)劃是為了讓模塊間通信的第三方工具(如Arouter)可插拔,即也可以用其它的三方模塊間通信庫來實(shí)現(xiàn)滔驶。目前只實(shí)現(xiàn)了使用Arouter的方式遇革,所以都會(huì)去調(diào)用ArouterMananger的getInstance方法。

public class ARouterManager implements IRouterManager {
    private final String TAG = LogUtils.makeLogTag(this.getClass());

    private static volatile List<String> mClassNameList = new ArrayList<>();
    private static volatile HashMap<String, ARouterManager> mInstanceMap = new HashMap<>();

    private String mBundleKey = "";
    private String mRemoteAction = "";

    static {
        try {
            mClassNameList = AndroidClassUtils.getFileNameByPackageName(AppUtils.getApplicationContext(),
                    "com.pine.router.command");
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private ARouterManager(@NonNull String bundleKey) {
        mBundleKey = bundleKey;
        for (int i = 0; i < mClassNameList.size(); i++) {
            try {
                Class<?> clazz = Class.forName(mClassNameList.get(i));
                ARouterRemoteAction remoteAction = clazz.getAnnotation(ARouterRemoteAction.class);
                if (remoteAction != null) {
                    if (mBundleKey.equals(remoteAction.Key())) {
                        mRemoteAction = remoteAction.RemoteAction();
                        break;
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public static ARouterManager getInstance(@NonNull String bundleKey) {
        if (mInstanceMap.get(bundleKey) == null) {
            synchronized (ARouterManager.class) {
                if (mInstanceMap.get(bundleKey) == null) {
                    mInstanceMap.put(bundleKey, new ARouterManager(bundleKey));
                }
            }
        }
        return mInstanceMap.get(bundleKey);
    }

    ……
    ……
    public void callCommand(final String commandType, final Context context, String commandName,
                            Bundle args, final IRouterCallback callback) {
        if (!checkBundleValidity(commandType, context, callback)) {
            return;
        }
        ARouterBundleRemote routerService = ((ARouterBundleRemote) ARouter.getInstance().build(mRemoteAction)
                .navigation(context, new NavigationCallback() {
                    @Override
                    public void onFound(Postcard postcard) {
                        LogUtils.d(TAG, "callOpCommand path:'" + postcard.getPath() + "'onFound");
                    }

                    @Override
                    public void onLost(Postcard postcard) {
                        LogUtils.d(TAG, "callOpCommand path:'" + postcard.getPath() + "'onLost");
                        if (callback != null && !callback.onFail(IRouterManager.FAIL_CODE_LOST, "onLost")) {
                            onCommandFail(commandType, context, IRouterManager.FAIL_CODE_LOST, "onLost");
                        }
                    }

                    @Override
                    public void onArrival(Postcard postcard) {
                        LogUtils.d(TAG, "callOpCommand path:'" + postcard.getPath() + "'onArrival");
                    }

                    @Override
                    public void onInterrupt(Postcard postcard) {
                        LogUtils.d(TAG, "callOpCommand path:'" + postcard.getPath() + "'onInterrupt");
                        if (callback != null && !callback.onFail(IRouterManager.FAIL_CODE_INTERRUPT, "onInterrupt")) {
                            onCommandFail(commandType, context, IRouterManager.FAIL_CODE_INTERRUPT, "onInterrupt");
                        }
                    }
                }));
        if (routerService != null) {
            routerService.call(context, commandName, args, callback);
        }
    }
}

ARouterManager類承載了主要的實(shí)現(xiàn)方法揭糕÷芸欤可以看到在類初始化的時(shí)候會(huì)去查找“com.pine.router.command”包下的所有類,而在構(gòu)造方法中會(huì)遍歷這些類著角,找到有注解@ARouterRemoteAction的類揪漩,并根據(jù)Key(模塊標(biāo)記)值,找到對(duì)應(yīng)的RemoteAction并保存起來吏口。
看一下RouterLoginCommand這個(gè)類

@ARouterRemoteAction(Key = ConfigKey.BUNDLE_LOGIN_KEY, RemoteAction = "/login/service")
public interface RouterLoginCommand {
    String goLoginActivity = "goLoginActivity";
    String autoLogin = "autoLogin";
    String logout = "logout";
}

因此奄容,這個(gè)getInstance方法實(shí)際上就是獲取Key值對(duì)應(yīng)的ARouterManager對(duì)象,這個(gè)對(duì)象在初始化的時(shí)候保存RemoteAction到mRemoteAction成員變量中产徊。實(shí)際上這個(gè)mRemoteAction就是Arouter的@Route注解的path值昂勒。
在Welcome模塊中獲取到RouterManager對(duì)象后調(diào)用的callXxxCommand,實(shí)際上最后都會(huì)走到RouterManager的callCommand方法舟铜。
callCommand方法就是我們熟悉的Arouter的使用了戈盈,而這個(gè)mRemoteAction對(duì)應(yīng)的@Route注解的類就是Login模塊的LoginARouterRemote。

@Route(path = "/login/service")
public class LoginARouterRemote extends ARouterBundleRemote<LoginRemoteService> {

}

這個(gè)類是一個(gè)空類谆刨,通過@Route注解讓Arouter找到它塘娶,在其父類ARouterBundleRemote中通過泛型找到實(shí)際的服務(wù)實(shí)現(xiàn)類LoginRemoteService
最后調(diào)用ARouterBundleRemote的call方法痊夭,call方法中通過@RouterCommand注解找到對(duì)應(yīng)的服務(wù)方法刁岸,調(diào)用該方法實(shí)現(xiàn)模塊間的調(diào)用服務(wù)。

本應(yīng)用模板之所以這樣路由的原因她我,主要有兩個(gè):
1. 實(shí)現(xiàn)第三方路由庫的可插拔
2. 進(jìn)一步解耦業(yè)務(wù)代碼和路由代碼


搭架好業(yè)務(wù)模塊的路由功能后难捌,往后的服務(wù)接口的添加只需要兩步:
1. 在router模塊中的RouterXxxCommand中添加一個(gè)表示該服務(wù)接口名的常量yyy
2. 在業(yè)務(wù)模塊中的XxxRemoteService中編寫實(shí)現(xiàn)方法膝宁,并添加注解@RouterCommand,CommandName值為第一步中添加的常量

其它模塊中調(diào)用該服務(wù)接口:

 RouterManager.getInstance(ConfigKey.BUNDLE_XXX_KEY).callOpCommand(context,
                RouterXxxCommand.yyy, args, callback);
2019-08-26變更:

router模塊的command集移到了base模塊中根吁,通過調(diào)用RouterManager的init方法员淫,傳入command集所在的包名來初始化。從而將command集與router模塊分離击敌,使得router模塊成為方法庫介返,在開發(fā)過程中不再需要頻繁更改。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沃斤,一起剝皮案震驚了整個(gè)濱河市圣蝎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌衡瓶,老刑警劉巖徘公,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異哮针,居然都是意外死亡关面,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門十厢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來等太,“玉大人,你說我怎么就攤上這事蛮放∷趼眨” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵包颁,是天一觀的道長瞻想。 經(jīng)常有香客問我,道長娩嚼,這世上最難降的妖魔是什么蘑险? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮待锈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嘴高。我一直安慰自己竿音,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布拴驮。 她就那樣靜靜地躺著春瞬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪套啤。 梳的紋絲不亂的頭發(fā)上宽气,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天随常,我揣著相機(jī)與錄音,去河邊找鬼萄涯。 笑死绪氛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涝影。 我是一名探鬼主播枣察,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼燃逻!你這毒婦竟也來了序目?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤伯襟,失蹤者是張志新(化名)和其女友劉穎猿涨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姆怪,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叛赚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了片效。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片红伦。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖淀衣,靈堂內(nèi)的尸體忽然破棺而出昙读,到底是詐尸還是另有隱情,我是刑警寧澤膨桥,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布蛮浑,位于F島的核電站,受9級(jí)特大地震影響只嚣,放射性物質(zhì)發(fā)生泄漏沮稚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一册舞、第九天 我趴在偏房一處隱蔽的房頂上張望蕴掏。 院中可真熱鬧,春花似錦调鲸、人聲如沸盛杰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽即供。三九已至,卻和暖如春于微,著一層夾襖步出監(jiān)牢的瞬間逗嫡,已是汗流浹背青自。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驱证,地道東北人延窜。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像雷滚,于是被迫代替她去往敵國和親需曾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353