Android 模組化/元件化 方案實踐
模組化方案實踐
為什麼需要模組化
- 在專案開發到一定階段,隨著功能需求越來越多,程式碼結構越來越臃腫,維護也隨之越來越麻煩,單次編譯除錯的時間越來越長,每一次修改都很容易牽一髮而動全身。
- 在大規模開發團隊中對大專案的協作開發可能被拆分到多個事業部,每個事業部有獨立的開發,測試團隊和獨立的部署需求,在單工程高耦合的情況下難以為繼。
- 在 toB 的產品中,可能涉及到為客戶做定製化的修改或單純向客戶提供部分功能,並且要將主線產品的最新功能及時更新給客戶,在單工程或者分支開發的情況下實現起來依然較為麻煩
- 在公司的多個業務線中,可能會用到一些公用的業務功能,在非模組化的情況下每個業務專案均需要重複實現。
元件化和模組化
在我的理解中,元件化是對專案的某項功能的抽離,模組化是對專案某項業務的抽離,所以我們先明確一下元件和模組的區別,這樣在下面的內容中有助於理解。
元件:由單一且獨立的功能構成業務無關的元件
模組:由一個或多個元件作為基礎,幷包含相關業務程式碼的模組
專案例項
以下將會用一個模組化的聊天專案作為例子,闡述構建一個模組化專案的整個流程。該專案可以進行登入,檢視聯絡人、群組、對話。
模組化需要解決的幾個問題
- 元件與元件之間,模組與模組之間保持橫向隔離
- 各模組擁有獨立執行除錯的能力
- 各模組之間可以互相通訊及呼叫方法
目錄:
一、元件化拆解
二、模組化拆解
三、膠水模組
四、模組配置
五、模組間方法呼叫
六、模組間頁面呼叫
七、模組間資料互動
八、模組間事件通訊
九、整合執行和單獨執行
十、模組間資料變化通知
十二、總結
一、元件化拆解
上面說過,模組是由一個或多個元件為基礎,幷包含相關業務程式碼的集合,所以要實現模組化,首先要做的是元件化的拆解。而元件是單一且獨立業務無關的元件,在這個例子中,將會拆解得到以下幾個元件。
Network:用於請求 HTTP API 的元件
Socket:用於維持 Socket 長連線的元件
二、模組化拆解
在基礎的元件化拆解完成後,需要對專案業務相關的部分進行拆解形成一個個的模組,拆解的粒度根據專案大小,業務結構以及實際需求均有不同,針對此例子,作為聊天專案,拆解為以下幾個模組。
Auth:登入和身份認證的模組
Chat:處理和展示聊天對話的模組
Contacts:提供聯絡人,群組等資訊的模組
Socket:管理長連線狀態,分發訊息的模組
三、膠水模組
膠水模組顧名思義是將各個業務模組相關聯起來的模組。各模組之間要能夠互相通訊,呼叫,整合,缺少不了膠水模組發揮的作用。本例項提供了兩個膠水模組:
App:在最外層將各模組整合起來的模組
Service:在業務模組下層支撐業務模組間互動與通訊的模組
整理一下目前的元件和模組,可以得到以下結構圖

