重构 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:
@@ -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-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<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()
|
||||
|
||||
/**
|
||||
* 重量数据缓存。
|
||||
* key:deviceNo(货架编号),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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user