Android元件化框架搭建

吳蜀黍_發表於2018-11-22

本篇文章已授權微信公眾號 hongyangAndroid (鴻洋)獨家釋出

背景

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

先放一個Demo地址,文章末尾也有

效果圖.png

一.淺談模組

其基本理念就是,把常用的功能、控制元件、基礎類、第三方庫、許可權等公共部分抽離封裝,把業務拆分成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 其結構如下:
    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"
複製程式碼

修改isModule欄位之後 需要Sysn才會生效

三.搭建過程中遇到的問題

1.Application、全域性ContextActivity管理問題

  • 在功能元件即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,在gradlesourceSets根據不同的模式載入不同的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'
                }
            }
        }
    }
}
...
複製程式碼

注意:在配置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.資原始檔衝突問題

目前沒有比較好的約束方式,只能通過設定資源的字首來防止資原始檔衝突,然後在提交程式碼的時候對程式碼進行檢查是否規範來控制。

資原始檔命名.jpg

其中使用的MVP結構可以參考另一篇文章

最後放上Demo地址,共同學習,有什麼不好的地方,歡迎大家指出!

參考文獻

移動架構這麼多,如何一次搞定所有

戲說移動江湖開發歷程

模組化,元件化傻傻分不清?附帶元件化福利

寄Android開發Gradle你需要知道的知識

解決元件化開發butterknife 在 library中使用的坑

相關文章