refactor(viewmodel): 封装调料槽位读写至 AppViewModel,消除页面直接调用 DAO 的情况

This commit is contained in:
2026-04-23 10:54:59 +08:00
parent 441cffe4da
commit debc7a0740
11 changed files with 220 additions and 155 deletions
@@ -26,12 +26,14 @@ import com.shuwei.dish.match.databinding.ListTypeScale18Binding
import com.shuwei.dish.match.databinding.ListTypeScale2Binding
import com.shuwei.dish.match.databinding.ListTypeScale22Binding
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.db.AppRepository
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
@@ -53,6 +55,8 @@ class MasterScaleActivity : BaseActivity() {
private lateinit var binding: ActivityMasterScaleBinding
private val size by lazy { SizeTool.getFlexLayoutSize(10.dp) }
private lateinit var appViewModel: AppViewModel
private data class DeviceGroup(
val deviceId: String,
val ip: String,
@@ -76,19 +80,19 @@ class MasterScaleActivity : BaseActivity() {
binding.rvScaleList.itemAnimator = null
binding.rvScaleList.adapter = adapter
val db = BaseApp.instance!!.database
appViewModel = ViewModelProvider(this, AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao())))[AppViewModel::class.java]
loadSlotNames()
}
/** 从 Room 加载槽位配置到 slotNameMap,加载完成后启动数据观测 */
private fun loadSlotNames() {
lifecycleScope.launch(Dispatchers.IO) {
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
withContext(Dispatchers.Main) {
slots.forEach { slot ->
slotNameMap["${slot.deviceId}#${slot.address}"] = slot.goodsName
}
observeScaleData()
lifecycleScope.launch {
val slots = appViewModel.loadSeasoningSlot()
slots.forEach { slot ->
slotNameMap["${slot.deviceId}#${slot.address}"] = slot.goodsName
}
observeScaleData()
}
}
@@ -141,7 +141,10 @@ class SamplingListActivity : BaseActivity() {
}
fun deleteCookFoodAndGoods(foodId: String, action: () -> Unit) {
appViewModel.deleteCookFoodAndGoods(cookMode = 1, foodId = foodId, onFinish = action)
lifecycleScope.launch {
appViewModel.deleteCookFoodAndGoods(cookMode = 1, foodId = foodId)
action()
}
}
override fun onNewIntent(intent: Intent) {
@@ -4,6 +4,8 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.addCallback
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
@@ -152,7 +154,10 @@ class SelectDishActivity : BaseActivity() {
}
fun deleteCookFoodAndGoods(foodId: String, action: () -> Unit) {
appViewModel.deleteCookFoodAndGoods(cookMode = 0, foodId = foodId, onFinish = action)
lifecycleScope.launch {
appViewModel.deleteCookFoodAndGoods(cookMode = 0, foodId = foodId)
action()
}
}
override fun onNewIntent(intent: Intent) {
@@ -4,12 +4,17 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.ui.fragment.CollectFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.ui.fragment.SeasoningConfigFragment
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
/**
* 通用单 Fragment 容器 Activity
@@ -58,8 +63,15 @@ class SingleFragmentActivity : BaseActivity() {
lifecycleCallback = callback
}
lateinit var appViewModel: AppViewModel
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = BaseApp.instance!!.database
appViewModel = ViewModelProvider(this, AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao())))[AppViewModel::class.java]
setHeaderBackground()
// 解析页面类型,非法值直接关闭
@@ -30,9 +30,11 @@ import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
/**
* 子设备专属页面(小屏幕设备使用)
@@ -64,12 +66,17 @@ class SlaveActivity : BaseActivity() {
private var clearHighlightRunnable: Runnable? = null
private val size by lazy { SizeTool.getFlexLayoutSize(10.dp) }
private lateinit var appViewModel: AppViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySlaveBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
val db = BaseApp.instance!!.database
appViewModel = ViewModelProvider(this, AppFactory(AppRepository(db.appDao(), db.seasoningSlotDao())))[AppViewModel::class.java]
binding.tvDeviceId.text = "设备:${GlobalData.deviceId}"
binding.tvDeviceIp.text = "IP${getLocalIpAddress()}"
@@ -152,10 +159,9 @@ class SlaveActivity : BaseActivity() {
* 主设备未开机时也能正常展示上次配置
*/
private fun loadSlotsFromDb() {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
val slots = dao.queryByDeviceId(GlobalData.deviceId)
withContext(Dispatchers.Main) { applySlots(slots) }
lifecycleScope.launch {
val slots = appViewModel.getSeasoningSlotsByDeviceId(GlobalData.deviceId)
applySlots(slots)
}
}
@@ -169,9 +175,9 @@ class SlaveActivity : BaseActivity() {
?.filter { it.deviceId == GlobalData.deviceId }
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
?: return@onSeasoningConfig
lifecycleScope.launch(Dispatchers.IO) {
BaseApp.instance!!.database.seasoningSlotDao().upsertAll(mySlots)
withContext(Dispatchers.Main) { applySlots(mySlots) }
lifecycleScope.launch {
appViewModel.upsertAllSeasoningSlots(mySlots)
applySlots(mySlots)
}
}
}
@@ -5,7 +5,9 @@ import android.os.Bundle
import android.util.Log
import androidx.activity.addCallback
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import com.google.gson.reflect.TypeToken
import com.shuwei.dish.match.R
@@ -19,6 +21,7 @@ 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.NetViewModel
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AddressUtil
@@ -121,12 +124,33 @@ class SubmitFoodActivity : BaseActivity() {
})
setupRecyclerView()
addViewListener()
initObserver()
loadSlotsThenObserveScales()
onBackPressedDispatcher.addCallback(this) {
remindSaveDataDialog()
}
}
/**
* 收集 submitCookFoodState,处理提交成功/失败
*/
private fun initObserver() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.submitCookFoodState.collect { state ->
when (state) {
is UiState.Success -> submitSuccess(food?.cookMode == 1)
is UiState.Error -> {
toast(state.msg)
dismissLoading()
}
else -> {}
}
}
}
}
}
/** 初始化 RecyclerView2列 GridLayoutManager */
private fun setupRecyclerView() {
binding.rvSeasoning.itemAnimator = null
@@ -139,35 +163,32 @@ class SubmitFoodActivity : BaseActivity() {
* isCooking=true 时先从数据库查询 goodsList,筛选调料预填充 adapter,再开始观测
*/
private fun loadSlotsThenObserveScales() {
lifecycleScope.launch(Dispatchers.IO) {
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
withContext(Dispatchers.Main) {
slots.forEach { slot ->
slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName
}
if (food?.isCooking == true) {
// 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter
val foodId = food?.foodId ?: return@withContext
val cookMode = food?.cookMode ?: return@withContext
appViewModel.getCookFoodGoodsList(foodId, cookMode) { list ->
goodsList = list
list.filter { it.materialType == 3 }.forEach { item ->
val name = item.goodsName ?: ""
val weight = item.useWeight ?: 0.0
baseSeasoningMap[name] = (item.goodsId) to weight
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = item.goodsId,
goodsName = name,
useWeight = weight
)
)
}
observeScaleData()
}
} else {
observeScaleData()
lifecycleScope.launch {
val slots = appViewModel.loadSeasoningSlot()
slots.forEach { slot ->
slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName
}
if (food?.isCooking == true) {
// 制作中:从数据库查询 goodsList,筛选调料数据预填充 adapter
val foodId = food?.foodId ?: return@launch
val cookMode = food?.cookMode ?: return@launch
val list = appViewModel.getCookFoodGoodsList(foodId, cookMode)
goodsList = list
list.filter { it.materialType == 3 }.forEach { item ->
val name = item.goodsName ?: ""
val weight = item.useWeight ?: 0.0
baseSeasoningMap[name] = (item.goodsId) to weight
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
goodsId = item.goodsId,
goodsName = name,
useWeight = weight
)
)
}
observeScaleData()
} else {
observeScaleData()
}
}
}
@@ -305,16 +326,15 @@ class SubmitFoodActivity : BaseActivity() {
}
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode,
entity = cookFoodEntity,
list = goodsList,
onFinish = {
runOnUiThread {
dismissLoading()
jumpPage(isSamplingData, true)
}
})
lifecycleScope.launch {
appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode,
entity = cookFoodEntity,
list = goodsList
)
dismissLoading()
jumpPage(isSamplingData, true)
}
}
private fun submit() {
@@ -354,13 +374,7 @@ class SubmitFoodActivity : BaseActivity() {
}
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
netViewModel.submitCookFood(
entity = cookFoodEntity,
onSuccess = { submitSuccess(isSamplingData) },
onFailure = { _, msg ->
toast(msg)
dismissLoading()
})
netViewModel.submitCookFood(entity = cookFoodEntity)
}
private fun submitSuccess(isSamplingData: Boolean) {
@@ -369,10 +383,11 @@ class SubmitFoodActivity : BaseActivity() {
jumpPage(isSamplingData, false)
return
}
appViewModel.deleteCookFoodAndGoods(
cookMode = food!!.cookMode,
foodId = food!!.foodId!!
) {
lifecycleScope.launch {
appViewModel.deleteCookFoodAndGoods(
cookMode = food!!.cookMode,
foodId = food!!.foodId!!
)
dismissLoading()
jumpPage(isSamplingData, false)
}
@@ -13,7 +13,6 @@ import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
@@ -29,9 +28,7 @@ import com.shuwei.dish.match.ui.SingleFragmentActivity
import com.shuwei.dish.match.utils.SizeTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
@@ -149,11 +146,10 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
* 仅主设备执行广播,子设备不调用此方法
*/
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
dao.upsert(slot)
lifecycleScope.launch {
currentActivity.appViewModel.upsertSeasoningSlot(slot)
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
val allSlots = dao.queryAll().map {
val allSlots = currentActivity.appViewModel.loadSeasoningSlot().map {
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
}
ScaleServiceManager.sendSeasoningConfig(allSlots)
@@ -205,25 +201,22 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
private fun loadData() {
adapter22.update(emptyList())
adapter18.update(emptyList())
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
val slots22 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_22)
val slots18 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_18)
val slotsOil = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_1)
withContext(Dispatchers.Main) {
slots22.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) adapter22.updateItemName(pos, slot.goodsName)
}
slots18.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
if (pos >= 0) adapter18.updateItemName(pos, slot.goodsName)
}
// 加载油盆格子名称(address=1
val oilSlot = slotsOil.firstOrNull { it.address == 1 }
binding.tvOilSlot.text =
if (oilSlot != null && oilSlot.goodsName.isNotEmpty()) oilSlot.goodsName else "-"
lifecycleScope.launch {
val slots22 = currentActivity.appViewModel.getSeasoningSlotsByDeviceId(ScaleDeviceConfig.DEVICE_ID_22)
val slots18 = currentActivity.appViewModel.getSeasoningSlotsByDeviceId(ScaleDeviceConfig.DEVICE_ID_18)
val slotsOil = currentActivity.appViewModel.getSeasoningSlotsByDeviceId(ScaleDeviceConfig.DEVICE_ID_1)
slots22.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) adapter22.updateItemName(pos, slot.goodsName)
}
slots18.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
if (pos >= 0) adapter18.updateItemName(pos, slot.goodsName)
}
// 加载油盆格子名称(address=1
val oilSlot = slotsOil.firstOrNull { it.address == 1 }
binding.tvOilSlot.text =
if (oilSlot != null && oilSlot.goodsName.isNotEmpty()) oilSlot.goodsName else "-"
}
}