Android 模塊化/組件化 方案實踐

模塊化方案實踐

為什么需要模塊化

  1. 在項目開發(fā)到一定階段跪另,隨著功能需求越來越多洁段,代碼結(jié)構(gòu)越來越臃腫晌柬,維護也隨之越來越麻煩,單次編譯調(diào)試的時間越來越長绑嘹,每一次修改都很容易牽一發(fā)而動全身稽荧。
  2. 在大規(guī)模開發(fā)團隊中對大項目的協(xié)作開發(fā)可能被拆分到多個事業(yè)部,每個事業(yè)部有獨立的開發(fā)工腋,測試團隊和獨立的部署需求姨丈,在單工程高耦合的情況下難以為繼畅卓。
  3. 在 toB 的產(chǎn)品中,可能涉及到為客戶做定制化的修改或單純向客戶提供部分功能构挤,并且要將主線產(chǎn)品的最新功能及時更新給客戶髓介,在單工程或者分支開發(fā)的情況下實現(xiàn)起來依然較為麻煩
  4. 在公司的多個業(yè)務(wù)線中,可能會用到一些公用的業(yè)務(wù)功能筋现,在非模塊化的情況下每個業(yè)務(wù)項目均需要重復(fù)實現(xiàn)唐础。

組件化和模塊化

在我的理解中,組件化是對項目的某項功能的抽離矾飞,模塊化是對項目某項業(yè)務(wù)的抽離一膨,所以我們先明確一下組件和模塊的區(qū)別,這樣在下面的內(nèi)容中有助于理解洒沦。

組件:由單一且獨立的功能構(gòu)成業(yè)務(wù)無關(guān)的組件

模塊:由一個或多個組件作為基礎(chǔ)豹绪,并包含相關(guān)業(yè)務(wù)代碼的模塊


項目實例

以下將會用一個模塊化的聊天項目作為例子,闡述構(gòu)建一個模塊化項目的整個流程申眼。該項目可以進行登錄瞒津,查看聯(lián)系人兼吓、群組彪腔、對話。

模塊化需要解決的幾個問題

  1. 組件與組件之間啦扬,模塊與模塊之間保持橫向隔離
  2. 各模塊擁有獨立運行調(diào)試的能力
  3. 各模塊之間可以互相通信及調(diào)用方法

目錄:

一濒翻、組件化拆解

二屁柏、模塊化拆解

三、膠水模塊

四有送、模塊配置

五淌喻、模塊間方法調(diào)用

六、模塊間頁面調(diào)用

七雀摘、模塊間數(shù)據(jù)交互

八裸删、模塊間事件通信

九、集成運行和單獨運行

十阵赠、模塊間數(shù)據(jù)變化通知

十二烁落、總結(jié)


一、組件化拆解

上面說過豌注,模塊是由一個或多個組件為基礎(chǔ),并包含相關(guān)業(yè)務(wù)代碼的集合灯萍,所以要實現(xiàn)模塊化轧铁,首先要做的是組件化的拆解。而組件是單一且獨立業(yè)務(wù)無關(guān)的組件旦棉,在這個例子中齿风,將會拆解得到以下幾個組件药薯。

Network:用于請求 HTTP API 的組件

Socket:用于維持 Socket 長連接的組件

二、模塊化拆解

在基礎(chǔ)的組件化拆解完成后救斑,需要對項目業(yè)務(wù)相關(guān)的部分進行拆解形成一個個的模塊童本,拆解的粒度根據(jù)項目大小,業(yè)務(wù)結(jié)構(gòu)以及實際需求均有不同脸候,針對此例子穷娱,作為聊天項目,拆解為以下幾個模塊运沦。

Auth:登錄和身份認證的模塊

Chat:處理和展示聊天對話的模塊

Contacts:提供聯(lián)系人泵额,群組等信息的模塊

Socket:管理長連接狀態(tài),分發(fā)消息的模塊

三携添、膠水模塊

膠水模塊顧名思義是將各個業(yè)務(wù)模塊相關(guān)聯(lián)起來的模塊嫁盲。各模塊之間要能夠互相通信,調(diào)用烈掠,集成羞秤,缺少不了膠水模塊發(fā)揮的作用。本實例提供了兩個膠水模塊:

