Android組件化框架搭建

本篇文章已授權(quán)微信公眾號 hongyangAndroid (鴻洋)獨家發(fā)布

背景

當(dāng)一個項目經(jīng)過N手人開發(fā),N個產(chǎn)品經(jīng)理的蹂躪,N長時間的維護,此時一定存在大量代碼冗余、業(yè)務(wù)耦合抖坪、項目臃腫,資源文件大把重復(fù)等等闷叉,不堪重負擦俐。當(dāng)需要增加新功能或者修改之前某個功能的時候,我相信很多同仁都說只敢增加握侧,不敢隨意的去刪除蚯瞧、修改原有的代碼,因為不知道哪些有用品擎,哪些沒有用埋合。不但增加了維護成本,也在無形中增加了APK的體積萄传,浪費了資源甚颂。
在此背景下,就衍生除了模塊化秀菱、組件化的概念振诬。目前也已經(jīng)有很多優(yōu)秀的案例,我就踩在巨人的肩膀上搭建了符合組件業(yè)務(wù)的組件化框架衍菱。

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

效果圖.png

一.淺談模塊

其基本理念就是,把常用的功能脊串、控件辫呻、基礎(chǔ)類、第三方庫洪规、權(quán)限等公共部分抽離封裝印屁,把業(yè)務(wù)拆分成N個模塊進行獨立(module)的管理,而所有的業(yè)務(wù)組件都依賴于封裝的基礎(chǔ)庫斩例,業(yè)務(wù)組件之間不做依賴,這樣的目的是為了讓每個業(yè)務(wù)模塊能單獨運行从橘。而在APP層對整個項目的模塊進行組裝念赶,拼湊成一個完整的APP础钠。借助路由(Arouter)來對各個業(yè)務(wù)組件之間的跳轉(zhuǎn),通過消息(eventbus)來做各個業(yè)務(wù)模塊之間的通信叉谜。
模塊化的好處:

  • 1.解耦 只要封裝做得好旗吁,實際開發(fā)中會省去大量的重復(fù)代碼的coding。
  • 2.結(jié)構(gòu)清晰停局、層次明顯很钓,對后面的維護也是極其容易。
  • 3.每個業(yè)務(wù)模塊可獨立運行董栽,單獨提測码倦,節(jié)省開發(fā)時間。

二.基礎(chǔ)搭建

先來一張整個項目構(gòu)思圖


項目構(gòu)思圖

根據(jù)項目構(gòu)思圖搭建的項目結(jié)構(gòu)圖


項目結(jié)構(gòu)圖

下面逐一介紹每個模塊的功:

  • app模塊 app殼沒有任何功能主要就是集成每個業(yè)務(wù)組件锭碳,最終打包成一個完整的APK
    app殼的gradle做如下配置袁稽,根據(jù)配置文件中的isModule字段來依賴不同的業(yè)務(wù)組件
...
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模塊功能組件主要負責(zé)封裝公共部分,如第三方庫加載擒抛、網(wǎng)絡(luò)請求推汽、數(shù)據(jù)存儲、自定義控件歧沪、各種工具類等歹撒。
    為了防止重復(fù)依賴問題,所有的第三方庫都放在該模塊加載诊胞,業(yè)務(wù)模塊不在做任何的第三方庫依賴暖夭,只做common_base庫的依賴即可。
    common模塊無論在什么情況下都是以library的形式存在厢钧,所有的業(yè)務(wù)組件都必須依賴于common
    其結(jié)構(gòu)如下:
    common_base結(jié)構(gòu)圖.png

    在commong的gradle中引入項目中使用的所有第三方庫鳞尔,業(yè)務(wù)組件就不用再去逐一引入
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

...

dependencies {
    // 在項目中的libs中的所有的.jar結(jié)尾的文件,都是依賴
    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 發(fā)布/訂閱事件總線
    api rootProject.ext.dependencies["eventbus"]

    //網(wǎng)絡(luò)
    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"]

}
  • 業(yè)務(wù)組件早直,在集成模式下它以library的形式存在寥假。在組件開發(fā)模式下它以application的形式存在,可以單獨獨立運行霞扬。
    業(yè)務(wù)組件完整的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的版本等配置
/**
 *  全局統(tǒng)一配置文件
 */
