重构 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.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.util.SparseIntArray
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope 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.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfResult import com.shuwei.intelligent.shelves.model.ShelfResult
import com.shuwei.intelligent.shelves.model.WeightEvent 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.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData 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.ext.visible
import com.shuwei.intelligent.shelves.utils.hexToBinary import com.shuwei.intelligent.shelves.utils.hexToBinary
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@@ -97,16 +99,6 @@ class HomeActivity : BaseActivity() {
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}" 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 //401-false1楼餐厅-true
@@ -115,6 +107,7 @@ class HomeActivity : BaseActivity() {
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels() private val viewModel: NetViewModel by viewModels()
private val homeViewModel: HomeViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged", "HardwareIds") @SuppressLint("NotifyDataSetChanged", "HardwareIds")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -128,23 +121,34 @@ class HomeActivity : BaseActivity() {
updateLeftStatus("") updateLeftStatus("")
initRecyclerView() initRecyclerView()
// 串口初始化:独立协程,仅执行一次
lifecycleScope.launch { lifecycleScope.launch {
val openState = SerialPortManager.open() val openState = SerialPortManager.open()
log("onCreate: openState=$openState") log("onCreate: openState=$openState")
if (openState) { if (openState) {
// 串口回调在 IO 线程,通过 Channel 无锁传递数据,不再嵌套 launch
SerialPortManager.startReceive { data -> SerialPortManager.startReceive { data ->
// 更新UI显示接收数据 serialDataChannel.trySend(data)
lifecycleScope.launch {
try {
receiveSerialPortData(data)
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
//SerialPortManager.send(DEVICE_INFO_CMD) //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 { launch {
viewModel.getAccessTokenUiState.collect { state -> viewModel.getAccessTokenUiState.collect { state ->
when (state) { when (state) {
@@ -202,16 +206,16 @@ class HomeActivity : BaseActivity() {
} }
private fun loadTestData() { private fun loadTestData() {
list.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 4, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 4, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 9, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 9, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId)) homeViewModel.shelfItems.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId))
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
} }
@@ -251,12 +255,12 @@ class HomeActivity : BaseActivity() {
// toast("数据错误") // toast("数据错误")
// return // return
// } // }
list.clear() homeViewModel.shelfItems.clear()
val subList01 = tempList.subList(0, tempList.size / 2) val subList01 = tempList.subList(0, tempList.size / 2)
val subList02 = tempList.subList(tempList.size / 2, tempList.size) val subList02 = tempList.subList(tempList.size / 2, tempList.size)
repeat(tempList.size / 2) { index -> repeat(tempList.size / 2) { index ->
list.add(subList01[index].also { it.deviceId = App.deviceId }) homeViewModel.shelfItems.add(subList01[index].also { it.deviceId = App.deviceId })
list.add(subList02[index].also { it.deviceId = App.deviceId }) homeViewModel.shelfItems.add(subList02[index].also { it.deviceId = App.deviceId })
} }
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
} }
@@ -277,7 +281,18 @@ class HomeActivity : BaseActivity() {
*/ */
private var terminalVersion: String = "" 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) { private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData") log("receiveSerialPortData: $srcData")
@@ -414,7 +429,7 @@ class HomeActivity : BaseActivity() {
//D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF //D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF
runOnUiThread { runOnUiThread {
if (tipDialog == null) { if (tipDialog == null) {
CommonDialog(this).apply { tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示" dialogTitle = "温馨提示"
// dialogContent = logMsg // dialogContent = logMsg
confirmBlock = { confirmBlock = {
@@ -513,21 +528,21 @@ class HomeActivity : BaseActivity() {
var realWeight = 0 var realWeight = 0
var firstByteBinary = hexToBinary(weightHex.substring(0, 2)) var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
if (firstByteBinary.startsWith("0")) { if (firstByteBinary.startsWith("0")) {
//正数 //正数:按十六进制解析,失败时兜底为 0
realWeight = weightHex.toInt() realWeight = weightHex.toIntOrNull(16) ?: 0
} else { } else {
//负数 //负数:清除符号位后取绝对值,再取反
firstByteBinary = "0${firstByteBinary.substring(1)}" firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary) val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2) 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) { if (index in 1..10 && homeViewModel.shelfItems.size == 10) {
list.firstOrNull { it.deviceNo == index }?.let { homeViewModel.shelfItems.firstOrNull { it.deviceNo == index }?.let {
it.weight = realWeight.toDouble() it.weight = realWeight.toDouble()
val pos = list.indexOf(it) val pos = homeViewModel.shelfItems.indexOf(it)
shelfAdapter.notifyItemChanged(pos) shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}") log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}")
EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight)) EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight))
@@ -608,35 +623,44 @@ class HomeActivity : BaseActivity() {
// TaskManager.cancelTask() // TaskManager.cancelTask()
saveTaskJob?.cancel() saveTaskJob?.cancel()
overdueTaskJob?.cancel() overdueTaskJob?.cancel()
// 关闭 Channel,消费协程的 for 循环将随之结束
serialDataChannel.close()
// 销毁前关闭 Dialog,防止 Activity 已销毁时窗口仍附着导致泄漏
tipDialog?.dismiss()
tipDialog = null
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
super.onDestroy() super.onDestroy()
} }
@Suppress("DEPRECATION")
private val shelfAdapter by lazy { private val shelfAdapter by lazy {
ShelfAdapter(list).apply { ShelfAdapter(homeViewModel.shelfItems).apply {
setOnItemClickListener { _, _, position -> setOnItemClickListener { _, _, position ->
shelfIndex = position homeViewModel.selectedShelfIndex = position
lifecycleScope.launch { lifecycleScope.launch {
if (isNeedOpenLock) { 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 LEFT_SHELF_OPEN_CMD
else else
RIGHT_SHELF_OPEN_CMD RIGHT_SHELF_OPEN_CMD
val openState = SerialPortManager.send(openCmd) val openState = SerialPortManager.send(openCmd)
} }
// if (openState.not()) { // if (openState.not()) {
// toast("${list[position].goodsName}柜门开启失败") // toast("${homeViewModel.shelfItems[position].goodsName}柜门开启失败")
// return@launch // return@launch
// } // }
//SerialPortManager.send(START_CMD) //SerialPortManager.send(START_CMD)
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also { 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 { it?.run {
val model = // API 33 起支持带类型参数的重载,低版本回退旧 API + as? 安全转换
getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
list[position].apply { 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 goodsId = model.goodsId
goodsName = model.goodsName goodsName = model.goodsName
weight = model.weight weight = model.weight
@@ -648,12 +672,12 @@ class HomeActivity : BaseActivity() {
try { try {
val submitList = list.sortedBy { item -> item.deviceNo } val submitList = homeViewModel.shelfItems.sortedBy { item -> item.deviceNo }
val body = ShelfBody().also { body -> val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: "" body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId body.canteenId = App.canteenId
body.temperature = showTemperatureC body.temperature = homeViewModel.temperatureC
body.humidity = showHumidity body.humidity = homeViewModel.humidity
body.goodsList = submitList body.goodsList = submitList
} }
Log.d("mzf", "performSync: body:${body.toJsonString()}") Log.d("mzf", "performSync: body:${body.toJsonString()}")
@@ -714,7 +738,7 @@ class HomeActivity : BaseActivity() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() { private fun loadEmptyView() {
list.clear() homeViewModel.shelfItems.clear()
shelfAdapter.notifyDataSetChanged() shelfAdapter.notifyDataSetChanged()
binding.include?.let { binding.include?.let {
@@ -735,7 +759,7 @@ class HomeActivity : BaseActivity() {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) { public fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo } val item = homeViewModel.shelfItems.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply { item?.apply {
goodsId = "" goodsId = ""
goodsName = null goodsName = null
@@ -744,7 +768,7 @@ class HomeActivity : BaseActivity() {
createTime = null createTime = null
overdueDay = null overdueDay = null
} }
shelfAdapter.notifyItemChanged(list.indexOf(item)) shelfAdapter.notifyItemChanged(homeViewModel.shelfItems.indexOf(item))
} }
private val taskExecutor by lazy { IntervalExecutor() } private val taskExecutor by lazy { IntervalExecutor() }
@@ -773,10 +797,14 @@ class HomeActivity : BaseActivity() {
private fun updateOverdueState() { private fun updateOverdueState() {
viewModel.updateOverdueState { state, items -> viewModel.updateOverdueState { state, items ->
if (items.isNullOrEmpty()) return@updateOverdueState if (items.isNullOrEmpty()) return@updateOverdueState
list.forEachIndexed { index, model -> homeViewModel.shelfItems.forEachIndexed { index, model ->
//接口过期状态变化后更新状态 // 按 deviceNo 精确匹配接口数据,避免:
if (items[index].overdue != model.overdue) { // 1. 两端列表大小不一致时的 IndexOutOfBoundsException
model.overdue = items[index].overdue // 2. 本地列表交叉排列与接口顺序不一致导致的状态串位
val serverModel = items.find { it.deviceNo == model.deviceNo }
?: return@forEachIndexed
if (serverModel.overdue != model.overdue) {
model.overdue = serverModel.overdue
shelfAdapter.notifyItemChanged(index) shelfAdapter.notifyItemChanged(index)
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}") log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}")
} }
@@ -785,22 +813,18 @@ class HomeActivity : BaseActivity() {
} }
private fun saveShelfGoodsList() { private fun saveShelfGoodsList() {
log("performSync: list:${list.toJsonString()}") log("performSync: list:${homeViewModel.shelfItems.toJsonString()}")
val submitList = list.sortedBy { it.deviceNo } val submitList = homeViewModel.shelfItems.sortedBy { it.deviceNo }
submitList.forEach { submitList.forEach {
if (it.goodsId.isNullOrBlank()) { if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak it.weight = it.weightBak
} }
} }
// submitList.forEach {
// it.temperature = HomeActivity.showTemperatureC
// it.humidity = HomeActivity.showHumidity
// }
val body = ShelfBody().also { val body = ShelfBody().also {
it.deviceId = App.deviceId it.deviceId = App.deviceId
it.canteenId = App.canteenId it.canteenId = App.canteenId
it.temperature = showTemperatureC it.temperature = homeViewModel.temperatureC
it.humidity = showHumidity it.humidity = homeViewModel.humidity
it.goodsList = submitList it.goodsList = submitList
} }
log("performSync: body:${body.toJsonString()}") 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.11.1"
agp = "8.4.0" agp = "8.4.0"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.12.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"