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:
@@ -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) {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user