本篇文章已授權微信公眾號 hongyangAndroid (鴻洋)獨家釋出
背景
當一個專案經過N手人開發,N個產品經理的蹂躪,N長時間的維護,此時一定存在大量程式碼冗餘、業務耦合、專案臃腫,資原始檔大把重複等等,不堪重負。當需要增加新功能或者修改之前某個功能的時候,我相信很多同仁都說只敢增加,不敢隨意的去刪除、修改原有的程式碼,因為不知道哪些有用,哪些沒有用。不但增加了維護成本,也在無形中增加了APK的體積,浪費了資源。 在此背景下,就衍生除了模組化、元件化的概念。目前也已經有很多優秀的案例,我就踩在巨人的肩膀上搭建了符合元件業務的元件化框架。
一.淺談模組
其基本理念就是,把常用的功能、控制元件、基礎類、第三方庫、許可權等公共部分抽離封裝,把業務拆分成N個模組進行獨立(module)的管理,而所有的業務元件都依賴於封裝的基礎庫,業務元件之間不做依賴,這樣的目的是為了讓每個業務模組能單獨執行。而在APP層對整個專案的模組進行組裝,拼湊成一個完整的APP。藉助路由(Arouter)來對各個業務元件之間的跳轉,通過訊息(eventbus)來做各個業務模組之間的通訊。 模組化的好處:
- 1.解耦 只要封裝做得好,實際開發中會省去大量的重複程式碼的coding。
- 2.結構清晰、層次明顯,對後面的維護也是極其容易。
- 3.每個業務模組可獨立執行,單獨提測,節省開發時間。
二.基礎搭建
先來一張整個專案構思圖
根據專案構思圖搭建的專案結構圖
下面逐一介紹每個模組的功:app模組
app殼沒有任何功能主要就是整合每個業務元件,最終打包成一個完整的APK app殼的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_base庫的依賴即可。
common模組無論在什麼情況下都是以library
的形式存在,所有的業務元件都必須依賴於common 其結構如下: 在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"
複製程式碼
修改isModule欄位之後 需要Sysn才會生效
三.搭建過程中遇到的問題
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
檔案。
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'
}
}
}
}
}
...
複製程式碼
注意:在配置Gradle的時候 manifest.srcFile... manifest 是小寫的
其中整合模式載入的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.不同元件之間的跳轉
業務元件之間沒有依賴,不能通過常規的Intent
顯示的進行跳轉,這個時候就需要引入路由的概念
利用阿里的ARouter對需要跳轉的頁面做配置
gradle
配置
android {
...
defaultConfig {
...
//Arouter路由配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
includeCompileClasspath = true
}
}
}
}
dependencies{
...
//Arouter路由
annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}
複製程式碼
目標頁面配置
@Route(path = "/wan/WanMainActivity")
public class WanMainActivity extends ActionBarActivity<WanMainPresenter> implements IWanMainView, OnRcyItemClickListener {
...
}
複製程式碼
跳轉
...
ARouter.getInstance()
.build("/wan/WanMainActivity")
.navigation();
...
複製程式碼
4.不同元件之間通訊
可以利用第三方 如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.資原始檔衝突問題
目前沒有比較好的約束方式,只能通過設定資源的字首來防止資原始檔衝突,然後在提交程式碼的時候對程式碼進行檢查是否規範來控制。
最後放上Demo地址,共同學習,有什麼不好的地方,歡迎大家指出!