App:在最外層將各模塊集成起來的模塊

Service:在業(yè)務(wù)模塊下層支撐業(yè)務(wù)模塊間交互與通信的模塊

整理一下目前的組件和模塊左敌,可以得到以下結(jié)構(gòu)圖

整體結(jié)構(gòu)圖.png

其中模塊與模塊瘾蛋,組件與組件,模塊與組件的依賴關(guān)系應(yīng)該是垂直從上到下的依賴母谎,而不應(yīng)該產(chǎn)生橫向的或者從下到上的依賴

四瘦黑、模塊配置

在本步之前假設(shè) Network 和 Socket 兩個組件都已經(jīng)發(fā)布到遠端倉庫了,各個模塊需要使用的直接引用即可奇唤。
根據(jù)以下路徑創(chuàng)建四個業(yè)務(wù)模塊和兩個膠水模塊幸斥,在此可將 Android Studio 的 Module 認為是上文所述的模塊。
Android Studio - File - New Module - Android Library

創(chuàng)建成功后此時在各個模塊的 build.gradle 文件中第一行均引用 Library 插件
apply plugin: 'com.android.library'

在開發(fā)中通常使用兩個插件設(shè)置該工程的運行類型

App 插件: com.android.application

Library 插件: com.android.library

因為我們是需要各個業(yè)務(wù)模塊不但能夠集成運行咬扇,也要能夠單獨運行甲葬,所以此處需要一個變量用于改變插件,在項目根目錄下創(chuàng)建 config.gradle 懈贺,然后在里面填入

ext {
    authIsApp = false
    contactsIsApp = false
    chatIsApp = false
    socketIsApp = false
}

這樣我們可以在模塊的 build.gradle 中引用 config.gradle 并訪問里面的變量经窖,根據(jù)變量的值去使用不同的插件,以支持模塊單獨以 App 的方式運行

apply from: rootProject.file('config.gradle')
if (contactsIsApp) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

模塊在單獨運行的時候還需要有 applicationId梭灿,這個也可以根據(jù)變量來控制

android {
    defaultConfig {
        if (contactsIsApp) {
            applicationId "com.test.contacts"
        }
    }
}

為了各個模塊之間的資源文件名稱不被混淆画侣,可以指定一個資源名稱前綴,如果不合符要求堡妒,IDE 會有報錯提示

android {
    resourcePrefix "contacts_"
}

最后我們還需要準備兩套 AndroidManifest 文件配乱,一套是用于集成調(diào)試時模塊作為一個 Library 會合并到主工程中,在這個文件中只需要包含權(quán)限申請以及相關(guān)組件的注冊即可,另外一套是模塊單獨運行時需要自己獨立的相關(guān)配置搬泥,需要完整的全套配置桑寨,在 build.gradle 中添加如下代碼:

