通过FileObserver设置LiveData时观察者未收到通知的问题
问题分析与解决方案
问题描述
我有一个带有LiveData属性的共享ViewModel。执行startLocationUpdates()时一切正常,观察者能收到通知。但当日志文件(JSON)通过WorkManager(可能在不同进程/线程)修改,触发FileObserver加载JSON文件并添加到LogViewModel时,LiveData未触发任何通知。想了解两种操作存在差异的原因,猜测是线程问题,但原以为LiveData会处理这类问题。
相关代码
MainActivity代码
private val logViewModel by viewModels<LogViewModel>() fun startLocationUpdates() { val locationItem = LocationItem(...) logViewModel.addLogEntry(locationItem) } override fun onCreate(savedInstanceState: Bundle?) { val observer = object : FileObserver(applicationContext.filesDir.path + "/" + jsonFileName) { override fun onEvent(event: Int, file: String?) { if (event == FileObserver.MODIFY) { loadLogViewModel() } } } observer.startWatching() } fun loadLogViewModel() { val json = getData(this) val myType = object : TypeToken<ArrayList<LocationItem>>() {}.type if (json != null) { val listItems: ArrayList<LocationItem> = gson.fromJson(json, myType) for (item in listItems) { logViewModel.addLogEntry(item) } } }
LogFragment代码
private val logViewModel: LogViewModel by activityViewModels() logViewModel.locationListItems.observe(requireActivity()) { locationItemAdapter.updateLocations() }
LogViewModel代码
class LogViewModel : ViewModel() { var locationListItems: MutableLiveData<LocationItem> = MutableLiveData<LocationItem>() fun addLogEntry(locationItem: LocationItem) { locationListItems.postValue(locationItem) } }
原因分析
1. 线程差异导致的通知行为不同
startLocationUpdates()在**应用主线程(UI线程)**执行,调用postValue时,任务会立即加入主线程消息队列,此时Activity处于活跃状态,观察者能正常接收通知。FileObserver的onEvent方法运行在Android系统的Binder线程池中,并非应用主线程。虽然postValue会自动切换到主线程,但存在两个关键问题:- 多次postValue被合并:
postValue是异步操作,短时间内连续调用(比如循环遍历列表时),LiveData会合并多次请求,仅将最后一次的value发送到主线程,前面的请求都会被丢弃。如果最后一个item与LiveData当前持有的value一致,就不会触发通知。 - 生命周期状态抑制:如果触发FileObserver时Activity处于后台(非活跃状态),LiveData会自动抑制通知,直到Activity回到前台才会发送。而
startLocationUpdates()通常在Activity前台调用,不会遇到这个问题。
- 多次postValue被合并:
2. ViewModel设计逻辑缺陷
当前locationListItems是MutableLiveData<LocationItem>,仅能存储单个位置项,但业务逻辑需要维护一个位置列表。这种设计会导致:
- 循环添加列表项时,后续的
postValue会覆盖前面的,最终观察者只能收到最后一个item。 - 当加载的JSON列表最后一个item与LiveData当前value一致时,LiveData不会触发通知(LiveData默认仅在value真正变化时发送通知)。
解决方案
1. 修正ViewModel的LiveData类型
改为存储列表的LiveData,确保每次更新整个列表,避免覆盖和合并问题:
class LogViewModel : ViewModel() { // 内部维护可变列表的LiveData,初始化为空列表 private val _locationListItems = MutableLiveData<MutableList<LocationItem>>(mutableListOf()) // 对外暴露不可变的LiveData,防止外部直接修改 val locationListItems: LiveData<List<LocationItem>> get() = _locationListItems // 添加单个项的方法 fun addLogEntry(locationItem: LocationItem) { val currentList = _locationListItems.value ?: mutableListOf() // 可选:避免重复添加 if (!currentList.contains(locationItem)) { currentList.add(locationItem) _locationListItems.postValue(currentList) } } // 直接替换整个列表的方法,适合从JSON加载的场景 fun setLogEntries(newItems: List<LocationItem>) { _locationListItems.postValue(newItems.toMutableList()) } }
2. 优化FileObserver的处理逻辑
不要循环调用addLogEntry,直接调用setLogEntries替换整个列表,避免多次postValue被合并:
fun loadLogViewModel() { val json = getData(this) val myType = object : TypeToken<ArrayList<LocationItem>>() {}.type if (json != null) { val listItems: ArrayList<LocationItem> = gson.fromJson(json, myType) logViewModel.setLogEntries(listItems) } }
3. 修正Fragment的观察者逻辑
观察者接收列表数据,更新Adapter时传入新列表:
logViewModel.locationListItems.observe(requireActivity()) { locationList -> locationItemAdapter.updateLocations(locationList) }
4. 修复内存泄漏与生命周期问题
在Activity销毁时停止FileObserver,避免内存泄漏:
override fun onDestroy() { super.onDestroy() observer.stopWatching() }
内容的提问来源于stack exchange,提问作者Houman
相关产品推荐
相关产品推荐

