Android組件化方案思路

在進(jìn)行app組件化之前我們要明白什么是組件化?為什么要組件化?

什么是組件化?為什么要組件化?

????在項(xiàng)目的體系結(jié)構(gòu),代碼量,功能,邏輯等不斷的增長(zhǎng)之后,項(xiàng)目的編譯,開發(fā)的協(xié)作,效率等都會(huì)變得毫無體驗(yàn)。所以我們就要相處一個(gè)辦法來解決這個(gè)事情。一般來說會(huì)有如下幾種思路

1,組件化
2,插件化
3,模塊化

????如果對(duì)于app的動(dòng)態(tài)化,體積等有比較大的考量則插件化是不錯(cuò)的選擇。但是一般來說市面上除非像淘寶這種超級(jí)app,有這強(qiáng)大的技術(shù)底蘊(yùn)的團(tuán)隊(duì)的app,其實(shí)大部分的app是不需要必須使用插件化的技術(shù)的。有些公司其實(shí)是完全跟風(fēng)罷了。而且在安卓9.0之后插件化的提要其實(shí)并不友好,所以一般非必須的情況下并不臺(tái)推薦使用插件化。

????模塊化我理解則是將組件化更加細(xì)粒度的進(jìn)行拆分,比如可能會(huì)將一個(gè)輸入欄作為一個(gè)模塊進(jìn)行復(fù)用,比如QQ的聊天輸入,或者微信的聊天輸入等類似的。這樣的場(chǎng)景架構(gòu)其實(shí)也是非一般公司可為。

????當(dāng)然你可以將三者組合起來使用,組件化的同時(shí)實(shí)現(xiàn)插件化,再將各個(gè)業(yè)務(wù)模塊完全抽離,然后還有各種動(dòng)態(tài)化,部分組件的跨平臺(tái)等等。。

????但是架構(gòu)是需要一步一步的進(jìn)化的,如果要一口氣的整一個(gè)巨無霸出來,那只能將自己耗死,即使整出來,也一定會(huì)被自己憋死,因?yàn)槌缘奶柫恕?/p>

????所以對(duì)于一個(gè)業(yè)務(wù)成長(zhǎng)飛速,但體量又沒有過于復(fù)雜的app來說,組件化是最好的架構(gòu)升級(jí)方案,因?yàn)樗銐虻母咝В?jí)之后的付出與收獲成正比。

組件化要解決的問題

????在進(jìn)行組件化之前我們要思考為什么要組件化,組件化能為我們解決什么要的問題。

????想象一個(gè)場(chǎng)景,我們?cè)谶M(jìn)行獨(dú)立開發(fā)的時(shí)候,整個(gè)代碼完全就是我們自己的世界,甚至我們都不用進(jìn)行g(shù)it pull 的操作,只需要git push即可,毫無代碼沖突風(fēng)險(xiǎn)。

????然而有一天小明來到了公司和你一起開發(fā),這回你需要熟悉git的各種操作了,如何merge,倆人商量著如何不互相改代碼。不過開發(fā)體驗(yàn)還是可以接受的。

????又過了一段時(shí)間公司獲得了馬云100個(gè)億的投資,老板飄了。老板決定每個(gè)開發(fā)的崗位增加二十個(gè)人。當(dāng)然因?yàn)轳R爸爸投資了公司,公司自然不能差,業(yè)務(wù)飛漲,這二十個(gè)人自然也沒有閑下來,每人都負(fù)責(zé)的一個(gè)小功能,比如小明同學(xué)在安心的做著商品詳情相關(guān)的功能。大家每人維護(hù)著一個(gè)包,其樂融融。