android {
    sourceSets {
        main {
            // 單獨調(diào)試與集成調(diào)試時使用不同的 AndroidManifest.xml 文件
            if (contactsIsApp) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

在集成調(diào)試的情況下所用到的 AndroidManifest 文件內(nèi)容如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.test.contacts">
    //申請本模塊需要的權(quán)限
    <uses-permission android:name="android.permission.INTERNET"/>
    //本實例中此模塊沒有需要對外提供的相關(guān)組件(Activity, Service..),所以無需注冊
</manifest>

在單獨運行時所用的 AndroidManifest 文件內(nèi)容如下

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

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:name=".ContactsModuleApp"
            android:theme="@style/AppTheme">
        //模塊單獨運行時的啟動頁
        <activity android:name=".ui.ModuleMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

而各個模塊的引用關(guān)系如下

App 模塊 :

dependencies {
    if(!authIsApp) runtimeOnly project(':auth')
    if(!socketIsApp) runtimeOnly project(':socket')
    if(!chatIsApp) runtimeOnly project(':chat')
    if(!contactsIsApp) runtimeOnly project(':contacts')

    implementation project(':service')
}

其他所有模塊

dependencies {
    implementation project(':service')
}

這里可以看到忿檩,對于四個業(yè)務(wù)模塊使用了 runtimeOnly 的方式進行引用尉尾,表示在編譯期間不可見,但是會參與打包到 APK 并在運行期間可見燥透。這就防止了我們互相直接訪問橫向模塊的類或資源文件沙咏,更好的做了隔離。前面的 if(!chatIsApp) 判斷是為了在其他模塊單獨作為 App 運行的時候在此處不進行依賴兽掰,因為一個 App 依賴另一個 App 會出錯芭碍。最后下方引用了 service,根據(jù)我們的結(jié)構(gòu)設(shè)計孽尽,service 作為整個架構(gòu)的中心會被所有模塊直接依賴窖壕。

五、模塊間方法調(diào)用

為了做到解耦杉女,模塊間是沒有互相引用的瞻讽,所以不能直接調(diào)用對方的方法,但是各模塊都引用了 Service熏挎,可以藉由 Service 來實現(xiàn)解耦并且接口化的模塊間方法調(diào)用速勇。這里有幾種不同的方案可供參考

自定義接口并注冊

在 Service 定義接口,以及創(chuàng)建管理類 ApiFactory坎拐,在業(yè)務(wù)模塊中實現(xiàn)接口烦磁,并且在業(yè)務(wù)模塊初始化的時候?qū)I(yè)務(wù)模塊的實現(xiàn)類傳遞給 ApiFactory,其他模塊即可利用 ApiFactory 調(diào)用相關(guān)方法哼勇。代碼如下:

Service 模塊中定義接口并創(chuàng)建管理類

interface IAuthApi {
    fun isLogin(): Boolean
}

object ApiFactory {
    private var authApi: IAuthApi? = null

    fun setAuthApi(IAuthApi authApi) {
        this.authApi = authApi
    }

    fun getAuthApi(): authApi? {
        return this.authApi
    }
}

Auth 模塊中實現(xiàn)接口并且注冊

class AuthApi : IAuthApi {
    override fun isLogin(): Boolean {
        return true
    }
}

class AuthApp : Application() {
   override fun onCreate() {
        super.onCreate()
        // 將 AuthApi 類的實例注冊到 ApiFactory
        ApiFactory.setAuthApi(AuthApi())
    }
}

這里會存在一個問題都伪,上面的代碼是在 AuthApp 里面去注冊的,但是應(yīng)用在運行的時候不會去加載各個模塊中的 Application积担,而是加載主工程 App 中的 Application陨晶,所以這里需要用一個反射的方法去實現(xiàn)模塊 Application 的初始化。

模塊 Application 的初始化

為了能夠?qū)Ω鱾€模塊中做一些初始化的操作帝璧,我們在 Service 模塊中創(chuàng)建了一個 ModuleBaseApp 給模塊中的 Application 繼承

abstract class ModuleBaseApp : Application() {

    override fun onCreate() {
        super.onCreate()
        //這里是為了模塊單獨運行的時候也能獨立進行初始化的操作
        onCreateModuleApp(this)
    }

    abstract fun onCreateModuleApp(application: Application)
}

在 Auth 模塊中繼承

class AuthModuleApp : ModuleBaseApp() {
    override fun onCreateModuleApp(application: Application) {
        ApiFactory.setAuthApi(AuthApi())
    }
}

只是這樣還不能夠讓 AuthModuleApp 在應(yīng)用啟動時被調(diào)用先誉,我們還需要在 Service 中記錄目前所有模塊的 Application 路徑類名

object ModuleAppNames {
    const val AUTH = "com.test.auth.AuthModuleApp"
    const val SERVICE = "com.test.service.ServiceModuleApp"
    const val CHAT = "com.test.chat.ChatModuleApp"
    const val CONTANCTS = "com.test.contacts.ContactsModuleApp"
    const val SOCKET = "com.test.socket.SocketModuleApp"

    val names = arrayOf(AUTH, SERVICE, CHAT, CONTANCTS,SOCKET)
}

最后在 App 模塊的 Application 中去初始化他們

class MainApp : Application() {

    override fun onCreate() {
        super.onCreate()
        initModules()
    }

    private fun initModules() {
        ModuleAppNames.names.forEach {
            val clazz = Class.forName(it)
            try {
                val app = clazz.newInstance() as ModuleBaseApp
                app.onCreateModuleApp(this)
            } catch (e: Exception) {
            }
        }
    }
}

至此的烁,所有模塊中需要初始化的內(nèi)容都與 App 模塊綁定在了一起初始化褐耳,并都能夠向 ApiFactory 中注冊自己模塊接口的具體實現(xiàn)了。

使用第三方庫實現(xiàn)接口注冊

在所有模塊 的 build.gradle 添加如下代碼

apply plugin: 'kotlin-kapt'

dependencies {
    implementation ("com.alibaba:arouter-api:1.4.1")
    kapt ("com.alibaba:arouter-compiler:1.2.1") 
}

這里個問題需要注意渴庆,如果沒有使用 Kotlin漱病,就不需要導(dǎo)入 kapt 插件买雾,用 annotationProcessor
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'

在 Application 中初始化 ARouter

class MainApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ARouter.init(this)
    }

