refactor(viewmodel): 将回调方式统一改为 StateFlow + UiState,消除竞态问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:01:24 +08:00
co-authored by Claude Sonnet 4.6
parent df0580d68f
commit 0243fe0dc8
8 changed files with 197 additions and 115 deletions
@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.collections.forEach
class DbViewModel : ViewModel() {
@@ -24,10 +23,12 @@ class DbViewModel : ViewModel() {
}
}
fun getCookFoodById(foodId: String, cookMode: Int, action: (CookFoodEntity?) -> Unit) {
private val _cookFoodDetailState = MutableStateFlow<CookFoodEntity?>(null)
val cookFoodDetailState: StateFlow<CookFoodEntity?> = _cookFoodDetailState.asStateFlow()
fun getCookFoodById(foodId: String, cookMode: Int) {
viewModelScope.launch {
val data = rep.getCookFoodById(foodId, cookMode)
action(data)
_cookFoodDetailState.value = rep.getCookFoodById(foodId, cookMode)
}
}
@@ -43,11 +44,8 @@ class DbViewModel : ViewModel() {
}
}
fun countCookFood(cookMode: Int, action: (Int) -> Unit) {
viewModelScope.launch {
val count = rep.countCookFood(cookMode)
action(count)
}
suspend fun countCookFood(cookMode: Int): Int {
return rep.countCookFood(cookMode)
}
// fun saveCookFood(cookMode: Int, entity: CookFoodEntity) {
@@ -141,11 +139,18 @@ class DbViewModel : ViewModel() {
// }
private val loadSeasoningMutex = Mutex()
fun loadSeasoning(action: (MutableList<SeasoningEntity>) -> Unit) {
/**
* 本地调料列表状态流
*/
private val _seasoningState = MutableStateFlow<MutableList<SeasoningEntity>>(mutableListOf())
val seasoningState: StateFlow<MutableList<SeasoningEntity>> = _seasoningState.asStateFlow()
fun loadSeasoning() {
if (loadSeasoningMutex.isLocked) return
viewModelScope.launch {
loadSeasoningMutex.withLock {
action(rep.getAllStream())
_seasoningState.value = rep.getAllStream()
}
}
}
@@ -172,21 +177,26 @@ class DbViewModel : ViewModel() {
}
}
fun clearAllSeasoning(action:()->Unit) {
private val _clearAllSeasoningState = MutableStateFlow(false)
val clearAllSeasoningState: StateFlow<Boolean> = _clearAllSeasoningState.asStateFlow()
fun clearAllSeasoning() {
viewModelScope.launch {
rep.clearAllSeasoning()
action()
_clearAllSeasoningState.value = true
}
}
private val _hasSeasoningSlotConfigState = MutableStateFlow(false)
val hasSeasoningSlotConfigState: StateFlow<Boolean> = _hasSeasoningSlotConfigState.asStateFlow()
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
* @param action 回调,true 表示已配置,false 表示未配置
*/
fun hasSeasoningSlotConfig(deviceId: String, action: (Boolean) -> Unit) {
fun hasSeasoningSlotConfig(deviceId: String) {
viewModelScope.launch {
action(rep.hasSeasoningSlotConfig(deviceId))
_hasSeasoningSlotConfigState.value = rep.hasSeasoningSlotConfig(deviceId)
}
}
@@ -8,6 +8,9 @@ import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -17,9 +20,12 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.DialogFoodSearchBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* 食材搜索弹窗,继承 BottomSheetDialog 确保只初始化一次
@@ -97,6 +103,8 @@ class FoodSearchDialog(
binding.ivSearch.setOnClickListener { searchGoods(it) }
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it.context, it) }
initObserver()
// 若有默认食材名称,自动填充并触发搜索
if (!defGoodsName.isNullOrBlank()) {
binding.etSheetInput.setText(defGoodsName)
@@ -104,6 +112,26 @@ class FoodSearchDialog(
}
}
/**
* 收集 goodsListState,统一处理 Loading / Success / Error 状态
*/
private fun initObserver() {
activity.lifecycleScope.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
activity.netViewModel.goodsListState.collect { state ->
when (state) {
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
}
else -> {}
}
}
}
}
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
@@ -130,12 +158,7 @@ class FoodSearchDialog(
if (!goodsName.isNullOrBlank()) {
param["goodsName"] = goodsName!!
}
activity.netViewModel.queryGoodsList(param = param, onSuccess = {
loadGoodsList(it)
}, onFailure = { _, msg ->
activity.toast(msg)
finishRefresh()
})
activity.netViewModel.queryGoodsList(param)
}
/**
@@ -18,6 +18,9 @@ import android.view.WindowManager
import androidx.core.graphics.toColorInt
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -27,6 +30,7 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.DialogSeasoningSearchBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
@@ -35,6 +39,8 @@ import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* 调料搜索弹窗,继承 BottomSheetDialog 确保只初始化一次
@@ -106,10 +112,10 @@ class SeasoningSearchDialog(
}
// RecyclerView 初始化及滑动冲突处理
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@SeasoningSearchDialog.adapter
addOnScrollListener(object : RecyclerView.OnScrollListener() {
binding.recyclerView.let {
it.layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
it.adapter = adapter
it.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(rv, dx, dy)
// 解决 RecyclerView 与 SmartRefreshLayout 滑动冲突
@@ -147,6 +153,8 @@ class SeasoningSearchDialog(
)
binding.tvWeight.text = getTextSpan(0.0)
initObserver()
// 若有默认调料名称,自动填充并触发搜索
if (!clickName.isNullOrBlank()) {
binding.etSheetInput.setText(clickName.trim())
@@ -155,6 +163,26 @@ class SeasoningSearchDialog(
}
}
/**
* 收集 seasoningListState,统一处理 Loading / Success / Error 状态
*/
private fun initObserver() {
activity.lifecycleScope.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
activity.netViewModel.seasoningListState.collect { state ->
when (state) {
is UiState.Success -> loadGoodsList(state.data)
is UiState.Error -> {
activity.toast(state.msg)
finishRefresh()
}
else -> {}
}
}
}
}
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
@@ -181,12 +209,7 @@ class SeasoningSearchDialog(
if (!goodsName.isNullOrBlank()) {
param["goodsName"] = goodsName!!
}
activity.netViewModel.querySeasoningList(param = param, onSuccess = {
loadGoodsList(it)
}, onFailure = { _, msg ->
activity.toast(msg)
finishRefresh()
})
activity.netViewModel.querySeasoningList(param)
}
/**
@@ -35,16 +35,19 @@ class NetViewModel(
}
}
/**
* 查询菜品详情的 UI 状态流,UI 层通过 collect 监听
*/
private val _foodDetailState = MutableStateFlow<UiState<CookFoodEntity?>>(UiState.Idle)
val foodDetailState: StateFlow<UiState<CookFoodEntity?>> = _foodDetailState.asStateFlow()
/**
* 查询菜品详情
*/
fun getFoodDetail(
foodId: String,
onSuccess: (CookFoodEntity?) -> Unit,
onFailure: (String, String) -> Unit
) {
fun getFoodDetail(foodId: String) {
viewModelScope.launch {
repository.getFoodDetail(foodId, onSuccess, onFailure)
_foodDetailState.value = UiState.Loading
_foodDetailState.value = repository.getFoodDetail(foodId)
}
}
@@ -80,29 +83,35 @@ class NetViewModel(
}
}
/**
* 查询物品信息列表的 UI 状态流,UI 层通过 collect 监听
*/
private val _goodsListState = MutableStateFlow<UiState<MutableList<CookFoodGoodsEntity>?>>(UiState.Idle)
val goodsListState: StateFlow<UiState<MutableList<CookFoodGoodsEntity>?>> = _goodsListState.asStateFlow()
/**
* 查询物品信息列表
*/
fun queryGoodsList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
fun queryGoodsList(param: MutableMap<String, Any>) {
viewModelScope.launch {
repository.queryGoodsList(param, onSuccess, onFailure)
_goodsListState.value = UiState.Loading
_goodsListState.value = repository.queryGoodsList(param)
}
}
/**
* 查询调料信息列表的 UI 状态流,UI 层通过 collect 监听
*/
private val _seasoningListState = MutableStateFlow<UiState<MutableList<SeasoningEntity>?>>(UiState.Idle)
val seasoningListState: StateFlow<UiState<MutableList<SeasoningEntity>?>> = _seasoningListState.asStateFlow()
/**
* 查询调料信息列表
*/
fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
fun querySeasoningList(param: MutableMap<String, Any>) {
viewModelScope.launch {
repository.querySeasoningList(param, onSuccess, onFailure)
_seasoningListState.value = UiState.Loading
_seasoningListState.value = repository.querySeasoningList(param)
}
}
}
@@ -30,18 +30,18 @@ class RemoteRepository {
/**
* 查询菜品详情
* @param foodId 菜品 ID
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
* @return UiState 包装的结果,Success 携带详情数据,Error 携带错误信息
*/
suspend fun getFoodDetail(
foodId: String,
onSuccess: (CookFoodEntity?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.getFoodDetail(foodId = foodId) },
onSuccess = onSuccess,
onFailure = onFailure
)
suspend fun getFoodDetail(foodId: String): UiState<CookFoodEntity?> {
return try {
val resp = apiService.getFoodDetail(foodId = foodId)
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
val ex = getApiException(e)
UiState.Error("-1", ex.errorMsg)
}
}
/**
* 搜索菜品列表
@@ -78,32 +78,34 @@ class RemoteRepository {
/**
* 查询物品信息列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
* @return UiState 包装的结果,Success 携带列表数据,Error 携带错误信息
*/
suspend fun queryGoodsList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.queryGoodsList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
suspend fun queryGoodsList(param: MutableMap<String, Any>): UiState<MutableList<CookFoodGoodsEntity>?> {
return try {
val resp = apiService.queryGoodsList(param = param)
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
val ex = getApiException(e)
UiState.Error("-1", ex.errorMsg)
}
}
/**
* 查询调料信息列表
* @param param 查询参数
* @param onSuccess 成功回调
* @param onFailure 失败回调,参数为 (errorCode, errorMsg)
* @return UiState 包装的结果,Success 携带列表数据,Error 携带错误信息
*/
suspend fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) = request(
onRequest = { apiService.querySeasoningList(param = param) },
onSuccess = onSuccess,
onFailure = onFailure
)
suspend fun querySeasoningList(param: MutableMap<String, Any>): UiState<MutableList<SeasoningEntity>?> {
return try {
val resp = apiService.querySeasoningList(param = param)
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
val ex = getApiException(e)
UiState.Error("-1", ex.errorMsg)
}
}
}
@@ -142,19 +142,18 @@ class HomeActivity : BaseActivity() {
finish()
return@launch
}
appViewModel.countCookFood(cookMode = 1) { count ->
if (count > 0) {
// 有烹饪中数据,打开采样历史列表页面
startActivity<SamplingListActivity>()
finish()
return@countCookFood
}
// 无烹饪数据,打开新增采样页面
startActivity<PrepareCookActivity> {
putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
}
val count = appViewModel.countCookFood(cookMode = 1)
if (count > 0) {
// 有烹饪中数据,打开采样历史列表页面
startActivity<SamplingListActivity>()
finish()
return@launch
}
// 无烹饪数据,打开新增采样页面
startActivity<PrepareCookActivity> {
putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
}
finish()
}
}
@@ -95,16 +95,15 @@ class InitActivity : BaseActivity() {
startActivity<SamplingListActivity>()
return@launch
}
appViewModel.countCookFood(cookMode = 1) { count ->
if (count > 0) {
// 有烹饪中数据,打开采样历史列表页面
startActivity<SamplingListActivity>()
return@countCookFood
}
// 无烹饪数据,打开新增采样页面
startActivity<PrepareCookActivity> {
putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
}
val count = appViewModel.countCookFood(cookMode = 1)
if (count > 0) {
// 有烹饪中数据,打开采样历史列表页面
startActivity<SamplingListActivity>()
return@launch
}
// 无烹饪数据,打开新增采样页面
startActivity<PrepareCookActivity> {
putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME)
}
}
}
@@ -8,7 +8,9 @@ import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
@@ -20,6 +22,7 @@ import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils
@@ -36,6 +39,7 @@ import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable
@@ -110,6 +114,7 @@ class PrepareCookActivity : BaseActivity() {
addBackKeyListener()
initCamera(binding.flCameraContainer)
initRecyclerView()
initObserver()
if (!isSamplingMode) {
getDishDetail()
@@ -267,6 +272,28 @@ class PrepareCookActivity : BaseActivity() {
}
private fun initObserver() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.foodDetailState.collect { state ->
when (state) {
is UiState.Success -> {
val detail = state.data
if (detail == null) {
toast("查询菜品信息为空")
return@collect
}
loadDishDetail(detail)
}
is UiState.Error -> toast(state.msg)
else -> {}
}
}
}
}
}
private fun getDishDetail() {
// //649
// val url = "${UrlConfig.DISH_DETAIL}?foodId=${food?.foodId}"
@@ -285,17 +312,7 @@ class PrepareCookActivity : BaseActivity() {
// toast(msg)
// }
netViewModel.getFoodDetail(
food?.foodId ?: "",
onSuccess = { detail ->
if (detail == null) {
toast("查询菜品信息为空")
return@getFoodDetail
}
loadDishDetail(detail)
}, onFailure = { code, msg ->
toast(msg)
})
netViewModel.getFoodDetail(food?.foodId ?: "")
}