Android Kotlin中LiveData Observer第二次请求未触发问题
问题描述
实现侧边菜单点击触发API请求的功能时,预期每次点击菜单发起请求后,绑定的Observer都会被触发。实际测试表现如下:
- 首次点击菜单时Retrofit可正常返回正确响应
- 第二次点击发起请求时Observer未被触发
- 初步排查确认Retrofit层已经正常更新MutableLiveData的值,但Repository层未返回对应数据
问题根因
出现第二次点击Observer不触发的问题,核心是数据层的LiveData设计逻辑错误:
- 每次调用
getDisastersByFilter()时,RemoteDataSource都会新建一个独立的MutableLiveData实例,Repository层的map操作也会基于这个新实例生成新的转换后LiveData,最终ViewModel返回给调用方的是一个全新的、没有任何观察者绑定的LiveData。 - MapsFragment仅在页面初始化时,对第一次调用
getDisastersByFilter("gempa")返回的旧LiveData做了订阅,后续点击菜单触发的请求,返回的新LiveData和之前订阅的旧实例完全无关,即使Retrofit正常postValue,也无法触发已经绑定在旧实例上的Observer。 - 现有代码没有统一的数据分发容器:点击菜单时仅执行了创建新LiveData、发起请求的逻辑,没有把新的请求结果和Fragment正在观察的数据源做关联,相当于请求发出去之后,结果被扔到了一个没人监听的新对象里,自然不会有UI回调。
原有问题代码
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> { val result = MutableLiveData<ApiResponse<DisastersDTO?>>() apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> { override fun onResponse( call: Call<DisastersResponse>, response: Response<DisastersResponse> ) { if(response.isSuccessful) { val data = response.body() data?.disastersDTO?.let { result.postValue(ApiResponse.Success(it)) Log.d("RemoteDataSource", "$it") } ?: run { result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan")) } } else { result.postValue(ApiResponse.Error("Terjadi kesalahan!")) } } override fun onFailure(call: Call<DisastersResponse>, t: Throwable) { result.postValue(ApiResponse.Error(t.localizedMessage!!)) Log.d("RemoteDataSource", t.localizedMessage!!) } }) return result }
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> = remoteDataSource.getDisastersByFilter(filter).map { when (it) { is ApiResponse.Empty -> Resource.Error("Terjadi error") is ApiResponse.Error -> Resource.Error(it.errorMessage) is ApiResponse.Loading -> Resource.Loading() is ApiResponse.Success -> Resource.Success( DataMapper.disastersResponseToDisasterDomain( it.data ) ) } }
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> = useCase.getDisastersByFilter(filter)
MapsFragment 相关代码
private val viewModel: SharedViewModel by activityViewModels() viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) { when (it) { is Resource.Success -> { Log.d("MapsFragmentFilter", "${it.data}") it.data?.let { listDisaster -> if(listDisaster.isNotEmpty()) { map.clear() addGeofence(listDisaster) listDisaster.map { disaster -> placeMarker(disaster) addCircle(disaster) } } } } is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show() is Resource.Loading -> {} } }
MainActivity 触发请求相关代码
private val viewModel: SharedViewModel by viewModels() binding.navViewMaps.setNavigationItemSelectedListener { menu -> when (menu.itemId) { R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa") R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir") R.id.about_us -> viewModel.getDisasters() } binding.drawerLayoutMain.closeDrawers() true }
修复方案
按照以下步骤调整代码即可解决问题:
- 改造ViewModel,持有唯一的可观察数据源
不要在getDisastersByFilter方法里直接返回新的LiveData,而是在ViewModel内部定义一个固定的MutableLiveData作为所有请求结果的分发容器,观察者只需要订阅这一个实例即可。// 固定的数据分发实例,全局唯一 private val _disasterList = MutableLiveData<Resource<List<Disaster>>>() val disasterList: LiveData<Resource<List<Disaster>>> = _disasterList fun getDisastersByFilter(filter: String? = "gempa") { // 先推送加载状态 _disasterList.postValue(Resource.Loading()) // 发起请求,将结果统一发射到固定的_diasterList中 viewModelScope.launch { useCase.getDisastersByFilter(filter).collect { response -> _disasterList.postValue(response) } } } - 调整Fragment的观察逻辑
只需要在Fragment初始化时,订阅ViewModel中固定的disasterList即可,不需要在调用请求方法的时候链式observe。private val viewModel: SharedViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 仅订阅一次固定数据源 viewModel.disasterList.observe(viewLifecycleOwner) { when (it) { is Resource.Success -> { Log.d("MapsFragmentFilter", "${it.data}") it.data?.let { listDisaster -> if(listDisaster.isNotEmpty()) { map.clear() addGeofence(listDisaster) listDisaster.map { disaster -> placeMarker(disaster) addCircle(disaster) } } } } is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show() is Resource.Loading -> {} } } // 页面初始化默认加载一次数据 viewModel.getDisastersByFilter("gempa") } - 调整数据层实现
RemoteDataSource和Repository层不需要每次请求都创建新的LiveData,可以改为直接返回业务数据,或者用Flow做数据流转换,最终所有结果都汇总到ViewModel的固定LiveData中即可。如果要保留现有层级写法,只需要确保上层始终持有同一个可观察实例,不要每次请求生成新的LiveData。 - 调整菜单点击逻辑
MainActivity中点击菜单时,直接调用ViewModel的请求方法即可,不需要处理返回值,原有点击逻辑除了不需要接收返回值之外不需要做其他改动。
内容的提问来源于stack exchange,提问作者Mohammad Azri Khairuddin
相关产品推荐
相关产品推荐