導(dǎo)入 ARouter 后,分為以下幾步使用

  1. 在 Service 模塊中定義接口杨帽,并且繼承 IProvider
interface IAuthApi : IProvider {
    fun isLogin(): Boolean
}
  1. 在 Auth 模塊中實現(xiàn)接口
//該注解為 ARouter 必須
@Route(path = "/auth/api")
class AuthApi() : IAuthApi {
    override fun isLogin(): Boolean {
        return true
    }
}
  1. 在其他模塊中使用接口
val authApi =  ARouter.getInstance().build("/auth/api").navigation() as IAuthApi
authApi.isLogin()

以上三步完成后則可以實現(xiàn)模塊間的方法互相調(diào)用了

  1. 為了方便調(diào)用,可以在 Service 集中管理接口嗤军,接口的 Path 用常量統(tǒng)一管理
object Api {
    const val AUTH_API = "/auth/api"
    const val SOCKET_API = "/socket/api"
    const val CONTACTS_API = "/contacts/api"

    fun getAuthApi(): IAuthApi {
        return ARouter.getInstance().build(AUTH_API).navigation() as IAuthApi
    }

    fun getSocketApi(): ISocketApi {
        return ARouter.getInstance().build(SOCKET_API).navigation() as ISocketApi
    }

    fun getContactsApi(): IContactsApi {
        return ARouter.getInstance().build(CONTACTS_API).navigation() as IContactsApi
    }
}

//實現(xiàn)時也用常量
@Route(path = Api.AUTH_API)
class AuthApi() : IAuthApi {
    override fun isLogin(): Boolean {
        return true
    }
}

以上注盈,就是模塊之間方法調(diào)用的一些方案

六、模塊間頁面調(diào)用

這里主要還是依賴上一步提到的 ARouter叙赚,使用 ARouter 可以容易的實現(xiàn)模塊之間的 Activity 跳轉(zhuǎn)老客,F(xiàn)ragment 實例獲取。

同樣的為了方便管理震叮,我們在 Service 中集中管理這些資源

  1. 在業(yè)務(wù)模塊創(chuàng)建 Fragment胧砰,并且使用 Route 注解設(shè)置 Path
@Route(path = "/fragment/contacts")
class ContactsFragment : Fragment() {
}
  1. 在需要使用到這個 Fragment 的地方調(diào)用 ARouter 方法獲取它
val fragment = ARouter.getInstance().build("/fragment/contacts").navigation() as Fragment
  1. 也可以在 Service 中集中管理,避免使用時的 Path 硬編碼
object Router {
    private val router = ARouter.getInstance()

    object Pages {
        const val LOGIN_ACTIVITY = "/auth/activity/login"
    }

    object Fragments {
        const val CONTACTS_FRAGMENT = "/contacts/fragment/contacts"
    }

    fun startLoginActivity() {
        router.build(Pages.LOGIN_ACTIVITY).navigation()
    }
    fun getContactsFragment(): Fragment {
        return router.build(Fragments.CONTACTS_FRAGMENT).navigation() as Fragment
    }
}

模塊中創(chuàng)建對應(yīng)組件的時候直接使用 Service 中的常量