????但是你作為一個(gè)公司元老級(jí)別的老鳥很快發(fā)現(xiàn)了一個(gè)問題,而是多個(gè)兄弟在同時(shí)在一個(gè)包下工作,有一天你突然發(fā)現(xiàn)util包下有timeUtil,dateUtil等幾個(gè)看起來很類似的工具類。并且你發(fā)現(xiàn)小明同學(xué)寫代碼的時(shí)候經(jīng)常會(huì)不小心動(dòng)了其他同學(xué)的代碼,導(dǎo)致小明同學(xué)的人生安全產(chǎn)生了很大的隱患。
????然后你發(fā)現(xiàn)一個(gè)更重要的問題,自己每次改一個(gè)小功能的時(shí)候,竟然都要花上一把王者榮耀的時(shí)間去等待app跑起來,偶爾還會(huì)編譯失敗。測(cè)試同學(xué)最近的臟話也月來越多。。。

????公司代碼混亂不堪,開發(fā)氛圍壓抑無比,看來解決問題迫在眉睫了!你發(fā)現(xiàn)了以下幾點(diǎn)需要解決的問題:

1,公共組件的提取
2,每個(gè)人維護(hù)的模塊內(nèi)容,不沖突,不會(huì)互相傷害,獨(dú)立調(diào)試
3,解決編譯速度問題
4,模塊跳轉(zhuǎn),通信等

如何組件化

???? 先看一張圖

組件化.png

???? 在整個(gè)結(jié)構(gòu)的最上層就是app模塊,其實(shí)他和module沒有太大區(qū)別,本質(zhì)上也是一個(gè)module,只不過是一個(gè)殼module,使用他來做一些集成其他子module的操作與分發(fā)。

???? 而在結(jié)構(gòu)的第二層便是各種module,比如order模塊,login模塊等等,每個(gè)模塊項(xiàng)目隔離,相互獨(dú)立。

???? 在結(jié)構(gòu)的第三層是base,router,bus共同模塊,按需集成。這里我沒有把brouter和bus放到base中。

???? 在結(jié)構(gòu)的最下層則是我們公司各種通用的基礎(chǔ)設(shè)施,比如http模塊,可能有多個(gè)項(xiàng)目使用,可能是第三方庫(kù)(okhttp),或者公司基礎(chǔ)設(shè)施的團(tuán)隊(duì)自己維護(hù)的libray,一般情況都是通過aar來引入。當(dāng)然這里為了直觀,我把這些以源碼的形式做到工程中。

這里有倆個(gè)東西是必備的,router和bus

Router

市面上有很router庫(kù),功能大同小異,這里推薦阿里的Arouter
https://github.com/alibaba/ARouter
用法不多介紹,很簡(jiǎn)單
說下使用router的必要性和好處

1,模塊之間的activity是不能通過原生去跳轉(zhuǎn)的,這是router首要解決的問題。
2,router沒有intent傳參大小限制的問題。
3,router有強(qiáng)大的攔截器和降級(jí)
4,router可以是模塊之間通信的重要手段,如服務(wù)的發(fā)現(xiàn)等。
5,對(duì)組件化的埋點(diǎn)和統(tǒng)計(jì)等有著很好的幫助。
。。。

Bus

bus主要解決的是組件之間通信的問題,替代廣播等原生方案,bus有很多中,eventbus,rxbus, livedataBus等。。

這里我簡(jiǎn)單的寫了個(gè)livedatabus來作為組件化的bus方案。livedatabus的好處是生命周期的感知,重要的是簡(jiǎn)單。避免了eventbus的各種注解,迷之傳遞。

實(shí)踐

首先砍下我寫的demo結(jié)構(gòu)圖


2019-05-22 11-24-46屏幕截圖.png

這里我簡(jiǎn)單的寫了一些gradle腳本來配置工程


2019-05-22 11-26-46屏幕截圖.png

一,module的動(dòng)態(tài)引用

app的gradle文件

apply plugin: 'com.android.application'
apply from: "$rootProject.projectDir/buildScript/main_build.gradle"

android {
    defaultConfig {
        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }
}

main_build.gradle文件
在這個(gè)文件中,我們讀取一個(gè)gradle的參數(shù)來識(shí)別殼工程需要引用的module,然后動(dòng)態(tài)的引用,這樣就避免了我們?yōu)榱霜?dú)立的調(diào)試需要不停的修改module的類型。然后在編譯期間把個(gè)參數(shù)插入到string中方便我們?cè)诖a中使用。

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appProductFlavors.gradle"
apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"
def getDependencyModule() {
    return project.properties.get("dependencyModule")
}

def modules = getDependencyModule().split(',')

android{
    buildTypes {
        release {
            resValue "string", "modules", getDependencyModule()
        }

        debug {
            resValue "string", "modules", getDependencyModule()
        }
    }

}

modules.each {
    module ->
        print(module)
        project.dependencies.add("implementation", project(':' + module))
}

然后我們?cè)趃radle的配置文件中配置一個(gè)參數(shù)來引用相應(yīng)的module
每次我們需要修改引用的module時(shí)就改一下這里的配置即可。

#需要依賴的moudle
dependencyModule=moudle1,moudle3

二,module的applaction初始化

我們每個(gè)module可能除了一些公有的庫(kù)之外,會(huì)引入一些只有自己會(huì)使用的庫(kù),我們無法在app的applaction中作初始化的操作,所以我們必須要為每個(gè)module都作初始化。

這里我們?cè)趓outer模塊中協(xié)理一個(gè)applaoction初始化的服務(wù)。

interface IAppInit : IProvider {
    fun initApp(applaction: Application)
}

然后需要初始化的module去實(shí)現(xiàn)這個(gè)接口,并把路由的地址暴露出來。

比如這里是module1的初始化實(shí)現(xiàn)。

@Route(path = AppModules.module1AppInit)
class Module1Applaction : IAppInit {

    override fun initApp(applaction: Application) {
        Log.e("Module1Applaction","initApp")
    }


    override fun init(context: Context) {

    }
}

然后我們AppModules這個(gè)里面配置我們的路由地址。

object AppModules {

    const val module1AppInit = "/module1/appInit"
    const val module2AppInit = "/module2/appInit"
    const val module3AppInit = "/module3/appInit"

    fun getModulePath(module: String): String? {
        return when (module) {
            "moudle1" -> module1AppInit
            "moudle2" -> module2AppInit
            "moudle3" -> module3AppInit

            else -> null
        }
    }
}

接著我們就可以在我們的app的主applaiction中去根據(jù)我們配置的modules去獲取每個(gè)module相應(yīng)的初始化實(shí)現(xiàn)進(jìn)行初始化,這樣就實(shí)現(xiàn)了每個(gè)module都可以使用自己的初始化方案了。這是我們要解決的第二個(gè)問題。

   private fun initModules() {
        val modules = getString(R.string.modules).split(",")
        modules.forEach { module ->
            val modulePath = AppModules.getModulePath(module)
            if (modulePath != null) {
                val navigation = ARouter.getInstance().build(modulePath).navigation()
                if (navigation != null && navigation is IAppInit) {
                    navigation.initApp(this)
                }
            }
        }
    }

三,模塊的獨(dú)立調(diào)試

同樣我們?yōu)槊總€(gè)模塊引入下面的腳本

apply from: "$rootProject.projectDir/buildScript/module_build.gradle"


android {
    compileSdkVersion rootProject.compileSdkVersion
    defaultConfig {

        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }


}

module_build.gradle腳本
在這個(gè)腳本中,我們同樣根據(jù)gradle的配置參數(shù)來動(dòng)態(tài)的配置
如果配置的參數(shù)中有我們的module,那么就為modlue引入:apply plugin: 'com.android.library'使其成為一個(gè)libray。反之則引入 apply plugin: 'com.android.application' 時(shí)期成為一個(gè)可以獨(dú)立調(diào)試的app。當(dāng)然這樣還是不能夠進(jìn)行調(diào)試的,我們還需要?jiǎng)討B(tài)的配置AndroidManifest的路徑。

def getDependencyModule() {
    return project.properties.get("dependencyModule")
}

def modules = getDependencyModule().split(',')

def contains = modules.contains(project.getName())

if (contains) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

delete project.buildDir

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

apply from: "$rootProject.projectDir/buildScript/router/arouter.gradle"
apply from: "$rootProject.projectDir/buildScript/buildTypes/appBuildTypes.gradle"
apply from: "$rootProject.projectDir/buildScript/module_common_build.gradle"


