不可錯過的新鮮出爐的通用Android組件化Demo

(本文提出的組件化項目已經開源,參見YouJu。*注:請勿商用,若有違反,責任自負)git

前言(廢話)

最近有段空閒期,公司的這個項目一直由我負責,以前一直爲了效率爲忽略了質量,加上以前項目的功能的不斷疊加,因此如今項目體積變得很是龐大且冗雜。可是考慮到往後可能其餘人接手,並且本身有點完美主義和強迫症,就想利用這段時間對項目進行重構。重構的目的無非是使項目功能及模塊具體劃分開來,避免過於耦合,牽一髮而動全身,這樣後續的功能開發和維護也會更加方便和快速。具體功能模塊單獨抽離出來,不會影響其餘的模塊。這樣每一個人只須要負責本身的那部分工做就能夠了。天然而然想到了組件化。其實關於組件化的文章已經處處都是了。可是始終都是別人的思想轉化出來的文字。本身可能看一篇就過了,不會有多麼深刻的理解和記憶。因而就想單獨寫一套而且搭建一套往後能夠通用的開發框架。因而就有了這個項目,到如今項目基本已經成型了,而且目前也已經實現了不少功能和內容。github

概述

YouJu是一款集多個平臺的資訊內容、複合型優秀資源和實用功能以及多種炫酷UI特效的組件化Demo,打破傳統限制,融合多元,發現更多、更優秀、更有趣的東西。 項目是一款組件化綜合案例,包含新聞,MONO資訊,開眼視頻,知乎日報,垃圾分類搜索,一言,古詩詞,智能聊天機器人,語音識別功能,乾貨集中營,豆瓣影視等等模塊。項目利用業餘時間開發,時間不固定,暫時只實現了這些功能。後續也會加入更多新的功能。參照該項目少量配置便可適用於任何通用項目開發框架,簡單易上手,傻瓜式操做。

當前模塊有:base基礎模塊、network網絡請求模塊、語音識別模塊、工具模塊、乾貨模塊、資訊模塊。除base和network外,每一個模塊均可單獨抽離爲APP運行,不影響主工程,具體參照config.gradle配置文件

項目採用組件化架構模式以及MVP+Walle+ARouter+AndroidX+Dagger2+Retrofit2+Okhttp3+ RxJava2+EventBus+ Kotlin/Java混編模式開發。

整個項目框架結構bash

流程

如上圖所示,base和network模塊做爲底層公用library提供給中間層,中間層能夠單獨做爲一個APP運行,也可做爲library提供給App殼做爲功能模塊。只須要在config.gradle中配置相應參數,這樣極大的簡化了複雜度和開發的成本。若是須要開發一個新模塊,直接繼承依賴base或者network模塊便可。

分析

此外,base還提供了ServiceProvider和Interceptor供上層調用,ServiceProvider能夠提供一個服務給外部調用,好比A模塊中的AServiceProvider依賴BaseServiceProvider,BaseServiceProvider是base中定義的基類提供器。A模塊可使用AServiceProvider提供AFragment或者其餘A模塊中的內容或者方法給B模塊使用,只須要AServiceProvider配置一個路徑,而後B模塊訪問這個路徑,ARouter會去尋找這個路徑,若是找到B模塊就能夠獲得AServiceProvider實例服務,而後執行相應的業務邏輯。找不到也不會崩潰。這樣就實現了跨模塊服務調用。網絡

public interface BaseServiceProvider extends IProvider {
    BaseFragment newInstance(Object... args);
    
    void customFun(Bundle bundle);
}
複製代碼
@Route(path = ConfigConstants.PATH_MODULE_PROVIDER)
public class AServiceProvider implements BaseServiceProvider {

    @Override
    public void init(Context context) {

    }

    @Override
    public BaseFragment newInstance(Object... args) {
        return AFragment.getInstance();
    }
    
    @Override
    public void customFun(Bundle bundle) {
         ......
    }
}
複製代碼


Interceptor是做爲一個攔截器,它做用於整個ARouter頁面跳轉,也能夠配置某個頁面跳轉不經過攔截器。取決於具體業務場景。常規的就是登陸跳轉攔截,A→B,若是已經登陸直接到B,若是沒有登陸,先去C登陸,登陸成功後直接到B,B若是返回直接到A。架構

