Android 模块化/组件化 方案实践

模块化方案实践

为什么需要模块化

  1. 在项目开发到一定阶段,随着功能需求越来越多,代码结构越来越臃肿,维护也随之越来越麻烦,单次编译调试的时间越来越长,每一次修改都很容易牵一发而动全身。
  2. 在大规模开发团队中对大项目的协作开发可能被拆分到多个事业部,每个事业部有独立的开发,测试团队和独立的部署需求,在单工程高耦合的情况下难以为继。
  3. 在 toB 的产品中,可能涉及到为客户做定制化的修改或单纯向客户提供部分功能,并且要将主线产品的最新功能及时更新给客户,在单工程或者分支开发的情况下实现起来依然较为麻烦
  4. 在公司的多个业务线中,可能会用到一些公用的业务功能,在非模块化的情况下每个业务项目均需要重复实现。

组件化和模块化

在我的理解中,组件化是对项目的某项功能的抽离,模块化是对项目某项业务的抽离,所以我们先明确一下组件和模块的区别,这样在下面的内容中有助于理解。

组件:由单一且独立的功能构成业务无关的组件

模块:由一个或多个组件作为基础,并包含相关业务代码的模块


项目实例

以下将会用一个模块化的聊天项目作为例子,阐述构建一个模块化项目的整个流程。该项目可以进行登录,查看联系人、群组、对话。

模块化需要解决的几个问题

  1. 组件与组件之间,模块与模块之间保持横向隔离
  2. 各模块拥有独立运行调试的能力
  3. 各模块之间可以互相通信及调用方法

目录:

一、组件化拆解

二、模块化拆解

三、胶水模块

四、模块配置

五、模块间方法调用

六、模块间页面调用

七、模块间数据交互

八、模块间事件通信

九、集成运行和单独运行

十、模块间数据变化通知

十二、总结


一、组件化拆解

上面说过,模块是由一个或多个组件为基础,并包含相关业务代码的集合,所以要实现模块化,首先要做的是组件化的拆解。而组件是单一且独立业务无关的组件,在这个例子中,将会拆解得到以下几个组件。

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 后,分为以下几步使用

  1. 在 Service 模块中定义接口,并且继承 IProvider
interface IAuthApi : IProvider {
    fun isLogin(): Boolean
}
  1. 在 Auth 模块中实现接口
//该注解为 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()

以上三步完成后则可以实现模块间的方法互相调用了

  1. 为了方便调用,可以在 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 中集中管理这些资源

  1. 在业务模块创建 Fragment,并且使用 Route 注解设置 Path
@Route(path = "/fragment/contacts")
class ContactsFragment : Fragment() {
}
  1. 在需要使用到这个 Fragment 的地方调用 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
    }
}

模块中创建对应组件的时候直接使用 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 : 用于跨模块的事件传递

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容