Content Provider 架构设计:分层架构、Repository 模式、依赖注入
说实话,很多开发者对 Content Provider 的理解停留在「用起来麻烦」的阶段。我自己早期做项目时也是这样——能不用就不用,实在要跨进程才勉强写一个。直到后来接手一个大型医疗App,数据层乱成一锅粥,我才意识到:不是 Content Provider 不好用,而是架构没搭对。
今天我们就来聊聊,怎么把 Content Provider 的架子搭得既清爽又抗造。核心就三件事:分层架构、Repository 模式、依赖注入。
核心观点:Content Provider 不应该直接操作数据库,它只是一个「门面」。真正的业务逻辑和数据访问,应该交给下层去处理。
一、为什么需要分层?
你想想看,如果直接把 SQLiteOpenHelper 写在 Provider 的 query() 方法里,会有什么后果?
- 单元测试没法写——Provider 依赖 Android 框架
- 业务逻辑和数据库操作混在一起
- 换数据库(比如换 Room)要改一大片
我在一个项目中就踩过这个坑。当时为了赶进度,直接在 Provider 里写了十几行 SQL 拼接。后来需求变更,要加一个缓存层……嗯,那代码改得我头皮发麻。
所以,分层是必须的。我个人习惯把 Content Provider 相关的代码分成三层:
| 层级 | 职责 | 典型类 |
|---|---|---|
| 表现层 | 接收 URI、处理权限、返回 Cursor | MyProvider |
| 业务层 | 数据校验、缓存策略、跨表事务 | UserRepository |
| 数据层 | 数据库操作、网络请求 | UserDao / ApiService |
说白了,Provider 只负责「接客」,真正的脏活累活交给下层。
二、Repository 模式:数据来源的统一出口
Repository 模式不是什么新鲜东西。但在 Content Provider 的场景下,它有一个特别的好处:屏蔽数据来源的差异。
举个例子。你的用户数据可能来自本地数据库,也可能来自远程服务器。如果直接在 Provider 里判断「有没有网络」「缓存有没有过期」,那代码会变得非常臃肿。
用 Repository 模式,我们可以这样写:
class UserRepository(
private val localDataSource: UserDao,
private val remoteDataSource: UserApi
) {
fun getUsers(): Flow<List<User>> {
return localDataSource.getAllUsers()
.onStart {
// 尝试从网络刷新
try {
val remoteUsers = remoteDataSource.fetchUsers()
localDataSource.insertAll(remoteUsers)
} catch (e: Exception) {
// 网络失败,直接用缓存
}
}
}
}
这样,Provider 的 query() 方法就变得非常干净:
class UserProvider : ContentProvider() {
private lateinit var repository: UserRepository
override fun query(...): Cursor? {
// 这里只做一件事:调用 Repository
return repository.getUsers().toCursor()
}
}
我曾经在一个项目中,把 Repository 的返回值从 Cursor 改成了 Flow。改动只影响了一个类,Provider 完全不用动。这就是分层的好处。
小技巧:Repository 的构造函数不要依赖 Context。这样单元测试时就不用 mock Android 环境了。
三、依赖注入:让 Provider 不再「new」一切
Content Provider 的实例化是由系统控制的,你不能像 Activity 那样用构造函数传参。那怎么注入依赖呢?
我推荐两种方式:
方式一:Application 级别的注入
在 Application 类中初始化依赖容器,Provider 通过 Application 获取:
class MyApplication : Application() {
lateinit var appContainer: AppContainer
private set
override fun onCreate() {
super.onCreate()
appContainer = AppContainer(this)
}
}
class UserProvider : ContentProvider() {
private lateinit var repository: UserRepository
override fun onCreate(): Boolean {
val app = context.applicationContext as MyApplication
repository = app.appContainer.userRepository
return true
}
}
方式二:Hilt / Dagger 手动注入
如果你用 Hilt,可以这样:
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class UserProvider : ContentProvider() {
@Inject lateinit var repository: UserRepository
override fun onCreate(): Boolean {
// Hilt 会自动注入
return true
}
}
我个人更倾向 Hilt。因为手动管理 Application 容器,在多模块项目里容易乱。Hilt 的 @AndroidEntryPoint 对 Provider 的支持虽然晚了一点,但现在已经很稳定了。
注意:ContentProvider 的 onCreate() 是在主线程调用的。不要在注入过程中做耗时操作,否则会拖慢 App 启动速度。
四、整体架构图
下面这张图,是我自己项目里用的架构。你可以参考一下:
从图上你能看到:Provider 只依赖 Repository,Repository 只依赖数据源。每一层各司其职,互不越界。
五、避坑指南
最后分享几个我踩过的坑:
- 不要在 Provider 中持有 Activity 或 Fragment 的引用。Provider 的生命周期和 Application 一样长,容易内存泄漏。
- URI 匹配不要用 switch-case 写死。我建议用 UriMatcher 配合常量表,方便扩展。
- 批量操作记得用 ContentProviderOperation。我之前用循环调用 applyBatch,性能差到被测试骂。
- Repository 的缓存一定要考虑多进程。如果 Provider 跑在独立进程,内存缓存是无效的。
总结一下:Content Provider 的架构设计,核心就是「薄 Provider + 厚 Repository」。把数据逻辑抽出去,用依赖注入管理依赖,你的代码会变得非常容易测试和维护。
好了,这一章就到这里。下一章我们会聊聊 Content Provider 的权限模型——说实话,那才是真正让人头疼的地方。