Android元件化框架專案詳解
簡介
什麼是元件化?
專案發展到一定階段時,隨著需求的增加以及頻繁地變更,專案會越來越大,程式碼變得越來越臃腫,耦合會越來越多,開發效率也會降低,這個時候我們就需要對舊專案進行重構即模組的拆分,官方的說法就是元件化。
元件化帶來的好處
那麼,採用元件化能帶來什麼好處呢?主要有以下兩點:
1、現在Android專案中程式碼量達到一定程度,編譯將是一件非常痛苦的事情,一般都需要編譯5到6分鐘。Android Studio 推出 instant run 由於各種缺陷和限制條件(比如採用熱修復tinker)一般情況下是被關閉的。而元件化框架可以使模組單獨編譯除錯,可以有效地減少編譯的時間。
2、通過元件化可以更好的進行並行開發,因為我們可以為每一個模組進行單獨的版本控制,甚至每一個模組的負責人可以選擇自己的設計架構而不影響其他模組的開發,與此同時元件化還可以避免模組之間的交叉依賴,每一個模組的開發人員可以對自己的模組進行獨立測試,獨立編譯和執行,甚至可以實現單獨的部署。從而極大的提高了並行開發效率。
元件化框架
來看元件化一個簡單的例子,圖例如下:
基類庫的封裝
對於Android中常用的基類庫,主要包括開發常用的一些框架。
1、網路請求(多工下載和上傳,採用 Retrofit+RxJava 框架)
2、圖片載入(策略模式,Glide 與 Picasso 之間可以切換)
3、通訊機制(RxBus)
4、基類 adapter 的封裝(支援 item動畫、多佈局item、下拉和載入更多、item點選事件)
5、基類 RecyclerView 的封裝(支援原生風格的下拉載入,item側滑等)
6、mvp 框架
7、各元件的資料庫實體類
8、通用的工具類
9、自定義view(包括對話方塊,ToolBar佈局,圓形圖片等view的自定義)
10、dagger 的封裝(用於初始化全域性的變數和網路請求等配置)
11、其他等等
元件模式和整合模式切換的實現
music元件 下的 build.gradle 檔案,其他元件類似。
/控制組件模式和整合模式 if (rootProject.ext.isAlone) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } apply plugin: 'com.neenbedankt.android-apt' android { compileSdkVersion rootProject.ext.android.compileSdkVersion buildToolsVersion rootProject.ext.android.buildToolsVersion defaultConfig {if (rootProject.ext.isAlone) { //元件模式下設定applicationId applicationId "com.example.cootek.music" } minSdkVersion rootProject.ext.android.minSdkVersion targetSdkVersion rootProject.ext.android.targetSdkVersion versionCode rootProject.ext.android.versionCode versionName rootProject.ext.android.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" if (!rootProject.ext.isAlone) { //整合模式下Arouter的配置,用於元件間通訊的實現 javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } sourceSets { main { //控制兩種模式下的資源和程式碼配置情況 if (rootProject.ext.isAlone) { manifest.srcFile 'src/main/module/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/module/java'] res.srcDirs = ['src/main/res', 'src/main/module/res'] } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) //依賴基類庫 compile project(':commonlibrary') //用作顏色選擇器 compile 'com.afollestad.material-dialogs:commons:0.9.1.0' apt rootProject.ext.dependencies.dagger2_compiler if (!rootProject.ext.isAlone) { //整合模式下需要編譯器生成路由通訊的程式碼 apt rootProject.ext.dependencies.arouter_compiler } testCompile 'junit:junit:4.12' }
為了區分整合模式和元件模式,我們使用isAlone變數來控制。
整合模式
1、首先需要在 config.gradle 檔案中設定 isAlone = false。形如:
ext { isAlone = false; //false:作為Lib元件存在,true:作為application存在
2、然後 Sync 下。
3、最後選擇 app 執行即可。
元件模式
1、首先需要在 config.gradle 檔案中設定 isAlone = true
2、然後 Sync 下。
3、最後相應的模組(new、chat、live、music、app)進行執行即可。
config.gradle 檔案的配置情況如下:
ext { isAlone = false;//false:作為整合模式存在,true:作為元件模式存在 // 各個元件版本號的統一管理 android = [ compileSdkVersion: 24, buildToolsVersion: "25.0.2", minSdkVersion : 16, targetSdkVersion : 22, versionCode : 1, versionName : '1.0.0', ] libsVersion = [ // 第三方庫版本號的管理 supportLibraryVersion = "25.3.0", retrofitVersion = "2.1.0", glideVersion = "3.7.0", loggerVersion = "1.15", // eventbusVersion = "3.0.0", gsonVersion = "2.8.0", butterknife = "8.8.0", retrofit = "2.3.0", rxjava = "2.1.1", rxjava_android = "2.0.1", rxlifecycle = "2.1.0", rxlifecycle_components = "2.1.0", dagger_compiler = "2.11", dagger = "2.11", greenDao = "3.2.2", arouter_api = "1.2.2", arouter_compiler = "1.1.3", transformations = "2.0.2", rxjava_adapter = "2.3.0", gson_converter = "2.3.0", scalars_converter = "2.3.0", rxpermission = "0.9.4", eventbus="3.0.0", support_v4="25.4.0", okhttp3="3.8.1" ] // 依賴庫管理 dependencies = [ appcompatV7 : "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion", design : "com.android.support:design:$rootProject.supportLibraryVersion", cardview : "com.android.support:cardview-v7:$rootProject.supportLibraryVersion", palette : "com.android.support:palette-v7:$rootProject.supportLibraryVersion", recycleview : "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion", support_v4 : "com.android.support:support-v4:$rootProject.support_v4", annotations : "com.android.support:support-annotations:$rootProject.supportLibraryVersion", eventBus : "org.greenrobot:eventbus:$rootProject.eventbus", glide : "com.github.bumptech.glide:glide:$rootProject.glideVersion", gson : "com.google.code.gson:gson:$rootProject.gsonVersion", logger : "com.orhanobut:logger:$rootProject.loggerVersion", butterknife : "com.jakewharton:butterknife:$rootProject.butterknife", butterknife_compiler : "com.jakewharton:butterknife-compiler:$rootProject.butterknife", retrofit : "com.squareup.retrofit2:retrofit:$rootProject.retrofit", okhttp3 : "com.squareup.okhttp3:okhttp:$rootProject.retrofit", retrofit_adapter_rxjava2 : "com.squareup.retrofit2:adapter-rxjava2:$rootProject.rxjava_adapter", retrofit_converter_gson : "com.squareup.retrofit2:converter-gson:$rootProject.gson_converter", retrofit_converter_scalars: "com.squareup.retrofit2:converter-scalars:$rootProject.scalars_converter", rxpermission : "com.tbruyelle.rxpermissions2:rxpermissions:[email protected]", rxjava2 : "io.reactivex.rxjava2:rxjava:$rootProject.rxjava", rxjava2_android : "io.reactivex.rxjava2:rxandroid:$rootProject.rxjava_android", rxlifecycle2 : "com.trello.rxlifecycle2:rxlifecycle:$rootProject.rxlifecycle", rxlifecycle2_components : "com.trello.rxlifecycle2:rxlifecycle-components:$rootProject.rxlifecycle_components", dagger2_compiler : "com.google.dagger:dagger-compiler:$rootProject.dagger_compiler", dagger2 : "com.google.dagger:dagger:$rootProject.dagger", greenDao : "org.greenrobot:greendao:$rootProject.greenDao", transformations : "jp.wasabeef:glide-transformations:$rootProject.transformations", //路由通訊 arouter_api : "com.alibaba:arouter-api:$rootProject.arouter_api", arouter_compiler : "com.alibaba:arouter-compiler:$rootProject.arouter_compiler" ] }
元件間通訊實現
元件間通訊的實現可以使用阿里開源的 Arouter 路由通訊。相關內容可以檢視:https://github.com/alibaba/ARouter。
首先,初始化所有的資料資訊。
private List<MainItemBean> getDefaultData() { List<MainItemBean> result = new ArrayList<>(); MainItemBean mainItemBean = new MainItemBean(); mainItemBean.setName("校園"); mainItemBean.setPath("/news/main"); mainItemBean.setResId(R.mipmap.ic_launcher); MainItemBean music=new MainItemBean(); music.setName("音樂"); music.setResId(R.mipmap.ic_launcher); music.setPath("/music/main"); MainItemBean live = new MainItemBean(); live.setName("直播"); live.setResId(R.mipmap.ic_launcher); live.setPath("/live/main"); MainItemBean chat = new MainItemBean(); chat.setName("聊天"); chat.setPath("/chat/splash"); chat.setResId(R.mipmap.ic_launcher); result.add(mainItemBean); result.add(music); result.add(live); result.add(chat); return result; }
然後在設定每個 item 的點選事件時,啟動元件介面跳轉。
@Override public void onItemClick(int position, View view) { MainItemBean item=mainAdapter.getData(position); ARouter.getInstance().build(item.getPath()).navigation(); }
每個元件入口介面的設定(比如直播 Live 元件,其它元件類似)。
@Route(path = "/live/main") public class MainActivity extends BaseActivity<List<CategoryLiveBean>, MainPresenter> implements View.OnClickListener { // }
res資源和AndroidManifest配置
我們通過判斷元件處於哪種模式來動態設定專案res資源和Manifest、以及程式碼的位置。以直播元件為例,其它元件類似。
作為一個元件模組後,再來看一下直播元件的 build.gradle 檔案對程式碼資源等位置的配置。
sourceSets { main { if (rootProject.ext.isAlone) { manifest.srcFile 'src/main/module/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/module/java'] res.srcDirs = ['src/main/res', 'src/main/module/res'] } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } }
全域性application的實現和資料的初始化
採用類似於 Glide 在 Manifest 初始化配置的方式來初始化各個元件的 Application,下面以直播元件為例來完成初始化,其它類似。
在 BaseApplication 中,初始化 ApplicationDelegate 代理類。
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); applicationDelegate = new ApplicationDelegate(); applicationDelegate.attachBaseContext(base); MultiDex.install(this); }
ApplicationDelegate 內部是怎樣的呢,看一段原始碼。
public class ApplicationDelegate implements IAppLife { private List<IModuleConfig> list; private List<IAppLife> appLifes; private List<Application.ActivityLifecycleCallbacks> liferecycleCallbacks; public ApplicationDelegate() { appLifes = new ArrayList<>(); liferecycleCallbacks = new ArrayList<>(); } @Override public void attachBaseContext(Context base) { //初始化Manifest檔案解析器,用於解析元件在自己的Manifest檔案配置的Application ManifestParser manifestParser = new ManifestParser(base); list = manifestParser.parse(); //解析得到的元件Application列表之後,給每個元件Application注入 //context,和Application的生命週期的回撥,用於實現application的同步 if (list != null && list.size() > 0) { for (IModuleConfig configModule : list) { configModule.injectAppLifecycle(base, appLifes); configModule.injectActivityLifecycle(base, liferecycleCallbacks); } } if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.attachBaseContext(base); } } } @Override public void onCreate(Application application) { //相應呼叫元件Application代理類的onCreate方法 if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.onCreate(application); } } if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) { for (Application.ActivityLifecycleCallbacks life : liferecycleCallbacks) { application.registerActivityLifecycleCallbacks(life); } } } @Override public void onTerminate(Application application) { //相應呼叫元件Application代理類的onTerminate方法 if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.onTerminate(application); } } if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) { for (Application.ActivityLifecycleCallbacks life : liferecycleCallbacks) { application.unregisterActivityLifecycleCallbacks(life); } } } }
元件 Manifest 中 application 的全域性配置如下:
<meta-data android:name="com.example.live.LiveApplication" android:value="IModuleConfig" />
ManifestParser 會對其中 value 為 IModuleConfig 的 meta-data 進行解析,並通過反射生成例項。
public final class ManifestParser { private static final String MODULE_VALUE = "IModuleConfig"; private final Context context; public ManifestParser(Context context) { this.context = context; } public List<IModuleConfig> parse() { List<IModuleConfig> modules = new ArrayList<>(); try { ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA); if (appInfo.metaData != null) { for (String key : appInfo.metaData.keySet()) { //會對其中value為IModuleConfig的meta-data進行解析,並通過反射生成例項 if (MODULE_VALUE.equals(appInfo.metaData.get(key))) { modules.add(parseModule(key)); } } } } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Unable to find metadata to parse IModuleConfig", e); } return modules; } //通過類名生成例項 private static IModuleConfig parseModule(String className) { Class<?> clazz; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Unable to find IModuleConfig implementation", e); } Object module; try { module = clazz.newInstance(); } catch (InstantiationException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } if (!(module instanceof IModuleConfig)) { throw new RuntimeException("Expected instanceof IModuleConfig, but found: " + module); } return (IModuleConfig) module; } }
這樣通過以上步驟就可以在 Manifest 檔案中配置自己元件的 Application,用於初始化元件內的資料,比如在直播元件中初始化 Dagger註解 的全域性配置。
public final class ManifestParser { private static final String MODULE_VALUE = "IModuleConfig"; private final Context context; public ManifestParser(Context context) { this.context = context; } public List<IModuleConfig> parse() { List<IModuleConfig> modules = new ArrayList<>(); try { ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA); if (appInfo.metaData != null) { for (String key : appInfo.metaData.keySet()) { //會對其中value為IModuleConfig的meta-data進行解析,並通過反射生成例項 if (MODULE_VALUE.equals(appInfo.metaData.get(key))) { modules.add(parseModule(key)); } } } } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Unable to find metadata to parse IModuleConfig", e); } return modules; } //通過類名生成例項 private static IModuleConfig parseModule(String className) { Class<?> clazz; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Unable to find IModuleConfig implementation", e); } Object module; try { module = clazz.newInstance(); } catch (InstantiationException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } if (!(module instanceof IModuleConfig)) { throw new RuntimeException("Expected instanceof IModuleConfig, but found: " + module); } return (IModuleConfig) module; } }
這樣通過以上步驟就可以在 Manifest 檔案中配置自己元件的 Application,用於初始化元件內的資料,比如在直播元件中初始化 Dagger註解 的全域性配置。
public class LiveApplication implements IModuleConfig,IAppLife { private static MainComponent mainComponent; @Override public void injectAppLifecycle(Context context, List<IAppLife> iAppLifes) { //這裡需要把本引用新增到Application的生命週期的回撥中,以便實現回撥 iAppLifes.add(this); } @Override public void injectActivityLifecycle(Context context, List<Application.ActivityLifecycleCallbacks> lifecycleCallbackses) { } @Override public void attachBaseContext(Context base) { } @Override public void onCreate(Application application) { //在onCreate方法中對Dagger進行初始化 mainComponent = DaggerMainComponent.builder().mainModule(new MainModule()) .appComponent(BaseApplication.getAppComponent()).build(); } @Override public void onTerminate(Application application) { if (mainComponent != null) { mainComponent = null; } } public static MainComponent getMainComponent() { return mainComponent; } }
元件內網路請求和攔截器
由於每個元件的 BaseUrl 和網路配置等可能不一樣,所以每個元件可以在自己配置的 dagger 中的 MainConponent 實現自己的網路請求和攔截器。以直播為例,部分程式碼內容如下:
MainComponent:
@PerApplication @Component(dependencies = AppComponent.class, modules = MainModule.class) public interface MainComponent { public DaoSession getDaoSession(); public MainRepositoryManager getMainRepositoryManager(); }
MainModule部分程式碼:
public class MainModule { @Provides @PerApplication public MainRepositoryManager provideRepositoryManager(@Named("live") Retrofit retrofit, DaoSession daoSession) { return new MainRepositoryManager(retrofit, daoSession); } @Provides @Named("live") @PerApplication public Retrofit provideRetrofit(@Named("live") OkHttpClient okHttpClient,@Nullable Gson gson){ Retrofit.Builder builder=new Retrofit.Builder().baseUrl(LiveUtil.BASE_URL).addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)).client(okHttpClient); return builder.build(); } @Provides @Named("live") @PerApplication public OkHttpClient provideOkHttpClient(@Named("live")LiveInterceptor interceptor){ OkHttpClient.Builder builder=new OkHttpClient.Builder(); builder.connectTimeout(10, TimeUnit.SECONDS).readTimeout(10,TimeUnit.SECONDS); builder.addInterceptor(interceptor); return builder.build(); } @Provides @Named("live") @PerApplication public LiveInterceptor provideNewsInterceptor(){ return new LiveInterceptor(); } }
難點
在專案中使用元件化,可能會遇到很多問題,下面將問題羅列如下:
資源命名衝突
官方說法是在每個 module 的 build.gradle 檔案中配置資原始檔名字首。
這種方法缺點就是,所有的資源名必須要以指定的字串(moudle_prefix)做字首,否則會異常報錯,而且這方法只限定xml裡面的資源,對圖片資源並不起作用,所以圖片資源仍然需要手動去修改資源名。所以不是很推薦使用這種方法來解決資源名衝突。所以只能自己注意點,在建立資源的時候,儘量不讓其重複。例如:
resourcePrefix "moudle_prefix"
butterKnife使用問題
雖然 Butterknife 支援在 lib 中使用,但是條件是用 R2 代替 R ,在元件模式和整合模式的切換中,R2<->R 之間的切換是無法完成轉換的,切換一次要改動全身,是非常麻煩的!所以不推薦在元件化中使用 Butterknife。
library重複依賴問題
相信這個問題,大家在平時的開發中都會遇到,所以我們需要將多餘的包給排除出去。可以參考如下的配置:
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile(rootProject.ext.dependencies.appcompatV7) { exclude module: "support-v4" exclude module: "support-annotations" } compile rootProject.ext.dependencies.recycleview compile rootProject.ext.dependencies.design compile(rootProject.ext.dependencies.support_v4) { exclude module: "support-annotations" } compile rootProject.ext.dependencies.annotations compile(rootProject.ext.dependencies.butterknife) { exclude module: 'support-annotations' } compile rootProject.ext.dependencies.rxjava2 compile(rootProject.ext.dependencies.rxjava2_android) { exclude module: "rxjava" } compile(rootProject.ext.dependencies.rxlifecycle2) { exclude module: 'rxjava' exclude module: 'jsr305' } compile(rootProject.ext.dependencies.rxlifecycle2_components) { exclude module: 'support-v4' exclude module: 'appcompat-v7' exclude module: 'support-annotations' exclude module: 'rxjava' exclude module: 'rxandroid' exclude module: 'rxlifecycle' } compile(rootProject.ext.dependencies.retrofit) { exclude module: 'okhttp' exclude module: 'okio' } compile(rootProject.ext.dependencies.retrofit_converter_gson) { exclude module: 'gson' exclude module: 'okhttp' exclude module: 'okio' exclude module: 'retrofit' } compile(rootProject.ext.dependencies.retrofit_adapter_rxjava2) { exclude module: 'rxjava' exclude module: 'okhttp' exclude module: 'retrofit' exclude module: 'okio' } compile rootProject.ext.dependencies.greenDao compile rootProject.ext.dependencies.okhttp3 compile rootProject.ext.dependencies.gson compile rootProject.ext.dependencies.glide compile rootProject.ext.dependencies.eventBus compile rootProject.ext.dependencies.dagger2 compile(rootProject.ext.dependencies.rxpermission) { exclude module: 'rxjava' } compile rootProject.ext.dependencies.retrofit_converter_scalars annotationProcessor rootProject.ext.dependencies.dagger2_compiler annotationProcessor rootProject.ext.dependencies.butterknife_compiler compile rootProject.ext.dependencies.butterknife compile rootProject.ext.dependencies.transformations compile rootProject.ext.dependencies.arouter_api }
附:專案例項
優秀專案參考:
MVPArms
https://github.com/JessYanCoding/MVPArms
全民直播
https://github.com/jenly1314/KingTV
音樂專案
https://github.com/hefuyicoder/ListenerMusicPlayer
https://github.com/aa112901/remusic
大象:PHPHub客戶端
https://github.com/Freelander/Elephant
MvpApp
https://github.com/Rukey7/MvpApp
CloudReader
https://github.com/youlookwhat/CloudReader