fix(fragment): 修复切换餐次时数据串台问题

三个餐次 Fragment 共享同一个 searchFoodState,导致早餐的本地数据会被错误合并到午餐/晚餐列表中。

修复方案:
- 新增 pendingDinnerType 字段记录本次请求对应的餐次
- 响应回来时比对餐次,忽略非本 Fragment 发起的请求结果
- 确保每个 Fragment 只处理自己发起的请求数据

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 17:16:34 +08:00
co-authored by Claude Sonnet 4.6
parent 67d64848de
commit 301f470274
2 changed files with 125 additions and 37 deletions
@@ -0,0 +1,70 @@
package com.sw.inbound.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import com.sw.inbound.GlobalData
import com.sw.inbound.GlobalKey
import com.sw.inbound.databinding.DialogEnvSwitchBinding
import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.toast
/**
* 环境切换弹窗
*
* 提供 TEST / UAT / PROD 三套环境的切换功能,
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
*
* @param context 上下文
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
*/
class EnvSwitchDialog(
context: Context,
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
) : BaseDialog(
context,
defWidth = 600.dp,
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
) {
private lateinit var binding: DialogEnvSwitchBinding
override fun getRootView(): View {
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
return binding.root
}
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.TEST_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
// 取消按钮
binding.btnCancel.setOnClickListener { dismiss() }
// 确认按钮
binding.btnConfirm.setOnClickListener {
// 未选中任何选项时提示用户
if (binding.rgEnv.checkedRadioButtonId == -1) {
context.toast("请选择环境")
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.TEST_BASE_URL
binding.rbUat.id -> GlobalData.UAT_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
GlobalData.appBaseUrl = newUrl
SPUtil.getInstance().put(GlobalKey.KEY_BASE_URL, newUrl)
onEnvChanged(newUrl)
dismiss()
}
}
}
@@ -28,7 +28,6 @@ import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.yanzhenjie.recyclerview.SwipeMenuItem
import java.io.Serializable
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
@@ -53,6 +52,12 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
private lateinit var activity: CookingModeActivity
/** 第一页时先查数据库存入此字段,网络结果回来后直接使用,避免时序问题 */
private var pendingLocalList: MutableList<CookFoodEntity>? = null
/** 记录本次请求对应的餐次,响应回来时比对,忽略非本 Fragment 发起的请求结果 */
private var pendingDinnerType: String = "0"
private var list: MutableList<FoodRecord> = mutableListOf()
private val dishAdapter by lazy {
FoodListAdapter(list = list).apply {
@@ -147,34 +152,37 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
}
/**
* 用 combine 同时监听网络结果与本地数据,网络成功时统一合并渲染
* pageNo > 1(加载更多)时 cookFoodListState 不参与合并,直接追加网络数据
* 只监听 searchFoodState,避免 cookFoodListState 变化触发重复渲染
* 第一页时使用 getDishList() 中预先查好的 pendingLocalList 参与合并;加载更多时直接追加。
* 三个 Fragment 共享同一个 searchFoodState,通过 pendingDinnerType 过滤非本 Fragment 发起的请求。
*/
private fun initObserver() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
activity.netViewModel.searchFoodState,
activity.appViewModel.cookFoodListState
) { networkState, localList -> Pair(networkState, localList) }
.collect { (networkState, localList) ->
activity.netViewModel.searchFoodState.collect { networkState ->
when (networkState) {
is UiState.Loading -> activity.showLoading()
is UiState.Success -> {
// 餐次不匹配,忽略(其他 Fragment 发起的请求)
if (dinnerType != pendingDinnerType) return@collect
activity.delayDismissLoading()
finishRefresh()
// 第一页使用预查好的本地数据合并;加载更多传 null 直接追加
val localList = if (pageNo == 1) pendingLocalList else null
loadAndMergeDishList(networkState.data, localList)
}
is UiState.Error -> {
// 餐次不匹配,忽略
if (dinnerType != pendingDinnerType) return@collect
if (isAdded.not()) return@collect
toast(networkState.msg)
finishRefresh()
activity.delayDismissLoading()
if (pageNo == 1) {
if (localList.isNullOrEmpty()) {
if (pendingLocalList.isNullOrEmpty()) {
loadEmptyView()
} else {
loadAndMergeDishList(null, localList)
loadAndMergeDishList(null, pendingLocalList)
}
}
}
@@ -217,6 +225,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
@SuppressLint("NotifyDataSetChanged")
@Suppress("unchecked_cast")
fun getDishList(foodName: String? = null) {
viewLifecycleOwner.lifecycleScope.launch {
val param = mutableMapOf<String, Any>(
"pageNum" to pageNo,
"pageSize" to pageSize,
@@ -225,8 +234,17 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
)
// foodName 不为空时按名称过滤,为 null 时查全部
foodName?.let { param["foodName"] = it }
if (pageNo == 1) {
// 先查本地数据库,完成后再发网络请求,确保合并时本地数据已就绪
pendingLocalList = activity.appViewModel.getCookFoodListDirect(
cookMode = 0,
dinnerType = dinnerType
)
}
// 记录本次请求的餐次,用于响应回来时过滤其他 Fragment 发起的请求
pendingDinnerType = dinnerType
activity.getFoodList(param = param)
if (pageNo == 1) activity.getCookFoodList()
}
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null