整體結構圖.png
其中模組與模組,元件與元件,模組與元件的依賴關係應該是垂直從上到下的依賴,而不應該產生橫向的或者從下到上的依賴
四、模組配置
在本步之前假設 Network 和 Socket 兩個元件都已經發布到遠端倉庫了,各個模組需要使用的直接引用即可。
根據以下路徑建立四個業務模組和兩個膠水模組,在此可將 Android Studio 的 Module 認為是上文所述的模組。
Android Studio - File - New Module - Android Library
建立成功後此時在各個模組的 build.gradle 檔案中第一行均引用 Library 外掛
apply plugin: 'com.android.library'
在開發中通常使用兩個外掛設定該工程的執行型別
App 外掛: com.android.application
Library 外掛: com.android.library
因為我們是需要各個業務模組不但能夠整合執行,也要能夠單獨執行,所以此處需要一個變數用於改變外掛,在專案根目錄下建立 config.gradle ,然後在裡面填入
ext { authIsApp = false contactsIsApp = false chatIsApp = false socketIsApp = false }
這樣我們可以在模組的 build.gradle 中引用 config.gradle 並訪問裡面的變數,根據變數的值去使用不同的外掛,以支援模組單獨以 App 的方式執行
apply from: rootProject.file('config.gradle') if (contactsIsApp) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
模組在單獨執行的時候還需要有 applicationId,這個也可以根據變數來控制
android { defaultConfig { if (contactsIsApp) { applicationId "com.test.contacts" } } }
為了各個模組之間的資原始檔名稱不被混淆,可以指定一個資源名稱字首,如果不合符要求,IDE 會有報錯提示
android { resourcePrefix "contacts_" }
最後我們還需要準備兩套 AndroidManifest 檔案,一套是用於整合除錯時模組作為一個 Library 會合併到主工程中,在這個檔案中只需要包含許可權申請以及相關元件的註冊即可,另外一套是模組單獨執行時需要自己獨立的相關配置,需要完整的全套配置,在 build.gradle 中新增如下程式碼:
android { sourceSets { main { // 單獨除錯與整合除錯時使用不同的 AndroidManifest.xml 檔案 if (contactsIsApp) { manifest.srcFile 'src/main/manifest/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } }
在整合除錯的情況下所用到的 AndroidManifest 檔案內容如下
<?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"/> //本例項中此模組沒有需要對外提供的相關元件(Activity, Service..),所以無需註冊 </manifest>
在單獨執行時所用的 AndroidManifest 檔案內容如下
<?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>
而各個模組的引用關係如下
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') }
這裡可以看到,對於四個業務模組使用了 runtimeOnly 的方式進行引用,表示在編譯期間不可見,但是會參與打包到 APK 並在執行期間可見。這就防止了我們互相直接訪問橫向模組的類或資原始檔,更好的做了隔離。前面的 if(!chatIsApp)
判斷是為了在其他模組單獨作為 App 執行的時候在此處不進行依賴,因為一個 App 依賴另一個 App 會出錯。最後下方引用了 service,根據我們的結構設計,service 作為整個架構的中心會被所有模組直接依賴。
五、模組間方法呼叫
為了做到解耦,模組間是沒有互相引用的,所以不能直接呼叫對方的方法,但是各模組都引用了 Service,可以藉由 Service 來實現解耦並且介面化的模組間方法呼叫。這裡有幾種不同的方案可供參考
自定義介面並註冊
在 Service 定義介面,以及建立管理類 ApiFactory,在業務模組中實現介面,並且在業務模組初始化的時候將業務模組的實現類傳遞給 ApiFactory,其他模組即可利用 ApiFactory 呼叫相關方法。程式碼如下:
Service 模組中定義介面並建立管理類
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 模組中實現介面並且註冊
class AuthApi : IAuthApi { override fun isLogin(): Boolean { return true } } class AuthApp : Application() { override fun onCreate() { super.onCreate() // 將 AuthApi 類的例項註冊到 ApiFactory ApiFactory.setAuthApi(AuthApi()) } }
這裡會存在一個問題,上面的程式碼是在 AuthApp 裡面去註冊的,但是應用在執行的時候不會去載入各個模組中的 Application,而是載入主工程 App 中的 Application,所以這裡需要用一個反射的方法去實現模組 Application 的初始化。
模組 Application 的初始化
為了能夠對各個模組中做一些初始化的操作,我們在 Service 模組中建立了一個 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 在應用啟動時被呼叫,我們還需要在 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) { } } } }
至此,所有模組中需要初始化的內容都與 App 模組繫結在了一起初始化,並都能夠向 ApiFactory 中註冊自己模組介面的具體實現了。
使用第三方庫實現介面註冊
在所有模組 的 build.gradle 新增如下程式碼
apply plugin: 'kotlin-kapt' dependencies { implementation ("com.alibaba:arouter-api:1.4.1") kapt ("com.alibaba:arouter-compiler:1.2.1") }
這裡個問題需要注意,如果沒有使用 Kotlin,就不需要匯入 kapt 外掛,用 annotationProcessor
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
在 Application 中初始化 ARouter
class MainApp : Application() { override fun onCreate() { super.onCreate() ARouter.init(this) }
匯入 ARouter 後,分為以下幾步使用
- 在 Service 模組中定義介面,並且繼承 IProvider
interface IAuthApi : IProvider { fun isLogin(): Boolean }
- 在 Auth 模組中實現介面
//該註解為 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()
以上三步完成後則可以實現模組間的方法互相呼叫了
- 為了方便呼叫,可以在 Service 集中管理介面,介面的 Path 用常量統一管理
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 } } //實現時也用常量 @Route(path = Api.AUTH_API) class AuthApi() : IAuthApi { override fun isLogin(): Boolean { return true } }
以上,就是模組之間方法呼叫的一些方案
六、模組間頁面呼叫
這裡主要還是依賴上一步提到的 ARouter,使用 ARouter 可以容易的實現模組之間的 Activity 跳轉,Fragment 例項獲取。
同樣的為了方便管理,我們在 Service 中集中管理這些資源
- 在業務模組建立 Fragment,並且使用 Route 註解設定 Path
@Route(path = "/fragment/contacts") class ContactsFragment : Fragment() { }
- 在需要使用到這個 Fragment 的地方呼叫 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 } }
模組中建立對應元件的時候直接使用 Service 中的常量
@Route(path = Router.Fragments.CONTACTS_FRAGMENT) class ContactsFragment : Fragment() { }
這裡需要特別注意的問題是: ARouter 會對 Path 進行分組,在預設情況下,Path 最少由兩級組成,其中的第一級為組名,如果在不同的模組使用了同一個組名 則會報錯。
例如 :
/auth/activity/login 其中 auth 為組名,這裡我們通常使用模組名作為組名,不要跨模組使用同樣的組名,不管是頁面還是介面都不行。
七、模組間資料互動
說到模組間的資料互動,首先要確定的是: 模組間以什麼樣的資料格式進行互動
比如 Contacts 模組中有 Member 這個物件,那 Chat 模組在呼叫 Contacts 模組方法的時候可能會需要返回一個 Member 型別的返回值,此時如果 Chat 模組中不存在 Member 或者 Service 中定義介面的時候沒有這個類的話,是無法進行下去的,此時需要一些方案來解決這個問題。
方案一、Model 下沉
第一種解決方法可能是將 Member 這個類進行下沉,放到一個公共的元件中,然後所有的模組都引用它,這樣所有的模組都可以使用這個 Model ,但是此方法會因為業務改動而去頻繁的改動下層元件,開發起來是十分不便的,也不符合模組化的理念,畢竟所有的 Model 都揉在一起了。
方案二、使用 API 子模組
此方法是將需要提供對外服務的業務模組中的 Model、介面、Event 等獨立到一個 API 子模組,這個 API 子模組可以被 Service 模組直接引用,這樣其他業務模組引用 Service 模組時間接的獲取到了相關的 Model,但是因為只是引用了 API 子模組而依然保持了和主要的業務模組的隔離。但是此方案會增加模組數量以及改動子模組的 Model 時,可能導致其他使用到該 Model 的模組發生異常。

