diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 002a5b1..bd56bc8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt index f5b32c0..5e86893 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/NetViewModel.kt @@ -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.Idle) - val searchFoodState: StateFlow?>> = _searchFoodState.asStateFlow() + private val _searchFoodState = MutableSharedFlow?>>( + replay = 1, + extraBufferCapacity = 1 + ) + val searchFoodState: SharedFlow?>> = _searchFoodState.asSharedFlow() /** * 搜索菜品列表 */ fun searchFoodList(param: MutableMap) { 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, + onLoading: () -> Unit = {}, + onResult: (UiState?>) -> Unit + ) { + viewModelScope.launch { + onLoading() + onResult(repository.searchFoodList(param)) } } /** - * 查询采样数据列表的 UI 状态流,UI 层通过 collect 监听 */ private val _samplingListState = MutableStateFlow?>>(UiState.Idle) val samplingListState: StateFlow?>> = _samplingListState.asStateFlow() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/CookingModeActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/CookingModeActivity.kt index 4ce11a3..b4e4649 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/CookingModeActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/CookingModeActivity.kt @@ -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() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt index 6a92f22..9b15bac 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt @@ -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() { adapter = dishAdapter } addViewListener() - initObserver() pageNo = 1 activity.showLoading() @@ -152,43 +150,12 @@ class FoodListFragment : BaseFragment() { } } - 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() { "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() { 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() { 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) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1409e3..6b0f055 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }