24、LiveData与ContentProvider:监听联系人变化、监听短信变化、实战:通讯录同步

说实话,ContentProvider 这个组件,很多 Android 开发平时用得不多。

但一旦涉及到跨进程数据共享,或者监听系统数据变化,你就绕不开它。

我个人习惯把 ContentProvider 理解成「数据的水龙头」——你拧开它,数据就流过来;你注册一个观察者,水龙头一开一关你都能知道。

这一章,我们就拿联系人和短信这两个最典型的系统数据源开刀,看看怎么用 LiveData 把它们的变化实时监听下来,最后做一个完整的通讯录同步功能。

为什么是 ContentProvider + LiveData?

你想想看,系统联系人、短信这些数据,不是你的 App 私有的。它们是系统进程维护的,其他 App 也能读写。

所以你不能直接搞个 Room 去监听——Room 监听的是你自己的数据库。

那怎么办?

系统提供了 ContentResolver 的 registerContentObserver 方法,你传一个 URI 进去,数据一变它就回调你。

但回调是传统的 Listener 模式,跟 ViewModel 的生命周期不搭。

所以我们要做的,就是把 ContentObserver 包装成 LiveData。

这样,你的 Activity/Fragment 只要 observe 一下,数据变化自动通知,页面自动刷新。

我在项目中遇到过好几次这种场景:用户在外面改了个联系人备注,回到 App 列表页还是旧的。嗯,其实就是没监听 ContentProvider 的变化。

监听联系人变化

先看最简单的场景:监听系统联系人表的变化。

核心思路就三步:

  • 拿到 ContentResolver
  • 注册一个 ContentObserver,监听 ContactsContract.Contacts.CONTENT_URI
  • 把回调结果通过 LiveData 发射出去

代码写起来其实很简洁:

class ContactObserverLiveData(context: Context) : LiveData<List<Contact>>() {

    private val contentResolver = context.contentResolver
    private val observer = object : ContentObserver(null) {
        override fun onChange(selfChange: Boolean) {
            loadContacts()
        }
    }

    override fun onActive() {
        super.onActive()
        contentResolver.registerContentObserver(
            ContactsContract.Contacts.CONTENT_URI,
            true,
            observer
        )
        loadContacts()
    }

    override fun onInactive() {
        super.onInactive()
        contentResolver.unregisterContentObserver(observer)
    }

    private fun loadContacts() {
        // 这里用协程或者 AsyncTask 去查联系人,避免卡主线程
        // 查完后 postValue 到 LiveData
    }
}

关键点:

  • onActive 在有观察者时注册监听,onInactive 在没有观察者时取消注册。这是 LiveData 的生命周期感知优势。
  • 第二个参数 true 表示监听子 URI 的变化,比如某个具体联系人的修改。

我的小技巧:

loadContacts 里我习惯用 withContext(Dispatchers.IO) 切到 IO 线程查询,然后用 postValue 把结果发回主线程。这样既不会卡 UI,又能保证数据及时更新。

监听短信变化

短信的监听套路跟联系人几乎一样,只是 URI 不同。

短信的 URI 是 Telephony.Sms.CONTENT_URI

不过这里有个坑——我曾经踩过。

避坑指南:

从 Android 14(API 34)开始,读取短信需要 READ_SMS 权限,并且 Google Play 对敏感权限审核非常严格。如果你的 App 不是默认短信 App,大概率拿不到这个权限。

所以,监听短信变化这个功能,更多是用在系统级 App 或者企业定制设备上。普通应用建议用 SmsRetriever API 做短信验证码自动填充,别直接读短信表。

代码结构跟联系人一样,只是把 URI 换掉,查询的列名换一下:

class SmsObserverLiveData(context: Context) : LiveData<List<SmsMessage>>() {

    private val contentResolver = context.contentResolver
    private val observer = object : ContentObserver(null) {
        override fun onChange(selfChange: Boolean) {
            loadSms()
        }
    }

    override fun onActive() {
        super.onActive()
        contentResolver.registerContentObserver(
            Telephony.Sms.CONTENT_URI,
            true,
            observer
        )
        loadSms()
    }

    override fun onInactive() {
        super.onInactive()
        contentResolver.unregisterContentObserver(observer)
    }

    private fun loadSms() {
        // 查询短信表,注意权限检查
    }
}

实战:通讯录同步

好,前面两个都是热身。现在我们来做一个真正能用的东西:通讯录同步。

需求很简单:

  • App 启动时,把系统联系人同步到本地 Room 数据库
  • 系统联系人发生变化时,自动增量同步
  • 同步过程不能卡 UI,要有进度反馈

架构图我画了一张,你看一眼就明白了:

