From 0243fe0dc805576b143361a44edddc36e8ae6727 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Thu, 23 Apr 2026 14:01:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(viewmodel):=20=E5=B0=86=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E6=96=B9=E5=BC=8F=E7=BB=9F=E4=B8=80=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20StateFlow=20+=20UiState=EF=BC=8C=E6=B6=88=E9=99=A4=E7=AB=9E?= =?UTF-8?q?=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../com/shuwei/dish/match/db/DbViewModel.kt | 42 +++++++----- .../dish/match/dialog/FoodSearchDialog.kt | 35 ++++++++-- .../match/dialog/SeasoningSearchDialog.kt | 43 +++++++++--- .../com/shuwei/dish/match/net/NetViewModel.kt | 45 +++++++----- .../shuwei/dish/match/net/RemoteRepository.kt | 68 ++++++++++--------- .../com/shuwei/dish/match/ui/HomeActivity.kt | 21 +++--- .../com/shuwei/dish/match/ui/InitActivity.kt | 19 +++--- .../dish/match/ui/PrepareCookActivity.kt | 39 ++++++++--- 8 files changed, 197 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt index 8cbbbba..68f87f4 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt @@ -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(null) + val cookFoodDetailState: StateFlow = _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) -> Unit) { + + /** + * 本地调料列表状态流 + */ + private val _seasoningState = MutableStateFlow>(mutableListOf()) + val seasoningState: StateFlow> = _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 = _clearAllSeasoningState.asStateFlow() + + fun clearAllSeasoning() { viewModelScope.launch { rep.clearAllSeasoning() - action() + _clearAllSeasoningState.value = true } } + private val _hasSeasoningSlotConfigState = MutableStateFlow(false) + val hasSeasoningSlotConfigState: StateFlow = _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) } } diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt index 4b16567..f42b31f 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt @@ -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) } /** diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt index b4a3174..eb2d76c 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt @@ -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) } /** 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 19741b0..f264d13 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 @@ -35,16 +35,19 @@ class NetViewModel( } } + /** + * 查询菜品详情的 UI 状态流,UI 层通过 collect 监听 + */ + private val _foodDetailState = MutableStateFlow>(UiState.Idle) + val foodDetailState: StateFlow> = _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.Idle) + val goodsListState: StateFlow?>> = _goodsListState.asStateFlow() + /** * 查询物品信息列表 */ - fun queryGoodsList( - param: MutableMap, - onSuccess: (MutableList?) -> Unit, - onFailure: (String, String) -> Unit - ) { + fun queryGoodsList(param: MutableMap) { viewModelScope.launch { - repository.queryGoodsList(param, onSuccess, onFailure) + _goodsListState.value = UiState.Loading + _goodsListState.value = repository.queryGoodsList(param) } } + /** + * 查询调料信息列表的 UI 状态流,UI 层通过 collect 监听 + */ + private val _seasoningListState = MutableStateFlow?>>(UiState.Idle) + val seasoningListState: StateFlow?>> = _seasoningListState.asStateFlow() + /** * 查询调料信息列表 */ - fun querySeasoningList( - param: MutableMap, - onSuccess: (MutableList?) -> Unit, - onFailure: (String, String) -> Unit - ) { + fun querySeasoningList(param: MutableMap) { viewModelScope.launch { - repository.querySeasoningList(param, onSuccess, onFailure) + _seasoningListState.value = UiState.Loading + _seasoningListState.value = repository.querySeasoningList(param) } } } diff --git a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt index eb149b0..e844311 100644 --- a/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt +++ b/app/src/main/java/com/shuwei/dish/match/net/RemoteRepository.kt @@ -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 { + 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, - onSuccess: (MutableList?) -> Unit, - onFailure: (String, String) -> Unit - ) = request( - onRequest = { apiService.queryGoodsList(param = param) }, - onSuccess = onSuccess, - onFailure = onFailure - ) + suspend fun queryGoodsList(param: MutableMap): UiState?> { + 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, - onSuccess: (MutableList?) -> Unit, - onFailure: (String, String) -> Unit - ) = request( - onRequest = { apiService.querySeasoningList(param = param) }, - onSuccess = onSuccess, - onFailure = onFailure - ) + suspend fun querySeasoningList(param: MutableMap): UiState?> { + 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) + } + } + } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/HomeActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/HomeActivity.kt index e054496..1e98f32 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/HomeActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/HomeActivity.kt @@ -142,19 +142,18 @@ class HomeActivity : BaseActivity() { finish() return@launch } - appViewModel.countCookFood(cookMode = 1) { count -> - if (count > 0) { - // 有烹饪中数据,打开采样历史列表页面 - startActivity() - finish() - return@countCookFood - } - // 无烹饪数据,打开新增采样页面 - startActivity { - putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME) - } + val count = appViewModel.countCookFood(cookMode = 1) + if (count > 0) { + // 有烹饪中数据,打开采样历史列表页面 + startActivity() finish() + return@launch } + // 无烹饪数据,打开新增采样页面 + startActivity { + putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME) + } + finish() } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt index 5a25cfc..2de962b 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/InitActivity.kt @@ -95,16 +95,15 @@ class InitActivity : BaseActivity() { startActivity() return@launch } - appViewModel.countCookFood(cookMode = 1) { count -> - if (count > 0) { - // 有烹饪中数据,打开采样历史列表页面 - startActivity() - return@countCookFood - } - // 无烹饪数据,打开新增采样页面 - startActivity { - putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME) - } + val count = appViewModel.countCookFood(cookMode = 1) + if (count > 0) { + // 有烹饪中数据,打开采样历史列表页面 + startActivity() + return@launch + } + // 无烹饪数据,打开新增采样页面 + startActivity { + putExtra(PrepareCookActivity.PAGE_FROM, PrepareCookActivity.HOME) } } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt index 071efcf..c1954fd 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt @@ -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 ?: "") }