You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过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前台调用,不会遇到这个问题。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:05:25