android {
    sourceSets {
        main {
            if (contains) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            }
        }
    }
}

2019-05-22 12-16-37屏幕截圖.png

如上圖,我們?cè)趍ain的同級(jí)別配置一個(gè)debug,在module沒有被引入的時(shí)候我們就使用這里面的AndroidManifest,我們?cè)贏ndroidManifest中配置我們的applaction,以及啟動(dòng)的activity。之后我們就可以直接進(jìn)行調(diào)試啟動(dòng)了。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.xiaofeiluo.moudle1">


    <application
            android:allowBackup="true"
            android:label="@string/app_name"
            android:name=".debug.Module1Applaction"
            android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
        <activity android:name=".Module1HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

如下圖便是沒有將module引入app中的時(shí)候


2019-05-22 12-21-27屏幕截圖.png

四,組件之間通信的問題

組件和組件之間是相互獨(dú)立的,想要進(jìn)行通信只能通過下層的共同模塊來進(jìn)行。一共有倆種方案

方案一:面向接口的通信,既是服務(wù)暴露

這種方案一般來說是我們一個(gè)模塊想要獲取另一個(gè)模塊的數(shù)據(jù),或者說一個(gè)模塊做了一個(gè)服務(wù)需要share給其他的模塊我們可以使用這中方案。
比如下面的例子

我們需要獲取module3中的用戶數(shù)據(jù),那么我們需要在router中定義一個(gè)接口

interface IUserInfoService : IProvider {
    fun getUserName(callback: (name: String) -> Unit)
    fun getUserAge(callback: (age: String) -> Unit)
    fun getUserSchool(callback: (school: String) -> Unit)
}

然后我們要在module3中去實(shí)現(xiàn)這個(gè)接口,并且把路由地址配置到這個(gè)實(shí)現(xiàn)類


@Route(path = Module3RouterPath.UserInfoService)
class UserInfoService : IUserInfoService {

    private var name: String? = null
    private var age: String? = null
    private var school: String? = null

    private var handler: Handler? = null

    override fun getUserName(callback: (name: String) -> Unit) {
        if (TextUtils.isEmpty(name)) {
            thread {
                Thread.sleep(1000);
                name = "張三"
                handler?.post {
                    callback.invoke(name!!)
                }
            }
        } else {
            callback.invoke(name!!)
        }

    }

    override fun getUserAge(callback: (age: String) -> Unit) {
        if (TextUtils.isEmpty(age)) {
            thread {
                Thread.sleep(1000);
                age = "10"
                handler?.post {
                    callback.invoke(age!!)
                }
            }
        } else {
            callback.invoke(age!!)
        }
    }

    override fun getUserSchool(callback: (school: String) -> Unit) {
        if (TextUtils.isEmpty(school)) {
            thread {
                Thread.sleep(1000);
                school = "清華大學(xué)"
                handler?.post {
                    callback.invoke(school!!)
                }
            }
        } else {
            callback.invoke(school!!)
        }
    }

    override fun init(context: Context?) {
        handler = Handler(Looper.getMainLooper())
    }
}

然后在module1中我們就可以獲取這個(gè)服務(wù)去使用了

    getName.setOnClickListener {
            val userInfo = ARouter.getInstance().build(Module3RouterPath.UserInfoService).navigation()
            userInfo?.let {
                if (it is IUserInfoService) {
                    it.getUserName {
                        name.text = it
                    }
                }
            }
        }

方案二,bus

bus的方案我們一般用來主動(dòng)的去監(jiān)聽一些變化的發(fā)生,而不想接口是去獲取,bus是主要解決接受的通信。
這里我簡(jiǎn)單寫了個(gè)livedatabus來作為總線,然后我們面向具體的數(shù)據(jù)模型進(jìn)行監(jiān)聽。
這里我用一個(gè)叫做event的注解標(biāo)識(shí)這是一個(gè)可以被傳遞的event,注解中的內(nèi)容是這個(gè)evnet對(duì)應(yīng)的key,
bus的原理就不多作介紹。