方案三、各自維護 Model,使用通用格式通訊
以上兩種方案都是在業務模組中可以直接使用定義好的 Model,但是也存在著各自的問題。換一個思路去看的話,如果使用通用格式去通訊,各自維護自己的 Model 是否能更加合適我們的場景。按照上面的例子中 Chat 模組需要從 Contacts 模組中獲取一個 Member 的場景,如果從 Contacts 模組中返回的是一個 Json,由 Chat 模組根據返回的 Json 建立一個結構簡潔的 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 ,並轉換成需要的物件使用,比如需要在聊天列表顯示成員的頭像和名字,那隻需要其中三個欄位即可。
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
此方式的問題在於使用方在獲取資料後均需要經過一個轉換過程才能使用,在時間和程式碼上會有一定冗餘,不過優勢在於各模組之間可以做到 Model 獨立,資料獨立。
總結:其實不管任何方式都存在各自的優缺點以及適合的場景,主要還是取決於開發團隊的實際情況取捨以及專案所需要應對的業務場景
八、模組間事件通訊
通常可以按照第五步的模組間方法呼叫實現回撥介面,但是在事件的通訊上使用觀察者模式會更為簡單清晰,所以可以考慮使用 EventBus 來作為模組間通訊的橋樑,在專案中我們有用到 RxJava,這裡也可以使用 RxJava 來實現一個簡單的事件分發系統。
需要在 Service 模組中實現下面的 EventBus 集中分發事件
並在 Service 中定義 Event Model
//用 RxJava 實現的 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() } } //建立一個空介面 interface BaseEvent //繼承介面實現一個 Event,用於通知長連線的連線狀態 class SocketEvent(val event: Event, val error: Throwable?) : BaseEvent { enum class Event { CONNECTED, DISCONNECTED } }
在其他模組中使用
//在 A 模組發出 Event EventBus.post(SocketEvent(SocketEvent.Event.DISCONNECTED, e)) //在 B 模組監聽 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}") } } }
九、模組單獨執行
在第四步裡面做了一些為單獨執行準備的一些配置,在第五步中實現了模組 Application 的初始化也能為我們切換整合和單獨執行提供便利。
首先明確的是,單獨一個模組,是不一定能夠完全獨立執行的,他或許可以單獨執行,也可能需要依賴其他幾個模組執行。比如 Auth 模組具有獨立執行的條件,但是 Contacts 模組則不是,他需要有 Auth 來提供賬號資訊獲取資料,如果需要的話,還可以加入 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) //初始化依賴的相關模組 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 用於顯示聯絡人列表,是沒有一個 Activity 的,所以此時應該建立一個用於獨立執行時的啟動頁
@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) // 這裡會呼叫 Auth 模組的方法判斷是否登入 if (Api.getAuthApi().isLogin()) { Api.getSocketApi().startSocketService() } else { Router.startLoginActivity(this) } } }
可以看到上文中在未登入的狀況下會跳轉到登入介面,那麼登入之後又是去到哪個頁面呢?這裡可以在 Service 中集中控制跳轉路由
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() } //通過此方法獲取當前登入後的首頁 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>
十、模組間資料變化通知
這裡有個場景,比如我在 Chat 模組中的聊天列表中用到了 Contacts 模組的 Member 頭像名字等資訊,但是如果這個 Member 發生了變化,此時 Chat 是不得而知的,所以需要一個模組間資料變化通知的機制,因為資料庫使用了 Realm,所以可以利用 Realm 的更新通知機制來通知其他模組。
這裡在各模組的 Api 中添加了監聽資料庫變化的方法
@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) } }
在其他模組可以註冊監聽
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)) }) //發生變化後就對 vchannels 重新賦值觸發列表更新 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() } }
這裡如果使用了其他的資料庫,也可以根據各個資料庫的通知機制來實現
十一、總結
總結一下目前的方案,各模組之間互相隔離,各自獨立負責資料儲存。各模組可以單獨執行或者依賴必要模組執行。模組間的資料交換使用 Json 格式,由呼叫方根據需要做最後轉換。模組間的事件通知用 RxJava 或者 EventBus 實現。跨模組的資料變化依賴 Realm 通知,
其中所有模組以 Service 為中心,包含了以下幾個重要類:
Api : 用於獲取某模組的對方方法介面
Router : 用於跳轉其他模組頁面或者獲取 Fragment 或 View
EventBus : 用於跨模組的事件傳遞