@Route(path = Router.Fragments.CONTACTS_FRAGMENT)
class ContactsFragment : Fragment() {
}

這里需要特別注意的問題是: ARouter 會對 Path 進行分組苇瓣,在默認情況下尉间,Path 最少由兩級組成,其中的第一級為組名击罪,如果在不同的模塊使用了同一個組名 則會報錯哲嘲。
例如 :
/auth/activity/login 其中 auth 為組名,這里我們通常使用模塊名作為組名媳禁,不要跨模塊使用同樣的組名眠副,不管是頁面還是接口都不行。

七竣稽、模塊間數(shù)據(jù)交互

說到模塊間的數(shù)據(jù)交互囱怕,首先要確定的是:模塊間以什么樣的數(shù)據(jù)格式進行交互
比如 Contacts 模塊中有 Member 這個對象,那 Chat 模塊在調(diào)用 Contacts 模塊方法的時候可能會需要返回一個 Member 類型的返回值毫别,此時如果 Chat 模塊中不存在 Member 或者 Service 中定義接口的時候沒有這個類的話娃弓,是無法進行下去的,此時需要一些方案來解決這個問題拧烦。

方案一忘闻、Model 下沉

第一種解決方法可能是將 Member 這個類進行下沉,放到一個公共的組件中恋博,然后所有的模塊都引用它齐佳,這樣所有的模塊都可以使用這個 Model ,但是此方法會因為業(yè)務(wù)改動而去頻繁的改動下層組件债沮,開發(fā)起來是十分不便的炼吴,也不符合模塊化的理念,畢竟所有的 Model 都揉在一起了疫衩。

方案二硅蹦、使用 API 子模塊

此方法是將需要提供對外服務(wù)的業(yè)務(wù)模塊中的 Model、接口、Event 等獨立到一個 API 子模塊童芹,這個 API 子模塊可以被 Service 模塊直接引用涮瞻,這樣其他業(yè)務(wù)模塊引用 Service 模塊時間接的獲取到了相關(guān)的 Model,但是因為只是引用了 API 子模塊而依然保持了和主要的業(yè)務(wù)模塊的隔離假褪。但是此方案會增加模塊數(shù)量以及改動子模塊的 Model 時署咽,可能導(dǎo)致其他使用到該 Model 的模塊發(fā)生異常。


方案三生音、各自維護 Model宁否,使用通用格式通信

以上兩種方案都是在業(yè)務(wù)模塊中可以直接使用定義好的 Model,但是也存在著各自的問題缀遍。換一個思路去看的話慕匠,如果使用通用格式去通信,各自維護自己的 Model 是否能更加合適我們的場景域醇。按照上面的例子中 Chat 模塊需要從 Contacts 模塊中獲取一個 Member 的場景台谊,如果從 Contacts 模塊中返回的是一個 Json,由 Chat 模塊根據(jù)返回的 Json 創(chuàng)建一個結(jié)構(gòu)簡潔的 TempMember歹苦。

Member 存在于 Contacts 模塊中青伤,提供的接口返回 Json

class Member {
    var id: String = ""
    var avatarUrl: String = ""
    var name: String = ""
    var inactive = false
    var role: String = ""
    var type: String = ""
    var indexSymbol: String = ""
    var fullName: String? = null
}

@Route(path = Api.CONTACTS_API)
class ContactsApi(private val context: Context) : IContactsApi {
    override fun findMemberById(id: String?): String? {
         val member = DBHelper.findMemberById(id)
         return GsonUtil.toString(member)
    }
}

在 Chat 模塊中獲取 Member 的 Json ,并轉(zhuǎn)換成需要的對象使用殴瘦,比如需要在聊天列表顯示成員的頭像和名字狠角,那只需要其中三個字段即可。

private fun toTarget(json: String?): Target? {
    return GsonUtil.toObject(json, Target::class.java)
}

class Target {
    var avatarUrl: String? = null
    var fullName: String? = null
    var name = ""
}

//使用 
val memberJson = Api.getContactApi().findMemberById(memberId)
val target = toTarget(memberJson)
nameView.text = target.fullName ?: target.name