通讯录同步架构图 系统联系人 ContentProvider ContentObserver LiveData 包装 onChange ViewModel 协程 + 业务逻辑 observe Room 数据库 本地联系人表 写入/查询 Activity / Fragment UI 观察者 LiveData 数据流方向:系统 → Observer → ViewModel → Room / UI

看到这个图,你应该能理解整个数据流了。

系统联系人变化 → ContentObserver 收到通知 → LiveData 发射事件 → ViewModel 拿到事件后启动协程去查询增量数据 → 写入 Room → Room 的 LiveData 自动通知 UI 刷新。

整个过程完全自动化,而且生命周期安全。

核心代码实现

ViewModel 里我这样写:

class ContactSyncViewModel(context: Context) : ViewModel() {

    private val contactObserver = ContactObserverLiveData(context)
    private val contactDao = AppDatabase.getInstance(context).contactDao()

    // 对外暴露的联系人列表,UI 直接 observe 这个
    val contacts: LiveData<List<Contact>> = contactDao.getAllContacts()

    // 同步状态,用于显示加载进度
    private val _syncState = MutableLiveData<SyncState>(SyncState.IDLE)
    val syncState: LiveData<SyncState> = _syncState

    init {
        // 监听系统联系人变化
        contactObserver.observeForever { newList ->
            // 这里 newList 是系统最新的联系人列表
            // 跟本地数据库做 diff,只更新变化的部分
            viewModelScope.launch(Dispatchers.IO) {
                _syncState.postValue(SyncState.SYNCING)
                val localContacts = contactDao.getAllContactsOnce()
                val diff = computeDiff(localContacts, newList)
                contactDao.insertOrUpdate(diff.added)
                contactDao.delete(diff.removed)
                _syncState.postValue(SyncState.SUCCESS)
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        contactObserver.removeObserver { }
    }
}

注意:

这里我用了 observeForever,因为 ViewModel 本身没有生命周期所有者。但一定要在 onCleared 里移除观察者,否则会内存泄漏。

如果你是在 Fragment 里创建这个 LiveData,直接用 observe(viewLifecycleOwner) 更安全。

增量同步的 diff 算法

全量同步太粗暴了,每次把系统联系人全部拉下来覆盖本地,性能差不说,还容易丢用户本地修改的数据。

我建议的做法是:

  • 用系统联系人的 CONTACT_ID 作为唯一键
  • 本地数据库也存这个 ID
  • 对比两个集合,找出新增、删除、修改的记录
  • 只对变化的部分做操作

代码大概长这样:

fun computeDiff(local: List<Contact>, remote: List<Contact>): DiffResult {
    val localMap = local.associateBy { it.id }
    val remoteMap = remote.associateBy { it.id }

    val added = remote.filter { it.id !in localMap }
    val removed = local.filter { it.id !in remoteMap }
    val modified = remote.filter { remoteContact ->
        val localContact = localMap[remoteContact.id]
        localContact != null && localContact != remoteContact
    }

    return DiffResult(added + modified, removed)
}

这样,即使系统联系人有一千个,每次变化只处理几个,性能完全没问题。

UI 层的使用

在 Activity 里,使用起来就很简单了:

class ContactListActivity : AppCompatActivity() {

    private val viewModel: ContactSyncViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_contact_list)

        viewModel.contacts.observe(this) { contacts ->
            // 更新 RecyclerView
            adapter.submitList(contacts)
        }

        viewModel.syncState.observe(this) { state ->
            when (state) {
                SyncState.SYNCING -> showLoading()
                SyncState.SUCCESS -> hideLoading()
                SyncState.IDLE -> { /* 什么都不做 */ }
            }
        }
    }
}

你看,Activity 的代码非常干净。它不需要知道 ContentProvider 的存在,也不需要关心同步逻辑。

这就是 LiveData + ViewModel 的魅力——把复杂的数据源监听封装在底层,上层只管 observe。

总结一下

这一章我们做了三件事:

  • 用 LiveData 包装 ContentObserver,监听联系人变化
  • 同样的套路监听短信变化(但要注意权限限制)
  • 做了一个完整的通讯录同步功能,包含增量 diff

说白了,ContentProvider 监听本身不难,难的是把它跟 ViewModel 的生命周期管理结合起来。

LiveData 在这里扮演了「胶水」的角色——把系统回调转成可观察的数据流,让 UI 层可以无感知地响应变化。

我在实际项目中,这套模式用了好几年,稳定可靠。你只要注意好权限和生命周期,基本不会出问题。

最后一个小建议:

如果你要监听的数据源不止一个(比如同时监听联系人和通话记录),可以考虑写一个通用的 ContentObserverLiveData,把 URI 作为构造参数传进去。这样代码复用性更高。


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