refactor(fragment): 改用回调方式查询菜品列表,修复刷新卡死问题

- FoodListFragment 改用 searchFoodListWithCallback 替代 SharedFlow collect,
  彻底解决下拉/上拉因数据相同导致不触发的问题
- NetViewModel 新增 searchFoodListWithCallback 回调版本
- CookingModeActivity loadFragment 改为 show/hide 方式,切换餐次不重建 Fragment
- 修复数据为空时未调用 finishRefresh 导致加载动画卡死的问题
- 新增 fragment-ktx 依赖,支持 by viewModels()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 17:54:59 +08:00
co-authored by Claude Sonnet 4.6
parent 120f5432de
commit 535da33aab
5 changed files with 71 additions and 50 deletions
+1
View File
@@ -98,6 +98,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
@@ -8,8 +8,11 @@ import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.GoodsItem
import com.shuwei.dish.match.entity.GoodsNameQueryDTO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@@ -62,23 +65,46 @@ class NetViewModel(
}
/**
* 搜索菜品列表的 UI 状态流UI 层通过 collect 监听
* 搜索菜品列表的 UI 状态流
* 使用 replay=1 保证 Fragment 重建后能收到最近一次结果;
* emit 时包一层 Event wrapper 打破 equals 去重,确保相同数据也能触发 collect。
*/
private val _searchFoodState = MutableStateFlow<UiState<MutableList<FoodRecord>?>>(UiState.Idle)
val searchFoodState: StateFlow<UiState<MutableList<FoodRecord>?>> = _searchFoodState.asStateFlow()
private val _searchFoodState = MutableSharedFlow<UiState<MutableList<FoodRecord>?>>(
replay = 1,
extraBufferCapacity = 1
)
val searchFoodState: SharedFlow<UiState<MutableList<FoodRecord>?>> = _searchFoodState.asSharedFlow()
/**
* 搜索菜品列表
*/
fun searchFoodList(param: MutableMap<String, Any>) {
viewModelScope.launch {
_searchFoodState.value = UiState.Loading
_searchFoodState.value = repository.searchFoodList(param)
// pageNum=1 时先 emit Idle 重置 replay 缓存,防止相同数据导致 SharedFlow 去重不触发
if (param["pageNum"] == 1) {
_searchFoodState.emit(UiState.Idle)
}
_searchFoodState.emit(UiState.Loading)
_searchFoodState.emit(repository.searchFoodList(param))
}
}
/**
* 搜索菜品列表(回调版本),结果通过 onResult 直接回调,不经过 StateFlow/SharedFlow
* 适合需要每次都能触发的场景(下拉刷新、上拉加载更多)
*/
fun searchFoodListWithCallback(
param: MutableMap<String, Any>,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<FoodRecord>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.searchFoodList(param))
}
}
/**
* 查询采样数据列表的 UI 状态流,UI 层通过 collect 监听
*/
private val _samplingListState = MutableStateFlow<UiState<MutableList<FoodRecord>?>>(UiState.Idle)
val samplingListState: StateFlow<UiState<MutableList<FoodRecord>?>> = _samplingListState.asStateFlow()
@@ -141,15 +141,16 @@ class CookingModeActivity : BaseActivity() {
private fun loadFragment() {
runCatching {
fragmentList[dinnerType.toInt() - 1].let {
supportFragmentManager.beginTransaction().apply {
if (fragmentList.contains(it)) {
replace(R.id.flSubPage, it)
val target = fragmentList[dinnerType.toInt() - 1]
supportFragmentManager.beginTransaction().apply {
fragmentList.forEach { fragment ->
if (fragment.isAdded) {
if (fragment == target) show(fragment) else hide(fragment)
} else {
add(R.id.flSubPage, it)
if (fragment == target) add(R.id.flSubPage, fragment)
}
commit()
}
commit()
}
}.onFailure {
it.printStackTrace()
@@ -3,12 +3,11 @@ package com.shuwei.dish.match.ui.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodListAdapter
@@ -132,7 +131,6 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
adapter = dishAdapter
}
addViewListener()
initObserver()
pageNo = 1
activity.showLoading()
@@ -152,43 +150,12 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
}
}
private fun initObserver() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.searchFoodState.collect { networkState ->
when (networkState) {
is UiState.Loading -> activity.showLoading()
is UiState.Success -> {
activity.delayDismissLoading()
finishRefresh()
val localList = if (pageNo == 1) pendingLocalList else null
loadAndMergeDishList(networkState.data, localList)
}
is UiState.Error -> {
if (isAdded.not()) return@collect
toast(networkState.msg)
finishRefresh()
activity.delayDismissLoading()
if (pageNo == 1) {
if (pendingLocalList.isNullOrEmpty()) {
loadEmptyView()
} else {
loadAndMergeDishList(null, pendingLocalList)
}
}
}
is UiState.Idle -> {}
}
}
}
}
}
private fun addViewListener() {
binding.refreshLayout.run {
setEnableRefresh(true)
setEnableLoadMore(false)
setOnRefreshListener {
Log.d("FoodList", "下拉刷新触发, pageNo reset to 1")
pageNo = 1
getDishList()
}
@@ -223,16 +190,37 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
"placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText()
)
// foodName 不为空时按名称过滤,为 null 时查全部
foodName?.let { param["foodName"] = it }
if (pageNo == 1) {
// 先查本地数据库,完成后再发网络请求,确保合并时本地数据已就绪
pendingLocalList = activity.appViewModel.getCookFoodListDirect(
cookMode = 0,
dinnerType = dinnerType
)
}
netViewModel.searchFoodList(param = param)
val localList = if (pageNo == 1) pendingLocalList else null
netViewModel.searchFoodListWithCallback(
param = param,
onLoading = { activity.showLoading() }
) { result ->
when (result) {
is UiState.Success -> {
activity.delayDismissLoading()
finishRefresh()
loadAndMergeDishList(result.data, localList)
}
is UiState.Error -> {
if (isAdded.not()) return@searchFoodListWithCallback
toast(result.msg)
finishRefresh()
activity.delayDismissLoading()
if (pageNo == 1) {
if (localList.isNullOrEmpty()) loadEmptyView()
else loadAndMergeDishList(null, localList)
}
}
else -> {}
}
}
}
}
@@ -288,6 +276,8 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
val effectiveLocalList = if (pageNo == 1) localList else null
if (records.isNullOrEmpty() && effectiveLocalList.isNullOrEmpty()) {
if (pageNo == 1) loadEmptyView()
finishRefresh()
binding.refreshLayout.setEnableLoadMore(false)
return
}
if (pageNo == 1) list.clear()
@@ -327,6 +317,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
list.addAll(mergedList)
dishAdapter.notifyDataSetChanged()
val isLoadMoreEnable = (records?.size ?: 0) >= pageSize
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
} catch (e: Exception) {
+2
View File
@@ -16,6 +16,7 @@ junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
kotlinGradlePlugin = "2.0.21"
fragmentKtx = "1.8.6"
lifecycleRuntimeKtx = "2.8.7"
lifecycleViewmodelKtx = "2.8.7"
loggingInterceptor = "4.9.1"
@@ -36,6 +37,7 @@ flexbox = "3.0.0"
[libraries]
android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }