From 85903eb646f65ad39d2dd9ca92197a995f40707f Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Wed, 11 Mar 2026 11:45:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20HomeActivity=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=9A=E9=A1=B9=E7=A8=B3=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E6=9E=B6=E6=9E=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 架构优化: - 新增 HomeViewModel,将 companion object 中的共享状态(shelfItems、 weightMap、selectedShelfIndex、temperatureC、humidity)迁移至 ViewModel, 消除 Activity 重建时的状态残留与全局状态污染问题 协程修复: - repeatOnLifecycle(CREATED) 改为 STARTED,确保 Flow 收集与 UI 可见性 生命周期对齐,进入后台时自动停止订阅 - 串口数据接收改用 Channel + 单一消费协程,替代多 launch 并发模式, 彻底消除 StringBuilder(serialReader)的并发竞争问题 稳定性修复: - 修复 tipDialog 从未赋值导致 Dialog 功能失效及 onDestroy 窗口泄漏问题 - 修复 weightHex.toInt() 十进制解析错误,改为 toIntOrNull(16) 并添加兜底 - 修复 updateOverdueState 按索引访问接口数据导致的越界风险, 改为按 deviceNo 精确匹配,同时修正交叉排列导致的状态串位问题 - 废弃 API getSerializableExtra 替换为版本分支兼容写法(minSdk 25+) 依赖升级: - androidx.core-ktx 1.10.1 → 1.12.0 Co-Authored-By: Claude Sonnet 4.6 --- .../shelves/activity/HomeActivity.kt | 164 ++++++++++-------- .../intelligent/shelves/net/HomeViewModel.kt | 59 +++++++ gradle/libs.versions.toml | 2 +- 3 files changed, 154 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt index a03be83..6975b5b 100644 --- a/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt +++ b/app/src/main/java/com/shuwei/intelligent/shelves/activity/HomeActivity.kt @@ -3,11 +3,11 @@ package com.shuwei.intelligent.shelves.activity import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log -import android.util.SparseIntArray import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -26,6 +26,7 @@ import com.shuwei.intelligent.shelves.model.ShelfBody import com.shuwei.intelligent.shelves.model.ShelfModel import com.shuwei.intelligent.shelves.model.ShelfResult import com.shuwei.intelligent.shelves.model.WeightEvent +import com.shuwei.intelligent.shelves.net.HomeViewModel import com.shuwei.intelligent.shelves.net.Loading import com.shuwei.intelligent.shelves.net.NetViewModel import com.shuwei.intelligent.shelves.net.RespData @@ -42,6 +43,7 @@ import com.shuwei.intelligent.shelves.utils.ext.toast import com.shuwei.intelligent.shelves.utils.ext.visible import com.shuwei.intelligent.shelves.utils.hexToBinary import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -97,16 +99,6 @@ class HomeActivity : BaseActivity() { const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}" - val list: MutableList = mutableListOf() - val weightArray = SparseIntArray() - var shelfIndex = 0 - - // public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0 - // 温度 - var showTemperatureC = "0" - - // 湿度 - var showHumidity = "0" } //401-false,1楼餐厅-true @@ -115,6 +107,7 @@ class HomeActivity : BaseActivity() { private lateinit var binding: ActivityHomeBinding private val viewModel: NetViewModel by viewModels() + private val homeViewModel: HomeViewModel by viewModels() @SuppressLint("NotifyDataSetChanged", "HardwareIds") override fun onCreate(savedInstanceState: Bundle?) { @@ -128,23 +121,34 @@ class HomeActivity : BaseActivity() { updateLeftStatus("") initRecyclerView() + + // 串口初始化:独立协程,仅执行一次 lifecycleScope.launch { val openState = SerialPortManager.open() log("onCreate: openState=$openState") if (openState) { + // 串口回调在 IO 线程,通过 Channel 无锁传递数据,不再嵌套 launch SerialPortManager.startReceive { data -> - // 更新UI显示接收数据 - lifecycleScope.launch { - try { - receiveSerialPortData(data) - } catch (e: Exception) { - e.printStackTrace() - } - } + serialDataChannel.trySend(data) } //SerialPortManager.send(DEVICE_INFO_CMD) } - repeatOnLifecycle(Lifecycle.State.CREATED) { + } + + // 串口数据消费:单一协程串行处理,serialReader 访问不存在并发竞争 + lifecycleScope.launch { + for (data in serialDataChannel) { + try { + receiveSerialPortData(data) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + // ViewModel 状态观察:STARTED 状态下收集,onStop 时自动取消,onStart 时恢复 + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.getAccessTokenUiState.collect { state -> when (state) { @@ -202,16 +206,16 @@ class HomeActivity : BaseActivity() { } private fun loadTestData() { - list.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 4, goodsName = "梨", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 9, goodsName = "梨", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId)) - list.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 4, goodsName = "梨", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 9, goodsName = "梨", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId)) + homeViewModel.shelfItems.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId)) shelfAdapter.notifyDataSetChanged() } @@ -251,12 +255,12 @@ class HomeActivity : BaseActivity() { // toast("数据错误") // return // } - list.clear() + homeViewModel.shelfItems.clear() val subList01 = tempList.subList(0, tempList.size / 2) val subList02 = tempList.subList(tempList.size / 2, tempList.size) repeat(tempList.size / 2) { index -> - list.add(subList01[index].also { it.deviceId = App.deviceId }) - list.add(subList02[index].also { it.deviceId = App.deviceId }) + homeViewModel.shelfItems.add(subList01[index].also { it.deviceId = App.deviceId }) + homeViewModel.shelfItems.add(subList02[index].also { it.deviceId = App.deviceId }) } shelfAdapter.notifyDataSetChanged() } @@ -277,7 +281,18 @@ class HomeActivity : BaseActivity() { */ private var terminalVersion: String = "" - private val serialReader by lazy { StringBuilder() } + /** + * 串口数据接收通道,无界缓冲。 + * IO 线程(串口回调)通过 trySend 写入,单一消费协程串行读取, + * 确保 serialReader 始终在同一协程中访问,规避并发竞争。 + */ + private val serialDataChannel = Channel(Channel.UNLIMITED) + + /** + * 串口数据拼包缓冲器。 + * 仅由 serialDataChannel 的单一消费协程访问,线程安全。 + */ + private val serialReader = StringBuilder() private fun receiveSerialPortData(srcData: String) { log("receiveSerialPortData: $srcData") @@ -414,7 +429,7 @@ class HomeActivity : BaseActivity() { //D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF runOnUiThread { if (tipDialog == null) { - CommonDialog(this).apply { + tipDialog = CommonDialog(this).apply { dialogTitle = "温馨提示" // dialogContent = logMsg confirmBlock = { @@ -513,21 +528,21 @@ class HomeActivity : BaseActivity() { var realWeight = 0 var firstByteBinary = hexToBinary(weightHex.substring(0, 2)) if (firstByteBinary.startsWith("0")) { - //正数 - realWeight = weightHex.toInt() + //正数:按十六进制解析,失败时兜底为 0 + realWeight = weightHex.toIntOrNull(16) ?: 0 } else { - //负数 + //负数:清除符号位后取绝对值,再取反 firstByteBinary = "0${firstByteBinary.substring(1)}" val firstByteHex = binaryToHex(firstByteBinary) weightHex = firstByteHex + weightHex.substring(2) - realWeight = -1 * weightHex.toInt() + realWeight = -1 * (weightHex.toIntOrNull(16) ?: 0) } - weightArray.put(index, realWeight) + homeViewModel.putWeight(index, realWeight) - if (index in 1..10 && list.size == 10) { - list.firstOrNull { it.deviceNo == index }?.let { + if (index in 1..10 && homeViewModel.shelfItems.size == 10) { + homeViewModel.shelfItems.firstOrNull { it.deviceNo == index }?.let { it.weight = realWeight.toDouble() - val pos = list.indexOf(it) + val pos = homeViewModel.shelfItems.indexOf(it) shelfAdapter.notifyItemChanged(pos) log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}") EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight)) @@ -608,35 +623,44 @@ class HomeActivity : BaseActivity() { // TaskManager.cancelTask() saveTaskJob?.cancel() overdueTaskJob?.cancel() + // 关闭 Channel,消费协程的 for 循环将随之结束 + serialDataChannel.close() + // 销毁前关闭 Dialog,防止 Activity 已销毁时窗口仍附着导致泄漏 + tipDialog?.dismiss() + tipDialog = null EventBus.getDefault().unregister(this) super.onDestroy() } - @Suppress("DEPRECATION") private val shelfAdapter by lazy { - ShelfAdapter(list).apply { + ShelfAdapter(homeViewModel.shelfItems).apply { setOnItemClickListener { _, _, position -> - shelfIndex = position + homeViewModel.selectedShelfIndex = position lifecycleScope.launch { if (isNeedOpenLock) { - val openCmd = if (list[position].deviceNo in 1..5) + val openCmd = if (homeViewModel.shelfItems[position].deviceNo in 1..5) LEFT_SHELF_OPEN_CMD else RIGHT_SHELF_OPEN_CMD val openState = SerialPortManager.send(openCmd) } // if (openState.not()) { -// toast("${list[position].goodsName}柜门开启失败") +// toast("${homeViewModel.shelfItems[position].goodsName}柜门开启失败") // return@launch // } //SerialPortManager.send(START_CMD) launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { - it.putExtra(ShelfActivity.SHELF_MODEL, list[position]) + it.putExtra(ShelfActivity.SHELF_MODEL, homeViewModel.shelfItems[position]) }) { it?.run { - val model = - getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel - list[position].apply { + // API 33 起支持带类型参数的重载,低版本回退旧 API + as? 安全转换 + val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java) + } else { + @Suppress("DEPRECATION") + getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel + } ?: return@run + homeViewModel.shelfItems[position].apply { goodsId = model.goodsId goodsName = model.goodsName weight = model.weight @@ -648,12 +672,12 @@ class HomeActivity : BaseActivity() { try { - val submitList = list.sortedBy { item -> item.deviceNo } + val submitList = homeViewModel.shelfItems.sortedBy { item -> item.deviceNo } val body = ShelfBody().also { body -> body.deviceId = App.deviceId ?: "" body.canteenId = App.canteenId - body.temperature = showTemperatureC - body.humidity = showHumidity + body.temperature = homeViewModel.temperatureC + body.humidity = homeViewModel.humidity body.goodsList = submitList } Log.d("mzf", "performSync: body:${body.toJsonString()}") @@ -714,7 +738,7 @@ class HomeActivity : BaseActivity() { @SuppressLint("NotifyDataSetChanged") private fun loadEmptyView() { - list.clear() + homeViewModel.shelfItems.clear() shelfAdapter.notifyDataSetChanged() binding.include?.let { @@ -735,7 +759,7 @@ class HomeActivity : BaseActivity() { @Subscribe(threadMode = ThreadMode.MAIN) public fun clearEmptyShelf(event: ClearShelfEvent) { - val item = list.firstOrNull { it.deviceNo == event.shelfNo } + val item = homeViewModel.shelfItems.firstOrNull { it.deviceNo == event.shelfNo } item?.apply { goodsId = "" goodsName = null @@ -744,7 +768,7 @@ class HomeActivity : BaseActivity() { createTime = null overdueDay = null } - shelfAdapter.notifyItemChanged(list.indexOf(item)) + shelfAdapter.notifyItemChanged(homeViewModel.shelfItems.indexOf(item)) } private val taskExecutor by lazy { IntervalExecutor() } @@ -773,10 +797,14 @@ class HomeActivity : BaseActivity() { private fun updateOverdueState() { viewModel.updateOverdueState { state, items -> if (items.isNullOrEmpty()) return@updateOverdueState - list.forEachIndexed { index, model -> - //接口过期状态变化后更新状态 - if (items[index].overdue != model.overdue) { - model.overdue = items[index].overdue + homeViewModel.shelfItems.forEachIndexed { index, model -> + // 按 deviceNo 精确匹配接口数据,避免: + // 1. 两端列表大小不一致时的 IndexOutOfBoundsException + // 2. 本地列表交叉排列与接口顺序不一致导致的状态串位 + val serverModel = items.find { it.deviceNo == model.deviceNo } + ?: return@forEachIndexed + if (serverModel.overdue != model.overdue) { + model.overdue = serverModel.overdue shelfAdapter.notifyItemChanged(index) log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") } @@ -785,22 +813,18 @@ class HomeActivity : BaseActivity() { } private fun saveShelfGoodsList() { - log("performSync: list:${list.toJsonString()}") - val submitList = list.sortedBy { it.deviceNo } + log("performSync: list:${homeViewModel.shelfItems.toJsonString()}") + val submitList = homeViewModel.shelfItems.sortedBy { it.deviceNo } submitList.forEach { if (it.goodsId.isNullOrBlank()) { it.weight = it.weightBak } } -// submitList.forEach { -// it.temperature = HomeActivity.showTemperatureC -// it.humidity = HomeActivity.showHumidity -// } val body = ShelfBody().also { it.deviceId = App.deviceId it.canteenId = App.canteenId - it.temperature = showTemperatureC - it.humidity = showHumidity + it.temperature = homeViewModel.temperatureC + it.humidity = homeViewModel.humidity it.goodsList = submitList } log("performSync: body:${body.toJsonString()}") diff --git a/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt b/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt new file mode 100644 index 0000000..d228d8b --- /dev/null +++ b/app/src/main/java/com/shuwei/intelligent/shelves/net/HomeViewModel.kt @@ -0,0 +1,59 @@ +package com.shuwei.intelligent.shelves.net + +import androidx.lifecycle.ViewModel +import com.shuwei.intelligent.shelves.model.ShelfModel + +/** + * HomeActivity 专用 ViewModel。 + * + * 职责:管理原本散落在 HomeActivity.companion object 中的 UI 状态数据, + * 避免静态共享状态导致的生命周期污染与状态残留问题。 + * + * 包含:货架列表、重量数据、选中索引、温湿度。 + */ +class HomeViewModel : ViewModel() { + + /** + * 货架列表。 + * Adapter 持有同一个列表引用,修改后需调用 notifyDataSetChanged / notifyItemChanged 触发刷新。 + */ + val shelfItems: MutableList = mutableListOf() + + /** + * 重量数据缓存。 + * key:deviceNo(货架编号),value:重量(克)。 + * 替代原 SparseIntArray,使用更符合 Kotlin 习惯的 MutableMap。 + */ + private val weightMap: MutableMap = mutableMapOf() + + /** + * 当前选中货架的列表索引(点击 item 时更新)。 + */ + var selectedShelfIndex: Int = 0 + + /** + * 当前摄氏温度,由串口心跳数据更新,字符串格式(如 "24.5")。 + */ + var temperatureC: String = "0" + + /** + * 当前湿度,由串口心跳数据更新,字符串格式(如 "60")。 + */ + var humidity: String = "0" + + /** + * 更新指定货架编号的重量。 + * @param deviceNo 货架编号 + * @param weight 重量(克) + */ + fun putWeight(deviceNo: Int, weight: Int) { + weightMap[deviceNo] = weight + } + + /** + * 获取指定货架编号的重量。 + * @param deviceNo 货架编号 + * @return 重量(克),若无记录返回 0 + */ + fun getWeight(deviceNo: Int): Int = weightMap[deviceNo] ?: 0 +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c889e6..801716e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ #agp = "8.11.1" agp = "8.4.0" kotlin = "2.0.21" -coreKtx = "1.10.1" +coreKtx = "1.12.0" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1"