Android元件化框架搭建
背景
當一個專案經過N手人開發,N個產品經理的蹂躪,N長時間的維護,此時一定存在大量程式碼冗餘、業務耦合、專案臃腫,資原始檔大把重複等等,不堪重負。當需要增加新功能或者修改之前某個功能的時候,我相信很多同仁都說只敢增加,不敢隨意的去刪除、修改原有的程式碼,因為不知道哪些有用,哪些沒有用。不但增加了維護成本,也在無形中增加了APK的體積,浪費了資源。
在此背景下,就衍生除了模組化、元件化的概念。目前也已經有很多優秀的案例,我就踩在巨人的肩膀上搭建了符合元件業務的元件化框架。
一.基礎搭建
先來一張整個專案構思圖

專案構思圖
根據專案構思圖搭建的專案結構圖

專案結構圖
下面逐一介紹每個模組的功:
-
app模組
app殼沒有任何功能主要就是整合每個業務元件,最終打包成一個完整的APKapp殼的
gradle
做如下配置,根據配置檔案中的isModule
欄位來依賴不同的業務元件
... dependencise{ //公用依賴包 implementation project(':common_base') if (!Boolean.valueOf(rootProject.ext.isModule)) { //main模組 implementation project(':module_main') implementation project(':module_market') implementation project(':module_wan_android') } } ...
-
common_base模組
功能元件主要負責封裝公共部分,如第三方庫載入、網路請求、資料儲存、自定義控制元件、各種工具類等common模組無論在什麼情況下都是以
library
的形式存在,所有的業務元件都必須依賴於common其結構如下:
common_base結構圖.png
在commong的
gradle
中引入專案中使用的所有第三方庫,業務元件就不用再去逐一引入
apply plugin: 'com.android.library' apply plugin: 'com.jakewharton.butterknife' ... dependencies { // 在專案中的libs中的所有的.jar結尾的檔案,都是依賴 implementation fileTree(dir: 'libs', include: ['*.jar']) //把implementation 用api代替,它是對外部公開的, 所有其他的module就不需要新增該依賴 api rootProject.ext.dependencies["appcompat_v7"] api rootProject.ext.dependencies["constraint_layout"] api rootProject.ext.dependencies["cardview-v7"] api rootProject.ext.dependencies["recyclerview-v7"] api rootProject.ext.dependencies["support-v4"] api rootProject.ext.dependencies["design"] api rootProject.ext.dependencies["support_annotations"] //MultiDex分包方法 api rootProject.ext.dependencies["multidex"] //黃油刀 annotationProcessor rootProject.ext.dependencies["butterknife_compiler"] api rootProject.ext.dependencies["butterknife"] //Arouter路由 annotationProcessor rootProject.ext.dependencies["arouter_compiler"] api rootProject.ext.dependencies["arouter_api"] api rootProject.ext.dependencies["arouter_annotation"] //eventbus 釋出/訂閱事件匯流排 api rootProject.ext.dependencies["eventbus"] //網路 api rootProject.ext.dependencies["novate"] //日誌 api rootProject.ext.dependencies["logger"] //fastJson api rootProject.ext.dependencies["fastjson"] //沉浸欄 api rootProject.ext.dependencies["barlibrary"] //banner api rootProject.ext.dependencies["banner"] //圖片載入 api rootProject.ext.dependencies["picasso"] //lombok api rootProject.ext.dependencies["lombok"] api rootProject.ext.dependencies["lombokJavax"] }
-
業務元件,在整合模式下它以
library
的形式存在。在元件開發模式下它以application
的形式存在,可以單獨獨立執行。業務元件完整的
gradle
如下:
if (Boolean.valueOf(rootProject.ext.isModule)) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } apply plugin: 'com.jakewharton.butterknife' ... dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //公用依賴包 implementation project(':common_base') //Arouter路由 annotationProcessor rootProject.ext.dependencies["arouter_compiler"] //黃油刀 annotationProcessor rootProject.ext.dependencies["butterknife_compiler"] }
- 配置檔案,對專案中的第三庫、app的版本等配置
/** *全域性統一配置檔案 */ ext { //true 每個業務Module可以單獨開發 //false 每個業務Module以lib的方式執行 //修改之後需要Sync方可生效 isModule = false //版本號 versions = [ applicationId: "com.wss.amd",//應用ID versionCode: 1,//版本號 versionName: "1.0.0",//版本名稱 compileSdkVersion: 27, buildToolsVersion: "27.0.3", minSdkVersion: 17, targetSdkVersion: 23, androidSupportSdkVersion: "27.1.1", constraintLayoutVersion : "1.1.1", runnerVersion: "1.0.1", espressoVersion: "3.0.1", junitVersion: "4.12", annotationsVersion: "24.0.0", multidexVersion: "1.0.2", butterknifeVersion: "8.4.0", arouterApiVersion: "1.4.0", arouterCompilerVersion: "1.2.1", arouterannotationVersion: "1.0.4", eventbusVersion: "3.0.0", novateVersion: "1.5.5", loggerVersion: "2.2.0", fastjsonVersion: "1.1.54", barlibraryVersion: "2.3.0", picassoVersion: "2.71828", bannerVersion: "1.4.10", javaxVersion: "1.2", lombokVersion: "1.16.6", greendaoVersion: "3.2.2", ] dependencies = ["appcompat_v7": "com.android.support:appcompat-v7:${versions["androidSupportSdkVersion"]}", "constraint_layout": "com.android.support.constraint:constraint-layout:${versions["constraintLayoutVersion"]}", "runner": "com.android.support.test:runner:${versions["runnerVersion"]}", "espresso_core": "com.android.support.test.espresso:espresso-core:${versions["espressoVersion"]}", "junit": "junit:junit:${versions["junitVersion"]}", "support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}", "design": "com.android.support:design:${versions["androidSupportSdkVersion"]}", "support-v4": "com.android.support:support-v4:${versions["androidSupportSdkVersion"]}", "cardview-v7": "com.android.support:cardview-v7:${versions["androidSupportSdkVersion"]}", "recyclerview-v7": "com.android.support:recyclerview-v7:${versions["androidSupportSdkVersion"]}", //方法數超過65535解決方法64K MultiDex分包方法 "multidex": "com.android.support:multidex:${versions["multidexVersion"]}", //路由 "arouter_api": "com.alibaba:arouter-api:${versions["arouterApiVersion"]}", "arouter_compiler": "com.alibaba:arouter-compiler:${versions["arouterCompilerVersion"]}", "arouter_annotation": "com.alibaba:arouter-annotation:${versions["arouterannotationVersion"]}", //黃油刀 "butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}", "butterknife": "com.jakewharton:butterknife:${versions["butterknifeVersion"]}", //事件訂閱 "eventbus": "org.greenrobot:eventbus:${versions["eventbusVersion"]}", //網路 "novate": "com.tamic.novate:novate:${versions["novateVersion"]}", //日誌 "logger": "com.orhanobut:logger:${versions["loggerVersion"]}", //fastJson "fastjson": "com.alibaba:fastjson:${versions["fastjsonVersion"]}.android", //沉浸式狀態列 "barlibrary": "com.gyf.barlibrary:barlibrary:${versions["barlibraryVersion"]}", //banner "banner": "com.youth.banner:banner:${versions["bannerVersion"]}", //圖片載入 "picasso": "com.squareup.picasso:picasso:${versions["picassoVersion"]}", //lombok "lombokJavax": "javax.annotation:javax.annotation-api:${versions["javaxVersion"]}", "lombok": "org.projectlombok:lombok:${versions["lombokVersion"]}", //資料庫 "greenDao": "org.greenrobot:greendao:${versions["greendaoVersion"]}", ] }
最後別忘記在工程的中 build.gradle
引入該配置檔案
apply from: "config.gradle" ...
二.搭建過程中遇到的問題
1. Application
、全域性 Context
、 Activity
管理問題
- 在功能元件即Demo中的
common_base
封裝BaseApplication
,在BaseApplication
對第三方庫初始化、全域性Context
的獲取等操作。在BaseActivity
中對Activity
進行新增和移除的管理
//BaseApplicion public class BaseApplication extends Application { ... //全域性唯一的context private static BaseApplication application; //Activity管理器 private ActivityManage activityManage; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); application = this; //MultiDex分包方法 必須最先初始化 MultiDex.install(this); } public void onCreate() { super.onCreate(); activityManage = new ActivityManage(); initARouter(); initLogger(); } /** * 獲取全域性唯一上下文 * * @return BaseApplication */ public static BaseApplication getApplication() { return application; } } //BaseActivity public abstract class BaseActivity extends Activity { ... @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //加入Activity管理器 BaseApplication.getApplication().getActivityManage().addActivity(this); } @Override protected void onDestroy() { super.onDestroy(); //將Activity從管理器移除 BaseApplication.getApplication().getActivityManage().removeActivityty(this); } }
2. AndroidManifest
的管理
我們知道APP在打包的時候最後會把所有的 AndroidManifest
進行合併,所以每個業務元件的 Activity
只需要在各自模組的 AndroidManifest
中註冊即可。如果業務元件需要獨立執行,則需要單獨配置一份 AndroidManifest
,在 gradle
的 sourceSets
根據不同的模式載入不同的 AndroidManifest
檔案。

業務元件Manifest.png
gradle
配置
... android { ... sourceSets { main { if (Boolean.valueOf(rootProject.ext.isModule)) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { //排除java/debug資料夾下的所有檔案 exclude '*module' } } } } } ...
其中整合模式載入的 Manifest
中不能設定 Application
和程式入口:
//整合模式下Manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wss.module.wan"> <application> <activity android:name=".main.WanMainActivity" /> </application> </manifest> //元件模式下Manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wss.module.wan"> <application android:name=".common.WanApplication" android:allowBackup="true" android:label="@string/app_name" android:theme="@style/AdmTheme"> <activity android:name=".main.WanMainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
需要注意的是如果在元件開發模式下,元件的 Applicaion
必須繼承自 BaseApplicaion
3.不同元件之間的跳轉
利用阿里的 ofollow,noindex">ARouter 對需要跳轉的頁面做配置
gradle
配置
android { ... defaultConfig { ... //Arouter路由配置 javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] includeCompileClasspath = true } } } }
目標頁面配置
@Route(path = "/wan/WanMainActivity") public class WanMainActivity extends ActionBarActivity<WanMainPresenter> implements IWanMainView, OnRcyItemClickListener { ... }
跳轉
... ARouter.getInstance() .build("/wan/WanMainActivity") .navigation(); ...
4.不同元件之間通訊
可以利用第三方 如 EventBus" target="_blank" rel="nofollow,noindex">EventBus 對訊息進行管理。在 common_base
元件中的 Base
類做了對訊息的簡單封裝,子類只需要重寫 regEvent()
返回 true
即可對事件的註冊,重寫 onEventBus(Object)
即可對事件的接收。
public abstract class BaseActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... if (regEvent()) { EventBus.getDefault().register(this); } } @Override protected void onDestroy() { super.onDestroy(); if (regEvent()) { EventBus.getDefault().unregister(this); } } /** * 子類接收事件 重寫該方法 */ @Subscribe(threadMode = ThreadMode.MAIN) public void onEventBus(Object event) { } /** * 需要接收事件 重寫該方法 並返回true */ protected boolean regEvent() { return false; }
5. butterknife
的問題
在 library
中使用 butterknife
會存在找不到的問題。
推薦使用 8.4.0
版本,用 R2
代替 R
, onClick
中使用 if else
不要使用 switch case
即可解決問題 。
public class HomeFragment extends BaseMvpFragment<HomePresenter> implements IHomeView, OnRcyItemClickListener { @BindView(R2.id.banner) Banner banner; @BindView(R2.id.recycle_view) RecyclerView recyclerView; ... @OnClick({R2.id.tv_title, R2.id.btn_open}) public void onClick(View v) { if (v.getId() == R.id.tv_title) { //do something } else if (v.getId() == R.id.btn_open) { //do something } } }
6.資原始檔衝突問題
目前沒有比較好的約束方式,只能通過設定資源的字首來防止資原始檔衝突,然後在提交程式碼的時候對程式碼進行檢查是否規範來控制。

資原始檔命名.jpg
,共同學習,有什麼不好的地方,歡迎大家指出!