模塊化方案實踐
為什么需要模塊化
- 在項目開發(fā)到一定階段跪另,隨著功能需求越來越多洁段,代碼結(jié)構(gòu)越來越臃腫晌柬,維護也隨之越來越麻煩,單次編譯調(diào)試的時間越來越長绑嘹,每一次修改都很容易牽一發(fā)而動全身稽荧。
- 在大規(guī)模開發(fā)團隊中對大項目的協(xié)作開發(fā)可能被拆分到多個事業(yè)部,每個事業(yè)部有獨立的開發(fā)工腋,測試團隊和獨立的部署需求姨丈,在單工程高耦合的情況下難以為繼畅卓。
- 在 toB 的產(chǎn)品中,可能涉及到為客戶做定制化的修改或單純向客戶提供部分功能构挤,并且要將主線產(chǎn)品的最新功能及時更新給客戶髓介,在單工程或者分支開發(fā)的情況下實現(xiàn)起來依然較為麻煩
- 在公司的多個業(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)系人兼吓、群組彪腔、對話。
模塊化需要解決的幾個問題
- 組件與組件之間啦扬,模塊與模塊之間保持橫向隔離
- 各模塊擁有獨立運行調(diào)試的能力
- 各模塊之間可以互相通信及調(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)圖
其中模塊與模塊瘾蛋,組件與組件,模塊與組件的依賴關(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 后,分為以下幾步使用
- 在 Service 模塊中定義接口杨帽,并且繼承 IProvider
interface IAuthApi : IProvider {
fun isLogin(): Boolean
}
- 在 Auth 模塊中實現(xiàn)接口
//該注解為 ARouter 必須
@Route(path = "/auth/api")
class AuthApi() : IAuthApi {
override fun isLogin(): Boolean {
return true
}
}
- 在其他模塊中使用接口
val authApi = ARouter.getInstance().build("/auth/api").navigation() as IAuthApi
authApi.isLogin()
以上三步完成后則可以實現(xiàn)模塊間的方法互相調(diào)用了
- 為了方便調(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 中集中管理這些資源
- 在業(yè)務(wù)模塊創(chuàng)建 Fragment胧砰,并且使用 Route 注解設(shè)置 Path
@Route(path = "/fragment/contacts")
class ContactsFragment : Fragment() {
}
- 在需要使用到這個 Fragment 的地方調(diào)用 ARouter 方法獲取它
val fragment = ARouter.getInstance().build("/fragment/contacts").navigation() as Fragment
- 也可以在 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 : 用于跨模塊的事件傳遞