27、DataBinding与模块化:多模块Binding配置、模块间数据共享、接口与实现分离、依赖管理

模块化开发,说白了就是把一个巨型App拆成几个小工程。每个模块各司其职,互不干扰。但问题来了——DataBinding在这种多模块架构下,该怎么配置?模块之间怎么共享数据?接口和实现怎么分离?依赖又该怎么管理?

嗯,这些坑我基本都踩过。今天咱们就一次性说清楚。

多模块下的Binding配置

先说说最基础的事:每个模块都要单独开启DataBinding。

build.gradle.kts里,每个模块都要加上这么一段:

android {
    ...
    buildFeatures {
        dataBinding = true
        viewBinding = true
    }
}

你可能会想:「我在主工程开了,子模块是不是自动继承?」

答案是:不会。每个模块都是独立的Gradle工程,配置必须自己写。我刚开始做模块化时,就因为这个漏配,折腾了半小时才找到原因。

核心原则:每个模块独立配置DataBinding,主工程不会自动传递给子模块。

模块间数据共享:别直接传Binding对象

模块A的Binding对象,能不能直接传给模块B?

我个人习惯是:绝对不要

为什么?因为模块B根本不知道模块A的布局文件长什么样。Binding类是编译时生成的,模块B的classpath里没有模块A的BR类,直接传会报编译错误。

那怎么办?用接口。

举个例子。假设有个user模块,它想把自己的用户数据暴露给profile模块:

// 在 user 模块中定义接口
interface IUserProvider {
    fun getUserName(): ObservableField<String>
    fun getUserAvatar(): ObservableField<String>
}

// 在 user 模块中实现
class UserProviderImpl : IUserProvider {
    override fun getUserName() = ObservableField<String>("张三")
    override fun getUserAvatar() = ObservableField<String>("avatar_url")
}

然后profile模块依赖这个接口,而不是具体的实现类:

// 在 profile 模块中
class ProfileViewModel(private val userProvider: IUserProvider) : ViewModel() {
    val userName = userProvider.getUserName()
    val userAvatar = userProvider.getUserAvatar()
}

这样,两个模块之间只通过接口通信,Binding对象完全隔离。你想想看,是不是清爽多了?

小技巧:接口里的字段尽量用ObservableField或LiveData,这样DataBinding才能自动刷新UI。

接口与实现分离:依赖注入是关键

接口定义好了,实现类怎么注入到其他模块?

我建议用依赖注入框架,比如Hilt或Koin。这里我用Hilt举个例子。

首先,在user模块里把实现类绑定到接口:

@Module
@InstallIn(SingletonComponent::class)
abstract class UserModule {
    @Binds
    abstract fun bindUserProvider(impl: UserProviderImpl): IUserProvider
}

然后在profile模块里直接注入接口:

@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val userProvider: IUserProvider
) : ViewModel() {
    // 直接用
}

这样做的好处很明显:

  • 编译期隔离:profile模块只依赖接口,不依赖实现
  • 运行时替换:测试时可以轻松mock
  • 解耦彻底:改一个模块的实现,其他模块不用重新编译

我曾经在一个大项目里,因为没做接口分离,改了一个模块的字段名,结果三个模块都要重新编译。嗯,那次之后我就老老实实把接口抽出来了。

依赖管理:统一版本,避免冲突

多模块最头疼的问题之一,就是依赖版本不一致。

比如模块A用了androidx.lifecycle:lifecycle-livedata-ktx:2.5.1,模块B用了2.6.0。虽然大部分情况下能兼容,但万一遇到API变更,那就是一场灾难。

我的做法是:在项目根目录的build.gradle.kts里统一管理版本。

// 根目录 build.gradle.kts
ext {
    set("lifecycleVersion", "2.6.0")
    set("databindingVersion", "8.1.0")
}

然后在每个模块里引用:

// 子模块 build.gradle.kts
val lifecycleVersion: String by rootProject.ext

dependencies {
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
}

或者用更现代的Version Catalog(版本目录):

// gradle/libs.versions.toml
[versions]
lifecycle = "2.6.0"

[libraries]
lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }

然后在模块里:

dependencies {
    implementation(libs.lifecycle.livedata)
}

注意:DataBinding的版本必须和AGP(Android Gradle Plugin)版本匹配。我曾经因为AGP升级到8.2但DataBinding版本没跟上,导致编译报错。建议统一用Version Catalog管理所有版本号。

模块间Binding数据流示意图

下面这张图展示了多模块下DataBinding的数据流向:

多模块DataBinding数据流 模块A(user) UserProviderImpl ObservableField<String> BR.userName 接口层 IUserProvider getUserName() 模块B(profile) ProfileViewModel @Inject IUserProvider Binding到布局 实现 依赖 关键点: 1. 模块A提供实现类,但对外只暴露接口 2. 模块B只依赖接口,不依赖具体实现 3. 接口中的字段使用ObservableField,保证DataBinding响应式更新 4. 依赖注入框架(Hilt/Koin)负责在运行时将实现注入到模块B 5. 模块A和模块B之间没有直接Binding依赖,完全解耦

避坑指南

最后,分享几个我踩过的坑:

  • BR类冲突:不同模块的BR类包名不同,但字段名可能重复。建议在接口中定义清晰的命名规范,比如加模块前缀。
  • 布局文件引用:模块A的布局文件里,如果引用了模块B的ViewModel,编译会报错。记住:布局文件只能引用当前模块的类。
  • 双向绑定失效:跨模块的双向绑定(@={})容易出问题。我建议只在模块内部使用双向绑定,跨模块通信统一用接口+LiveData。

我的经验:模块化不是银弹。如果你的App只有两三个页面,没必要搞模块化。但一旦团队超过5个人,或者代码量超过10万行,模块化带来的收益就非常明显了。

好了,关于DataBinding与模块化的内容就聊到这里。记住:接口隔离、依赖注入、统一版本管理,这三件事做好了,多模块下的DataBinding就能跑得很顺畅。


公众号:蓝海资料掘金营,微信deep3321