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 启动速度。

四、整体架构图

下面这张图,是我自己项目里用的架构。你可以参考一下:

Content Provider 分层架构 表现层 ContentProvider (URI 分发、权限校验) 业务层 Repository (缓存策略、数据校验、跨表事务) 数据层 Room DAO / Retrofit API / SharedPreferences 依赖注入 Hilt / Dagger Application 容器 @Inject 箭头方向:调用关系 | 虚线:依赖注入关系

从图上你能看到:Provider 只依赖 Repository,Repository 只依赖数据源。每一层各司其职,互不越界。

五、避坑指南

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

  • 不要在 Provider 中持有 Activity 或 Fragment 的引用。Provider 的生命周期和 Application 一样长,容易内存泄漏。
  • URI 匹配不要用 switch-case 写死。我建议用 UriMatcher 配合常量表,方便扩展。
  • 批量操作记得用 ContentProviderOperation。我之前用循环调用 applyBatch,性能差到被测试骂。
  • Repository 的缓存一定要考虑多进程。如果 Provider 跑在独立进程,内存缓存是无效的。

总结一下:Content Provider 的架构设计,核心就是「薄 Provider + 厚 Repository」。把数据逻辑抽出去,用依赖注入管理依赖,你的代码会变得非常容易测试和维护。

好了,这一章就到这里。下一章我们会聊聊 Content Provider 的权限模型——说实话,那才是真正让人头疼的地方。


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