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,要有进度反馈
架构图我画了一张,你看一眼就明白了:
看到这个图,你应该能理解整个数据流了。
系统联系人变化 → 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