ext {
    //true 每個業(yè)務(wù)Module可以單獨開發(fā)
    //false 每個業(yè)務(wù)Module以lib的方式運行
    //修改之后需要Sync方可生效
    isModule = false
    
    //版本號
    versions = [
            applicationId           : "com.wss.amd",        //應(yīng)用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"]}",

                    //方法數(shù)超過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"]}",

                    //網(wǎng)絡(luò)
                    "novate"              : "com.tamic.novate:novate:${versions["novateVersion"]}",

                    //日志
                    "logger"              : "com.orhanobut:logger:${versions["loggerVersion"]}",

                    //fastJson
                    "fastjson"            : "com.alibaba:fastjson:${versions["fastjsonVersion"]}.android",

                    //沉浸式狀態(tài)欄
                    "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"]}",

                    //數(shù)據(jù)庫
                    "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進行合并,所以每個業(yè)務(wù)組件的Activity只需要在各自模塊的AndroidManifest中注冊即可。如果業(yè)務(wù)組件需要獨立運行愚墓,則需要單獨配置一份AndroidManifest予权,在gradlesourceSets根據(jù)不同的模式加載不同的AndroidManifest文件。

業(yè)務(wù)組件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中不能設(shè)置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>

需要注意的是如果在組件開發(fā)模式下浪册,組件的Applicaion必須繼承自BaseApplicaion

3.不同組件之間的跳轉(zhuǎn)

業(yè)務(wù)組件之間沒有依賴扫腺,不能通過常規(guī)的Intent顯示的進行跳轉(zhuǎn),這個時候就需要引入路由的概念

路由

利用阿里的ARouter對需要跳轉(zhuǎn)的頁面做配置
gradle配置

android {
   ...
       defaultConfig {
         ...
        //Arouter路由配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath = true
            }
        }
    }
}
dependencies{
     ...
    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}

目標(biāo)頁面配置

@Route(path = "/wan/WanMainActivity")
public class WanMainActivity extends ActionBarActivity<WanMainPresenter> implements IWanMainView, OnRcyItemClickListener {
      ...
}

跳轉(zhuǎn)

...
   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.資源文件沖突問題

目前沒有比較好的約束方式习绢,只能通過設(shè)置資源的前綴來防止資源文件沖突,然后在提交代碼的時候?qū)Υa進行檢查是否規(guī)范來控制蝙昙。


資源文件命名.jpg

其中使用的MVP結(jié)構(gòu)可以參考另一篇文章

最后放上Demo地址闪萄,共同學(xué)習(xí),有什么不好的地方奇颠,歡迎大家指出败去!

參考文獻
移動架構(gòu)這么多,如何一次搞定所有
戲說移動江湖開發(fā)歷程
模塊化烈拒,組件化傻傻分不清圆裕?附帶組件化福利
寄Android開發(fā)Gradle你需要知道的知識
解決組件化開發(fā)butterknife 在 library中使用的坑`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荆几,隨后出現(xiàn)的幾起案子吓妆,更是在濱河造成了極大的恐慌,老刑警劉巖吨铸,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件行拢,死亡現(xiàn)場離奇詭異,居然都是意外死亡诞吱,警方通過查閱死者的電腦和手機舟奠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來房维,“玉大人沼瘫,你說我怎么就攤上這事×” “怎么了耿戚?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我溅话,道長晓锻,這世上最難降的妖魔是什么歌焦? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任飞几,我火速辦了婚禮,結(jié)果婚禮上独撇,老公的妹妹穿的比我還像新娘屑墨。我一直安慰自己,他們只是感情好纷铣,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布卵史。 她就那樣靜靜地躺著,像睡著了一般搜立。 火紅的嫁衣襯著肌膚如雪以躯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天啄踊,我揣著相機與錄音忧设,去河邊找鬼。 笑死颠通,一個胖子當(dāng)著我的面吹牛址晕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顿锰,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谨垃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了硼控?” 一聲冷哼從身側(cè)響起刘陶,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牢撼,沒想到半個月后匙隔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡浪默,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年牡直,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纳决。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡碰逸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阔加,到底是詐尸還是另有隱情饵史,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站胳喷,受9級特大地震影響湃番,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吭露,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一吠撮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讲竿,春花似錦泥兰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至迈嘹,卻和暖如春削彬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秀仲。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工融痛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人啄育。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓酌心,卻偏偏與公主長得像,于是被迫代替她去往敵國和親挑豌。 傳聞我的和親對象是個殘疾皇子安券,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android組件化項目地址:Android組件化項目AndroidModulePattern Android組件...
    半灬邊灬天閱讀 2,922評論 4 37
  • 前言 組件化是什么,是把一個功能完整的 App 或模塊拆分成多個子模塊, 表現(xiàn)在androidStudio項目工程...
    嘎啦果安卓獸閱讀 8,547評論 6 121
  • 問題 在已經(jīng)開發(fā)過幾個項目的童鞋,如果這時需要重新開發(fā)一個新項目,是否需要自己重新搭建框架呢,還是從老項目中拷貝粘...
    8ba406212441閱讀 43,057評論 84 381
  • 不怕跌倒氓英,所以飛翔 組件化開發(fā) 參考資源 Android組件化方案 為什么要組件化開發(fā) 解決問題 實際業(yè)務(wù)變化非常...
    筆墨Android閱讀 2,984評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,182評論 25 707