@Event("UserEvent")
class UserEvent(var newName: String)

然后我們就可以去監(jiān)聽變化了

接受消息

  BusManager.call(UserEvent::class).observe(this) {
            name.text = it.newName
        }

發(fā)送消息

      updateName.setOnClickListener {
            BusManager.postEvent(UserEvent("李四"))
        }

好了,到這里我們就完成了組件之間通信的方案。

最后說幾個(gè)問題。

1,為什么bus,router,base要分開維護(hù)?

這里我覺得base作為一個(gè)功能是每個(gè)模塊都需要的,一般來說是由一個(gè)伙計(jì)去維護(hù)的,這個(gè)東西一般來說也是以一種aar的形式去集成的。而route和bus則不同,他們是需要每個(gè)模塊的小伙伴自己去維護(hù)的,所以物品們要在router和bus之內(nèi)去明確的分包,所以改動(dòng)可能會(huì)很頻繁,我們避免不小心碰觸到base中的核心內(nèi)容,最好把這些配置類的東西分離出來獨(dú)立維護(hù)。還有一種辦法是,這倆個(gè)東西我們統(tǒng)一由一個(gè)小伙伴維護(hù),其他的模塊想要在里面添加服務(wù),或者路由,那么就需要統(tǒng)一的告訴這個(gè)小伙伴并且配備相應(yīng)的說明文檔,這個(gè)小伙伴再進(jìn)行統(tǒng)一的審核。

2,關(guān)于工程的一些配置

這里我么可以利用gradle把一些沒必要重復(fù)的配置抽離出來,同是也可以統(tǒng)一的進(jìn)行管理,比如一些每個(gè)模塊都需要的libray,junit,apt,kpt這些的可以拿出去。然后關(guān)于版本的話我們可以每個(gè)模塊進(jìn)行獨(dú)立的維護(hù),因?yàn)槲覀兏鱾€(gè)模塊的versionCode不一定一樣,大部分情況下,我們是需要CI進(jìn)行配合來自動(dòng)化構(gòu)建的,所以模塊一般也是需要發(fā)aar來引用。模塊之間也有獨(dú)立的倉(cāng)庫(kù),一般我們會(huì)用submodule的形式引入自己開發(fā)的模塊,別人的模塊我們則需要使用aar的形式來引用。所以可以根據(jù)實(shí)際的需求來寫一些gradle的腳本來擴(kuò)展我們的工程。比如說我們加個(gè)配置來決定使用源碼引用還是aar引用等。

這個(gè)只是最簡(jiǎn)單的一系列配置,解決了基本的組件化方案的思路,實(shí)際情況可能要復(fù)雜很多,但是基本的問題已經(jīng)解決。

demo地址
https://github.com/wxxewx/Componentization

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • (轉(zhuǎn)載) Android組件化方案已經(jīng)開源,參見Android組件化方案開源。方案的解讀文章是一個(gè)小的系列,這是系...
    江左灬梅郎閱讀 2,832評(píng)論 2 31
  • 組件化方案調(diào)研 組件化概念 組件化就是將一個(gè)app分成多個(gè)Module,如下圖,每個(gè)Module都是一個(gè)組件(也可...
    xuelang閱讀 1,215評(píng)論 0 2
  • MVVMHabitComponent 關(guān)于Android的組件化,相信大家并不陌生,網(wǎng)上談?wù)摻M件化的文章,多如過江...
    goldze閱讀 5,819評(píng)論 2 22
  • 不怕跌倒,所以飛翔 組件化開發(fā) 參考資源 Android組件化方案 為什么要組件化開發(fā) 解決問題 實(shí)際業(yè)務(wù)變化非常...
    筆墨Android閱讀 3,101評(píng)論 0 0
  • 有人說過高考在中國(guó)社會(huì)一定程度上能決定一個(gè)人一生的走向。我不敢全部認(rèn)同,但也確實(shí)受其影響。它有沒有改變我的...
    HighPriests閱讀 333評(píng)論 0 1

友情鏈接更多精彩內(nèi)容