fix(food): 修复并发竞态、双重刷新及搜索态丢失问题

- FoodListFragment:添加 currentJob 取消机制,防止快速切换时旧请求覆盖新数据
- FoodListFragment:缓存 currentFoodName,返回二级页面后 onResume 复用搜索词
- CookingModeActivity:RadioGroup+RadioButton 替换为 LinearLayout+CheckedTextView
- CookingModeActivity:新增 selectTab() 统一管理选中态,内置幂等保护
- CookingModeActivity:重构 onNewIntent,消除切换餐次时 getDishList 被双重调用的问题
- VectorCollectionFragment:Error/Success 消费后重置 StateFlow,防止 replay 导致 toast 重复弹出

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 12:09:21 +08:00
co-authored by Claude Sonnet 4.6
parent d7f9fb05fb
commit ef6a29ffb0
3 changed files with 50 additions and 33 deletions
@@ -97,17 +97,9 @@ class CookingModeActivity : BaseActivity() {
private var isClearingByTabSwitch = false
private fun addViewListener() {
binding.dishRadioGroup.setOnCheckedChangeListener { _, checkedId ->
dinnerType = when (checkedId) {
R.id.rbBreakfast -> "1"
R.id.rbLunch -> "2"
R.id.rbDinner -> "3"
else -> "1"
}
// 切换餐次时清空搜索框,避免用户误以为列表是被过滤的状态
clearSearchText()
loadFragment()
}
binding.tvBreakfast.setOnClickListener { selectTab("1") }
binding.tvLunch.setOnClickListener { selectTab("2") }
binding.tvDinner.setOnClickListener { selectTab("3") }
binding.etInputDish.addOnActionSearchListener {
jumpSearch()
}
@@ -122,6 +114,20 @@ class CookingModeActivity : BaseActivity() {
})
}
/**
* 切换餐次 Tab:更新选中态、清空搜索框、切换 Fragment
* 由点击事件调用,不触发额外的 getDishList(),由 onHiddenChanged 负责刷新
*/
private fun selectTab(type: String) {
if (dinnerType == type) return
dinnerType = type
binding.tvBreakfast.isChecked = type == "1"
binding.tvLunch.isChecked = type == "2"
binding.tvDinner.isChecked = type == "3"
clearSearchText()
loadFragment()
}
/** 获取当前显示的 FoodListFragment */
private fun currentFragment(): FoodListFragment? {
return runCatching { fragmentList[dinnerType.toInt() - 1] }.getOrNull()
@@ -169,16 +175,15 @@ class CookingModeActivity : BaseActivity() {
if (isConfigPage) {
return
}
//val isCooking = intent.getBooleanExtra(IS_COOKING, false)
val lastDinnerType = intent.getStringExtra(DINNER_TYPE) ?: "0"
if (lastDinnerType == "1" || lastDinnerType == "2" || lastDinnerType == "3") {
if (lastDinnerType !in listOf("1", "2", "3")) return
if (lastDinnerType == dinnerType) {
// 同一 TabonHiddenChanged 不会触发,直接刷新
clearSearchText()
fragmentList.getOrNull(lastDinnerType.toInt() - 1)?.getDishList(1, lastDinnerType)
}
when (lastDinnerType) {
"1" -> binding.dishRadioGroup.check(R.id.rbBreakfast)
"2" -> binding.dishRadioGroup.check(R.id.rbLunch)
"3" -> binding.dishRadioGroup.check(R.id.rbDinner)
currentFragment()?.getDishList(1, lastDinnerType)
} else {
// 不同 TabselectTab → loadFragment → show → onHiddenChanged 负责刷新,无需直接调用
selectTab(lastDinnerType)
}
}.onFailure {
it.printStackTrace()
@@ -30,6 +30,7 @@ import com.yanzhenjie.recyclerview.SwipeMenuItem
import androidx.fragment.app.viewModels
import kotlinx.coroutines.Dispatchers
import java.io.Serializable
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -55,6 +56,12 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
private var dinnerType = "0"
/** 跟踪当前请求协程,新请求发起前取消旧的,防止并发竞态导致数据错乱 */
private var currentJob: Job? = null
/** 缓存最后一次搜索词,返回二级页面后 onResume 可复用,避免搜索态被重置为全量查询 */
private var currentFoodName: String? = null
private lateinit var activity: CookingModeActivity
/** 第一页时先查数据库存入此字段,网络结果回来后直接使用,避免时序问题 */
@@ -140,7 +147,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
super.onResume()
runCatching {
pageNo = 1
getDishList()
getDishList(currentFoodName)
}.onFailure {
it.printStackTrace()
}
@@ -200,7 +207,9 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
@SuppressLint("NotifyDataSetChanged")
@Suppress("unchecked_cast")
fun getDishList(foodName: String? = null) {
viewLifecycleOwner.lifecycleScope.launch {
currentFoodName = foodName
currentJob?.cancel()
currentJob = viewLifecycleOwner.lifecycleScope.launch {
val param = mutableMapOf<String, Any>(
"pageNum" to pageNo,
"pageSize" to pageSize,
@@ -6,59 +6,62 @@
android:orientation="vertical"
tools:background="@drawable/bg_other_page">
<RadioGroup
android:id="@+id/dishRadioGroup"
<LinearLayout
android:id="@+id/llDishTabs"
android:layout_width="match_parent"
android:layout_height="88dp"
android:layout_marginTop="15dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbBreakfast"
<CheckedTextView
android:id="@+id/tvBreakfast"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="30dp"
android:layout_weight="1"
android:background="@drawable/breakfast_bg"
android:button="@null"
android:checkMark="@null"
android:checked="true"
android:gravity="center"
android:textAlignment="center"
android:text="@string/text_breakfast"
android:textColor="@color/breakfast_font"
android:textSize="30sp"
android:textStyle="bold" />
<RadioButton
android:id="@+id/rbLunch"
<CheckedTextView
android:id="@+id/tvLunch"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="9dp"
android:layout_weight="1"
android:background="@drawable/breakfast_bg"
android:button="@null"
android:checkMark="@null"
android:checked="false"
android:gravity="center"
android:textAlignment="center"
android:text="@string/text_lunch"
android:textColor="@color/breakfast_font"
android:textSize="30sp"
android:textStyle="bold" />
<RadioButton
android:id="@+id/rbDinner"
<CheckedTextView
android:id="@+id/tvDinner"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="9dp"
android:layout_marginEnd="30dp"
android:layout_weight="1"
android:background="@drawable/breakfast_bg"
android:button="@null"
android:checkMark="@null"
android:checked="false"
android:gravity="center"
android:textAlignment="center"
android:text="@string/text_dinner"
android:textColor="@color/breakfast_font"
android:textSize="30sp"
android:textStyle="bold" />
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/llSearchBar"