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的数据流向:
避坑指南
最后,分享几个我踩过的坑:
- BR类冲突:不同模块的BR类包名不同,但字段名可能重复。建议在接口中定义清晰的命名规范,比如加模块前缀。
- 布局文件引用:模块A的布局文件里,如果引用了模块B的ViewModel,编译会报错。记住:布局文件只能引用当前模块的类。
- 双向绑定失效:跨模块的双向绑定(
@={})容易出问题。我建议只在模块内部使用双向绑定,跨模块通信统一用接口+LiveData。
我的经验:模块化不是银弹。如果你的App只有两三个页面,没必要搞模块化。但一旦团队超过5个人,或者代码量超过10万行,模块化带来的收益就非常明显了。
好了,关于DataBinding与模块化的内容就聊到这里。记住:接口隔离、依赖注入、统一版本管理,这三件事做好了,多模块下的DataBinding就能跑得很顺畅。
公众号:蓝海资料掘金营,微信deep3321