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.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,15 +141,16 @@ 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) {
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user