此方式的問題在于使用方在獲取數(shù)據(jù)后均需要經(jīng)過一個轉(zhuǎn)換過程才能使用蚪腋,在時間和代碼上會有一定冗余丰歌,不過優(yōu)勢在于各模塊之間可以做到 Model 獨立,數(shù)據(jù)獨立屉凯。

總結(jié):其實不管任何方式都存在各自的優(yōu)缺點以及適合的場景立帖,主要還是取決于開發(fā)團隊的實際情況取舍以及項目所需要應(yīng)對的業(yè)務(wù)場景

八、模塊間事件通信

通秤蒲猓可以按照第五步的模塊間方法調(diào)用實現(xiàn)回調(diào)接口晓勇,但是在事件的通信上使用觀察者模式會更為簡單清晰,所以可以考慮使用 EventBus 來作為模塊間通信的橋梁灌旧,在項目中我們有用到 RxJava绑咱,這里也可以使用 RxJava 來實現(xiàn)一個簡單的事件分發(fā)系統(tǒng)。

需要在 Service 模塊中實現(xiàn)下面的 EventBus 集中分發(fā)事件
并在 Service 中定義 Event Model

//用 RxJava 實現(xiàn)的 Eventbus
object EventBus {
    private val bus = PublishSubject.create<BaseEvent>().toSerialized()

    fun <T : BaseEvent> post(event: T) {
        bus.onNext(event)
    }

    fun <T : BaseEvent> registerEvent(eventClass: KClass<T>, mainThread: Boolean = true,
                                      onEvent: (event: T) -> Unit): Disposable {

        return bus.filter { it::class == eventClass }
                  .observeOn(if (mainThread) AndroidSchedulers.mainThread() else Schedulers.io())
                  .subscribe({ onEvent(it as T) }, { Log.d(TAG, "error ${it.message}") })
    }

    fun unregister(disposable: Disposable?) {
        if (disposable != null && !disposable.isDisposed) disposable.dispose()
    }
}

//創(chuàng)建一個空接口
interface BaseEvent

//繼承接口實現(xiàn)一個 Event枢泰,用于通知長連接的連接狀態(tài)
class SocketEvent(val event: Event, val error: Throwable?) : BaseEvent {
    enum class Event {
        CONNECTED, DISCONNECTED
    }
}

在其他模塊中使用

//在 A 模塊發(fā)出 Event
EventBus.post(SocketEvent(SocketEvent.Event.DISCONNECTED, e))

//在 B 模塊監(jiān)聽 Event
EventBus.registerEvent(SocketEvent::class) {
    when (it.event) {
        SocketEvent.Event.CONNECTED -> { Log.d("Event", "connected") }
        SocketEvent.Event.DISCONNECTED -> { Log.d("Event", "disconnected error: ${it.error}") }
    }
}

九描融、模塊單獨運行

在第四步里面做了一些為單獨運行準備的一些配置,在第五步中實現(xiàn)了模塊 Application 的初始化也能為我們切換集成和單獨運行提供便利衡蚂。

首先明確的是窿克,單獨一個模塊骏庸,是不一定能夠完全獨立運行的,他或許可以單獨運行年叮,也可能需要依賴其他幾個模塊運行具被。比如 Auth 模塊具有獨立運行的條件,但是 Contacts 模塊則不是只损,他需要有 Auth 來提供賬號信息獲取數(shù)據(jù)硬猫,如果需要的話,還可以加入 Socket 來提供成員的實時變化信息改执。

至此我們用 Contacts 模塊作為例子來說下

首先是改變 config.gradle , 將 contactsIsApp 改成 true

ext {
    authIsApp = false
    contactsIsApp = true
    chatIsApp = false
    socketIsApp = false
}

如果是獨立運行 這里需要在運行時加載 Auth 和 Socket 模塊

dependencies {
    implementation project(':service')

    if (contactsIsApp) {
        runtimeOnly project(':auth')
        runtimeOnly project(':socket')
    }
}

然后是在 Application 中初始化

class ContactsModuleApp : ModuleBaseApp() {

    override fun onCreateModuleApp(application: Application) {
        //如果是獨立運行的話這里的 application 是 ContactsModuleApp
        if (application is ContactsModuleApp) aloneInit(application)
    }

    //模塊作為 App 啟動時初始化的方法
    private fun aloneInit(application: Application) {
        //作為 App 啟動時需要由它來初始化 DRouter
        ARouter.init(this)

        //初始化依賴的相關(guān)模塊
        arrayOf(ModuleAppNames.SERVICE, ModuleAppNames.SOCKET, ModuleAppNames.AUTH)
            .forEach {
                val clazz = Class.forName(it)
                try {
                    val app = clazz.newInstance() as ModuleBaseApp
                    app.onCreateModuleApp(application)
                } catch (e: Exception) {
                }
           
}

在這里 Contacts 是依賴了三個其他模塊的坑雅,所以也需要初始化這些模塊

然后就是模塊單獨運行的話辈挂,是需要提供一個啟動頁面的,在這之前 Contacts 只是對外提供了一個 Fragment 用于顯示聯(lián)系人列表裹粤,是沒有一個 Activity 的终蒂,所以此時應(yīng)該創(chuàng)建一個用于獨立運行時的啟動頁

@Route(path = Router.Pages.CONTACTS_MODULE_MAIN_ACTIVITY)
class ModuleMainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.contacts_activity_module_main)

        // 這里會調(diào)用 Auth 模塊的方法判斷是否登錄
        if (Api.getAuthApi().isLogin()) {
            Api.getSocketApi().startSocketService()
        } else {
            Router.startLoginActivity(this)
        }
    }
}

可以看到上文中在未登錄的狀況下會跳轉(zhuǎn)到登錄界面,那么登錄之后又是去到哪個頁面呢遥诉?這里可以在 Service 中集中控制跳轉(zhuǎn)路由

object Router {
    private val router = ARouter.getInstance()

    object Pages {
        const val LOGIN_ACTIVITY = "/auth/activity/login"
        const val MAIN_ACTIVITY = "/app/activity/main"
        const val CONTACTS_MODULE_MAIN_ACTIVITY = "/contacts/activity/module_main"
    }

    object Fragments {
        const val CONVERSATION_FRAGMENT = "/chat/fragment/conversation"
        const val CONTACTS_FRAGMENT = "/contacts/fragment/contacts"
    }

    fun startLoginActivity() {
        router.build(Pages.LOGIN_ACTIVITY).navigation()
    }

    fun startMainActivity(context: Context) {
        router.build(getMainActivityPath(context)).navigation()
    }

//通過此方法獲取當(dāng)前登錄后的首頁
    private fun getMainActivityPath(context: Context): String {
        return when (context.applicationInfo.className) {
            ModuleAppNames.CONTANCTS -> Pages.CONTACTS_MODULE_MAIN_ACTIVITY
            else -> Pages.MAIN_ACTIVITY
        }
    }

    fun getContactsFragment(): Fragment {
        return router.build(Fragments.CONTACTS_FRAGMENT).navigation() as Fragment
    }

    fun getConversationFragment(): Fragment {
        return router.build(Fragments.CONVERSATION_FRAGMENT).navigation() as Fragment
    }
}

第四步的時候我們配置了兩套 AndroidManifest拇泣,這里需要編輯一下獨立運行時的那一套 AndroidManifest 文件,主要是將模塊的啟動頁面添加進去

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

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:name=".ContactsModuleApp"
            android:theme="@style/AppTheme">
        <activity android:name=".ui.ModuleMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

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

</manifest>

十矮锈、模塊間數(shù)據(jù)變化通知

這里有個場景霉翔,比如我在 Chat 模塊中的聊天列表中用到了 Contacts 模塊的 Member 頭像名字等信息,但是如果這個 Member 發(fā)生了變化苞笨,此時 Chat 是不得而知的债朵,所以需要一個模塊間數(shù)據(jù)變化通知的機制,因為數(shù)據(jù)庫使用了 Realm瀑凝,所以可以利用 Realm 的更新通知機制來通知其他模塊序芦。

這里在各模塊的 Api 中添加了監(jiān)聽數(shù)據(jù)庫變化的方法

@Service(function = [IContactsApi::class])
class ContactsApi(private val context: Context) : IContactsApi {

