重构 HomeActivity:修复多项稳定性与架构问题

架构优化:
- 新增 HomeViewModel,将 companion object 中的共享状态(shelfItems、
  weightMap、selectedShelfIndex、temperatureC、humidity)迁移至 ViewModel,
  消除 Activity 重建时的状态残留与全局状态污染问题

协程修复:
- repeatOnLifecycle(CREATED) 改为 STARTED,确保 Flow 收集与 UI 可见性
  生命周期对齐,进入后台时自动停止订阅
- 串口数据接收改用 Channel<String> + 单一消费协程,替代多 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 11:45:44 +08:00
co-authored by Claude Sonnet 4.6
parent 11e6123cc9
commit 85903eb646
3 changed files with 154 additions and 71 deletions
@@ -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<ShelfModel> = mutableListOf()
val weightArray = SparseIntArray()
var shelfIndex = 0
// public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0
// 温度
var showTemperatureC = "0"
// 湿度
var showHumidity = "0"
}
//401-false1楼餐厅-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<String>(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()}")
@@ -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<ShelfModel> = mutableListOf()
/**
* 重量数据缓存。
* keydeviceNo(货架编号),value:重量(克)。
* 替代原 SparseIntArray,使用更符合 Kotlin 习惯的 MutableMap。
*/
private val weightMap: MutableMap<Int, Int> = 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
}
+1 -1
View File
@@ -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"