@Interceptor(priority = 5, name = "login")
public class LoginInterceptor implements IInterceptor {
    private Context mContext;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        Bundle extras = postcard.getExtras();
        boolean isNeedIntercept = false;
        if (extras != null) {
            isNeedIntercept = extras.getBoolean(ConfigConstants.IS_NEED_INTERCEPT, false);
        }
        if (isNeedIntercept) {//須要執行登陸攔截邏輯
            boolean isNeedLogin = new Random().nextInt(10) % 2 == 0;
            if (isNeedLogin) { //須要登陸
                //主線程跳轉登陸頁面(走綠色通道,不走攔截器)
                Single.just(postcard)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new SingleObserver<Postcard>() {
                            @Override
                            public void onSubscribe(Disposable d) {
                            }

                            @Override
                            public void onSuccess(Postcard postcard) {
                                Bundle bundle = null;
                                if (null != postcard) {
                                    String targetPath = postcard.getPath();
                                    bundle = postcard.getExtras();
                                    if (null != bundle && !TextUtils.isEmpty(targetPath) && !TextUtils.equals(ConfigConstants.PATH_LOGIN, targetPath)) {
                                        bundle.putString(ConfigConstants.PATH_TARGET, targetPath);
                                    }
                                }
                                ARouter.getInstance().build(ConfigConstants.PATH_LOGIN)
                                        .with(bundle)
                                        .greenChannel()
                                        .navigation();
                            }

                            @Override
                            public void onError(Throwable e) {
                                e.printStackTrace();
                            }
                        });
                callback.onInterrupt(null);
            } else {//不須要登陸
                callback.onContinue(postcard);
            }
        } else {//不須要攔截
            callback.onContinue(postcard);
        }
    }

    @Override
    public void init(Context context) {
        // 攔截器的初始化,會在sdk初始化的時候調用該方法,僅會調用一次
        mContext = context;
    }
}
複製代碼

有時候咱們還須要在Application中初始化一些東西,可是各個模塊又是能夠單獨抽離的,因此不能將全部的初始化操做都放在一個Application中,否則仍是會耦合,這裏呢能夠採用在base模塊中定義一個BaseApplication,用於初始化base模塊中須要初始化的操做,而後其餘中間層模塊若是須要初始化,就能夠經過MouleApplication繼承BaseApplication,而後執行所須要的初始化。這種狀況能夠適用於中間層模塊做爲單獨APP運行時,可是當中間層模塊做爲library提供給殼時,這個時候整個應用只有一個Application,也就是殼的Application,因此中間層模塊的MouleApplication這個時候不會被執行。這個時候就須要另一種方式去實現。能夠在base模塊中定義一個BaseApplicationImpl接口,而後須要初始化的中間模塊能夠實現BaseApplicationImpl,而後在BaseApplication中分別遍歷執行全部BaseApplicationImpl的實現類。剩下的就是把每個實現類的具體路徑加入到初始化路徑列表ModuleConfig.MODULESLIST中便可。app

public interface BaseApplicationImpl {
    void onCreate(BaseApplication application);
}
複製代碼
private void modulesApplicationInit() {
        for (String moduleImpl : ModuleConfig.MODULESLIST) {
            try {
                Class<?> clazz = Class.forName(moduleImpl);
                Object obj = clazz.newInstance();
                if (obj instanceof BaseApplicationImpl) {
                    ((BaseApplicationImpl) obj).onCreate(this);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
複製代碼
public class ModuleConfig {
    public static final String[] MODULESLIST = {
            "com.heyongrui.iflytek.mscv5plusdemo.IflytedApplicationImpl"
    };
}
複製代碼
public class IflytedApplicationImpl implements BaseApplicationImpl {

    @Override
    public void onCreate(BaseApplication application) {
        StringBuffer param = new StringBuffer();
        param.append("appid=" + application.getString(R.string.app_id));
        param.append(",");
        param.append(SpeechConstant.ENGINE_MODE + "=" + SpeechConstant.MODE_MSC);
        SpeechUtility.createUtility(application, param.toString());
    }
}
複製代碼

若是A模塊的某個頁面須要跳轉到B模塊的某個頁面,可是App殼只依賴了A模塊,這個時候就會跳轉失敗,並且還會彈出Toast,就會很煩,對於用戶體驗是很差的;亦或是A模塊的某個頁面的一個View須要根據App殼是否依賴B模塊動態設置顯示隱藏,這個時候能夠經過checkIsHasPath方法去判斷,這樣就能夠兼容到兩種狀況並且不會彈出Toast提示。框架

private boolean checkIsHasPath(String path) {
        try {
            String group = path.substring(1, path.indexOf("/", 1));
            LogisticsCenter.completion(new Postcard(path, group));
        } catch (Exception e) {
            return false;
        }
        return true;
    }
複製代碼

感悟

以上就是該項目的大體狀況經過這個項目,中間也遇到了很多的問題,作起來其實沒有提及來那麼容易,體會到了抽絲剝繭的艱難。找到一個合適的方案當然重要,更重要的是克服重重困難堅決的實施下去。在整個過程當中,不斷去完善才有了如今的樣子。總的來講,它學習成本比較低,能夠快速的入手,最主要的是代碼結構清晰了不少。若是你也面臨着龐大的工程或者在重構的邊緣猶豫不決,建議你趕忙行動起來,否則後面你會更難行走。也歡迎使用該方案,以最小的代價儘快開始實施組件化。若是你如今負責的是一個開發初期的項目,代碼量還不大,那麼更建議儘快進行組件化的規劃,不要給將來的本身增長徒勞的工做量。dom

github源碼已上傳,喜歡的給個star,歡迎小夥伴fork~ide

*注:此項目屬於業餘時間練手的項目,接口數據來源均來自網絡,若是存在侵權狀況,請第一時間告知。本項目僅作學習交流使用,API數據內容全部權歸原做公司全部,請勿用於其餘用途!!

相關文章
相關標籤/搜索