    private val changeListeners: HashMap<String, RealmChangeListener<Realm>> = hashMapOf()

    override fun registerDBChange(tag: String, onChange: () -> Unit) {
        val listener = RealmChangeListener<Realm> { onChange() }
        ContactsRealmHelper.getRealm().addChangeListener(listener)
        changeListeners[tag] = listener
    }

    override fun unregisterDBChange(tag: String) {
        val listener = changeListeners[tag] ?: return
        ContactsRealmHelper.getRealm().removeChangeListener(listener)
        changeListeners.remove(tag)
    }
}

在其他模塊可以注冊監(jiān)聽

class ConversationViewModel(application: Application) : AndroidViewModel(application) {

    private val realm = ChatRealmHelper.getRealm()

    val vchannels = MutableLiveData<List<VChannel>>()
    private var vchannelsInRealm: RealmResults<VChannel>

    init {
        vchannelsInRealm = realm.where(VChannel::class.java)
                .sort("readTs", Sort.DESCENDING)
                .findAll()

        vchannelsInRealm.addChangeListener(RealmChangeListener { vchannels.postValue(realm.copyFromRealm(it)) })

        //發(fā)生變化后就對 vchannels 重新賦值觸發(fā)列表更新
        Api.getContactApi().registerDBChange(this.javaClass.name) { vchannels.postValue(vchannels.value) }
    }

    override fun onCleared() {
        super.onCleared()
        Api.getContactApi().unregisterDBChange(this.javaClass.name)
        vchannelsInRealm.removeAllChangeListeners()
        realm.close()
    }
}

這里如果使用了其他的數(shù)據(jù)庫,也可以根據(jù)各個數(shù)據(jù)庫的通知機制來實現(xiàn)

十一粤咪、總結(jié)

總結(jié)一下目前的方案谚中,各模塊之間互相隔離,各自獨立負責(zé)數(shù)據(jù)存儲寥枝。各模塊可以單獨運行或者依賴必要模塊運行宪塔。模塊間的數(shù)據(jù)交換使用 Json 格式,由調(diào)用方根據(jù)需要做最后轉(zhuǎn)換脉顿。模塊間的事件通知用 RxJava 或者 EventBus 實現(xiàn)蝌麸。跨模塊的數(shù)據(jù)變化依賴 Realm 通知艾疟,

其中所有模塊以 Service 為中心来吩,包含了以下幾個重要類:

Api : 用于獲取某模塊的對方方法接口

Router : 用于跳轉(zhuǎn)其他模塊頁面或者獲取 Fragment 或 View

EventBus : 用于跨模塊的事件傳遞

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末敢辩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子弟疆,更是在濱河造成了極大的恐慌戚长,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怠苔,死亡現(xiàn)場離奇詭異同廉,居然都是意外死亡,警方通過查閱死者的電腦和手機柑司,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門迫肖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人攒驰,你說我怎么就攤上這事蟆湖。” “怎么了玻粪?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵隅津,是天一觀的道長。 經(jīng)常有香客問我劲室,道長伦仍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任很洋,我火速辦了婚禮充蓝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹲缠。我一直安慰自己棺克,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布线定。 她就那樣靜靜地躺著娜谊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斤讥。 梳的紋絲不亂的頭發(fā)上纱皆,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音芭商,去河邊找鬼派草。 笑死,一個胖子當(dāng)著我的面吹牛铛楣,可吹牛的內(nèi)容都是我干的近迁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼簸州,長吁一口氣:“原來是場噩夢啊……” “哼鉴竭!你這毒婦竟也來了歧譬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤搏存,失蹤者是張志新(化名)和其女友劉穎瑰步,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體璧眠,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡缩焦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了责静。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袁滥。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖灾螃,靈堂內(nèi)的尸體忽然破棺而出呻拌,到底是詐尸還是另有隱情,我是刑警寧澤睦焕,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站靴拱,受9級特大地震影響垃喊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜袜炕,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一本谜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧偎窘,春花似錦乌助、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仆葡,卻和暖如春赏参,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沿盅。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工把篓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腰涧。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓韧掩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窖铡。 傳聞我的和親對象是個殘疾皇子疗锐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

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