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.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.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.GoodsItem
import com.shuwei.dish.match.entity.GoodsNameQueryDTO import com.shuwei.dish.match.entity.GoodsNameQueryDTO
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody 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) private val _searchFoodState = MutableSharedFlow<UiState<MutableList<FoodRecord>?>>(
val searchFoodState: StateFlow<UiState<MutableList<FoodRecord>?>> = _searchFoodState.asStateFlow() replay = 1,
extraBufferCapacity = 1
)
val searchFoodState: SharedFlow<UiState<MutableList<FoodRecord>?>> = _searchFoodState.asSharedFlow()
/** /**
* 搜索菜品列表 * 搜索菜品列表
*/ */
fun searchFoodList(param: MutableMap<String, Any>) { fun searchFoodList(param: MutableMap<String, Any>) {
viewModelScope.launch { viewModelScope.launch {
_searchFoodState.value = UiState.Loading // pageNum=1 时先 emit Idle 重置 replay 缓存,防止相同数据导致 SharedFlow 去重不触发
_searchFoodState.value = repository.searchFoodList(param) 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) private val _samplingListState = MutableStateFlow<UiState<MutableList<FoodRecord>?>>(UiState.Idle)
val samplingListState: StateFlow<UiState<MutableList<FoodRecord>?>> = _samplingListState.asStateFlow() val samplingListState: StateFlow<UiState<MutableList<FoodRecord>?>> = _samplingListState.asStateFlow()
@@ -141,16 +141,17 @@ class CookingModeActivity : BaseActivity() {
private fun loadFragment() { private fun loadFragment() {
runCatching { runCatching {
fragmentList[dinnerType.toInt() - 1].let { val target = fragmentList[dinnerType.toInt() - 1]
supportFragmentManager.beginTransaction().apply { supportFragmentManager.beginTransaction().apply {
if (fragmentList.contains(it)) { fragmentList.forEach { fragment ->
replace(R.id.flSubPage, it) if (fragment.isAdded) {
if (fragment == target) show(fragment) else hide(fragment)
} else { } else {
add(R.id.flSubPage, it) if (fragment == target) add(R.id.flSubPage, fragment)
}
} }
commit() commit()
} }
}
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
} }
@@ -3,12 +3,11 @@ package com.shuwei.dish.match.ui.fragment
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodListAdapter import com.shuwei.dish.match.adapter.FoodListAdapter
@@ -132,7 +131,6 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
adapter = dishAdapter adapter = dishAdapter
} }
addViewListener() addViewListener()
initObserver()
pageNo = 1 pageNo = 1
activity.showLoading() 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() { private fun addViewListener() {
binding.refreshLayout.run { binding.refreshLayout.run {
setEnableRefresh(true) setEnableRefresh(true)
setEnableLoadMore(false) setEnableLoadMore(false)
setOnRefreshListener { setOnRefreshListener {
Log.d("FoodList", "下拉刷新触发, pageNo reset to 1")
pageNo = 1 pageNo = 1
getDishList() getDishList()
} }
@@ -223,16 +190,37 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
"placeId" to BaseApp.canteenId, "placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText() "dinnerType" to getDinnerTypeText()
) )
// foodName 不为空时按名称过滤,为 null 时查全部
foodName?.let { param["foodName"] = it } foodName?.let { param["foodName"] = it }
if (pageNo == 1) { if (pageNo == 1) {
// 先查本地数据库,完成后再发网络请求,确保合并时本地数据已就绪
pendingLocalList = activity.appViewModel.getCookFoodListDirect( pendingLocalList = activity.appViewModel.getCookFoodListDirect(
cookMode = 0, cookMode = 0,
dinnerType = dinnerType 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 val effectiveLocalList = if (pageNo == 1) localList else null
if (records.isNullOrEmpty() && effectiveLocalList.isNullOrEmpty()) { if (records.isNullOrEmpty() && effectiveLocalList.isNullOrEmpty()) {
if (pageNo == 1) loadEmptyView() if (pageNo == 1) loadEmptyView()
finishRefresh()
binding.refreshLayout.setEnableLoadMore(false)
return return
} }
if (pageNo == 1) list.clear() if (pageNo == 1) list.clear()
@@ -327,6 +317,7 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
list.addAll(mergedList) list.addAll(mergedList)
dishAdapter.notifyDataSetChanged() dishAdapter.notifyDataSetChanged()
val isLoadMoreEnable = (records?.size ?: 0) >= pageSize val isLoadMoreEnable = (records?.size ?: 0) >= pageSize
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable) binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++ if (isLoadMoreEnable) pageNo++
} catch (e: Exception) { } catch (e: Exception) {
+2
View File
@@ -16,6 +16,7 @@ junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
appcompat = "1.6.1" appcompat = "1.6.1"
kotlinGradlePlugin = "2.0.21" kotlinGradlePlugin = "2.0.21"
fragmentKtx = "1.8.6"
lifecycleRuntimeKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7"
lifecycleViewmodelKtx = "2.8.7" lifecycleViewmodelKtx = "2.8.7"
loggingInterceptor = "4.9.1" loggingInterceptor = "4.9.1"
@@ -36,6 +37,7 @@ flexbox = "3.0.0"
[libraries] [libraries]
android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" } android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } 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-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-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }