refactor(scale): 优化串口数据处理逻辑和自动应答机制

- 在串口数据读取中增加帧拼装与解析,统一自动应答发送
- 实现心跳日志降频,减少重复日志,避免主线程无效计算
- 将数据回调分派至主线程,确保线程安全
- 缓存时间格式化器,避免重复创建对象
- 增加秤重数据变更判断,避免无变化时刷新UI和日志
- 异步优化文件日志写入,使用协程通道和单线程写入,避免多线程SimpleDateFormat竞争
- 修复文件路径和上下文使用,防止Activity内存泄漏
- 精简部分注释和代码未使用段落,提高代码清晰度
This commit is contained in:
mazengfei
2026-07-03 14:57:25 +08:00
parent 1e44cd2765
commit 7ab8e09a46
5 changed files with 194 additions and 253 deletions
@@ -166,10 +166,23 @@ object ScaleManager {
val flow = serialPort?.read() val flow = serialPort?.read()
flow?.collect { buffer -> flow?.collect { buffer ->
val data = buffer.toHexString2().uppercase(Locale.ROOT) val data = buffer.toHexString2().uppercase(Locale.ROOT)
// IO 线程:帧拼装与解析
val parsed = tryParseCompleteFrame(data)
if (parsed != null) {
// IO 线程:发送自动应答(不占用主线程)
sendAutoResponse(parsed.first)
// IO 线程:心跳日志降频
if (parsed.first.cmd == CMD_HEARTBEAT || parsed.first.cmd == CMD_HEARTBEAT_STORE) {
logHeartbeatIfNeeded(parsed.first, parsed.second)
}
}
// 仅回调分发和 preHook 需要主线程
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
try { try {
preHook?.invoke(data) preHook?.invoke(data)
onDataReceived(data) if (parsed != null) {
invokeCallbacks(parsed.first)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
log("串口数据处理异常: ${e.message}") log("串口数据处理异常: ${e.message}")
@@ -179,7 +192,7 @@ object ScaleManager {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
delay(50) delay(10)
} }
} }
} }
@@ -189,19 +202,117 @@ object ScaleManager {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
/** /**
* 接收串口原始数据(在串口回调中调用此方法 * 接收串口原始数据(公开 API,主线程安全
* *
* 内部自动完成分片拼装 → 帧解析 → 命令分发 → 自动应答 * 内部自动完成分片拼装 → 帧解析 → 自动应答 → 回调分发
* *
* @param hexData 串口收到的十六进制字符串(可能为分片数据) * @param hexData 串口收到的十六进制字符串(可能为分片数据)
*/ */
fun onDataReceived(hexData: String) { fun onDataReceived(hexData: String) {
val completeFrame = parser.appendData(hexData) ?: return val parsed = tryParseCompleteFrame(hexData) ?: return
val frame = parser.parseFrame(completeFrame) ?: run { sendAutoResponse(parsed.first)
invokeCallbacks(parsed.first)
}
/**
* IO 线程安全的帧拼装与解析(纯计算,无副作用)
*
* @return Pair<ScaleFrame, rawHex> 或 null(分片未完整/解析失败)
*/
private fun tryParseCompleteFrame(hexData: String): Pair<ScaleFrame, String>? {
val completeFrame = parser.appendData(hexData) ?: return null
val frame = parser.parseFrame(completeFrame)
if (frame == null) {
log("帧解析失败,原始数据:$completeFrame") log("帧解析失败,原始数据:$completeFrame")
return
} }
dispatch(frame, completeFrame) return frame?.let { it to completeFrame }
}
/**
* IO 线程:发送协议自动应答(不阻塞主线程)
*/
private fun sendAutoResponse(frame: ScaleFrame) {
when (frame.cmd) {
CMD_HEARTBEAT, CMD_HEARTBEAT_STORE -> {
val firmwareHex = frame.firmwareVersion.toAsciiHex()
val resp = "${HEADER_HEX}0102%02X%02X${firmwareHex}${FOOTER_HEX}".format(
heartbeatInterval and 0xFF, lockTimeout and 0xFF
)
sendCmd(resp)
}
CMD_UNLOCK -> {
val terminalIdHex = frame.terminalId.toAsciiHex()
val firmwareHex = frame.firmwareVersion.toAsciiHex()
sendCmd("${HEADER_HEX}020200${terminalIdHex}${firmwareHex}${FOOTER_HEX}")
}
CMD_CARD_UPLOAD -> sendCmd("${HEADER_HEX}050203${FOOTER_HEX}")
CMD_OPERATION_RECORD -> sendCmd("${HEADER_HEX}0B0203${FOOTER_HEX}")
CMD_TEMP_REPORT -> sendCmd("${HEADER_HEX}100200${FOOTER_HEX}")
CMD_POSITION_REPORT -> sendCmd("${HEADER_HEX}0E0200${FOOTER_HEX}")
CMD_TIME_SYNC -> {
val nowMs = System.currentTimeMillis()
val timeSec = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
val timeHex = "%08X".format(timeSec.toInt())
.chunked(2).reversed().joinToString("")
sendCmd("${HEADER_HEX}0C02${timeHex}${FOOTER_HEX}")
}
CMD_ACTIVATE -> {
if (activateCode.isNotBlank()) sendCmd(activateCode)
}
}
}
/**
* 主线程:仅回调分发(不含 sendCmd 和日志)
*/
private fun invokeCallbacks(frame: ScaleFrame) {
when (frame.cmd) {
CMD_ACTIVATE -> {
activateListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_HEARTBEAT, CMD_HEARTBEAT_STORE -> {
val data = parser.parseHeartbeat(frame.rawBody)
if (data != null) {
heartbeatListeners.forEach { it(frame, data) }
}
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_UNLOCK -> {
lockListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_GET_STATUS -> {
val data = parser.parseStatus(frame.rawBody)
if (data != null) {
statusListeners.forEach { it(frame, data) }
}
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_CARD_UPLOAD -> {
cardListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_OPERATION_RECORD -> {
operationListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_TIME_SYNC -> {
timeSyncListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_POSITION_REPORT -> {
positionListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
CMD_TEMP_REPORT -> {
tempReportListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
else -> {
rawListeners[frame.cmd]?.forEach { it(frame) }
}
}
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -443,164 +554,28 @@ object ScaleManager {
rawListeners.clear() rawListeners.clear()
logListeners.clear() logListeners.clear()
activateCode = "" activateCode = ""
lastHeartbeatLogSecond = 0
parser.reset() parser.reset()
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 内部命令分发与自动应答 // 日志与工具
// ---------------------------------------------------------------- // ----------------------------------------------------------------
private fun dispatch(frame: ScaleFrame, rawHex: String) { /** 上次心跳日志时间(秒),用于降频:同一秒内不重复记录心跳日志 */
when (frame.cmd) { private var lastHeartbeatLogSecond: Long = 0
CMD_ACTIVATE -> handleActivate(frame, rawHex)
CMD_HEARTBEAT,
CMD_HEARTBEAT_STORE -> handleHeartbeat(frame, rawHex)
CMD_UNLOCK -> handleLockFeedback(frame, rawHex)
CMD_GET_STATUS -> handleStatus(frame, rawHex)
CMD_CARD_UPLOAD -> handleCardUpload(frame, rawHex)
CMD_OPERATION_RECORD -> handleOperationRecord(frame, rawHex)
CMD_TIME_SYNC -> handleTimeSync(frame, rawHex)
CMD_POSITION_REPORT -> handlePositionReport(frame, rawHex)
CMD_TEMP_REPORT -> handleTempReport(frame, rawHex)
else -> {
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到未处理命令:0x%02X,终端ID:${frame.terminalId},原始数据:$rawHex".format(frame.cmd))
}
}
}
/** /**
* 处理终端激活(CMD=0x00 * IO 线程:心跳日志降频 — 每秒最多记录一次
* 若 activateMap 中存在当前 deviceId 对应的激活码,则自动应答;
* 否则仍回调 onActivate,由业务方自行处理
*/ */
private fun handleActivate(frame: ScaleFrame, rawHex: String) { private fun logHeartbeatIfNeeded(frame: ScaleFrame, rawHex: String) {
log("收到[终端激活]指令,终端ID${frame.terminalId},固件版本:${frame.firmwareVersion},原始数据:$rawHex") val nowSec = System.currentTimeMillis() / 1000
val activeCmd = activateCode if (nowSec == lastHeartbeatLogSecond) return
if (!activeCmd.isNullOrBlank()) { lastHeartbeatLogSecond = nowSec
sendCmd(activeCmd)
log("激活码自动应答:$activeCmd")
}
activateListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
}
/**
* 处理心跳(CMD=0x01 / 0x81
* 自动应答:[心跳间隔] [上锁时间] [设备固件版本5字节]
*/
private fun handleHeartbeat(frame: ScaleFrame, rawHex: String) {
val firmwareHex = frame.firmwareVersion.toAsciiHex()
val resp = "${HEADER_HEX}0102%02X%02X${firmwareHex}${FOOTER_HEX}".format(
heartbeatInterval and 0xFF,
lockTimeout and 0xFF
)
sendCmd(resp)
val data = parser.parseHeartbeat(frame.rawBody) val data = parser.parseHeartbeat(frame.rawBody)
if (data != null) {
heartbeatListeners.forEach { it(frame, data) }
}
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[心跳]指令,终端ID${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex") log("收到[心跳]指令,终端ID${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex")
} }
/**
* 处理开锁反馈(CMD=0x02,终端发起,关门后上报)
* 自动应答:[锁号00] [终端ID 15字节] [固件版本 5字节]
*/
private fun handleLockFeedback(frame: ScaleFrame, rawHex: String) {
val terminalIdHex = frame.terminalId.toAsciiHex()
val firmwareHex = frame.firmwareVersion.toAsciiHex()
val resp = "${HEADER_HEX}020200${terminalIdHex}${firmwareHex}${FOOTER_HEX}"
sendCmd(resp)
lockListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[开锁反馈]指令,终端ID${frame.terminalId},原始数据:$rawHex")
}
/**
* 处理获取状态响应(CMD=0x03,终端响应)
* 无需应答,直接解析并分发
*/
private fun handleStatus(frame: ScaleFrame, rawHex: String) {
val data = parser.parseStatus(frame.rawBody)
if (data != null) {
statusListeners.forEach { it(frame, data) }
}
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[获取状态]响应,终端ID${frame.terminalId},秤数量:${data?.scaleCount ?: 0},原始数据:$rawHex")
}
/**
* 处理卡号上发(CMD=0x05,终端发起)
* 自动应答:成功 0x03
*/
private fun handleCardUpload(frame: ScaleFrame, rawHex: String) {
val resp = "${HEADER_HEX}050203${FOOTER_HEX}"
sendCmd(resp)
cardListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[卡号上发]指令,终端ID${frame.terminalId},原始数据:$rawHex")
}
/**
* 处理操作记录上传(CMD=0x0B,终端发起)
* 自动应答:成功 0x03
*/
private fun handleOperationRecord(frame: ScaleFrame, rawHex: String) {
val resp = "${HEADER_HEX}0B0203${FOOTER_HEX}"
sendCmd(resp)
operationListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[操作记录上传]指令,终端ID${frame.terminalId},原始数据:$rawHex")
}
/**
* 处理对时请求(CMD=0x0C,终端发起,仅以太网版本)
* 自动应答:当前 Unix 时间戳(含时区偏移),小端序4字节
*/
private fun handleTimeSync(frame: ScaleFrame, rawHex: String) {
val nowMs = System.currentTimeMillis()
val timeSec = nowMs / 1000 + TimeZone.getDefault().getOffset(nowMs) / 1000
// 取低32位,转为小端序4字节十六进制
val timeHex = "%08X".format(timeSec.toInt())
.chunked(2).reversed().joinToString("")
val resp = "${HEADER_HEX}0C02${timeHex}${FOOTER_HEX}"
sendCmd(resp)
timeSyncListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[对时]请求,终端ID${frame.terminalId},时间戳:${timeSec},应答:$resp")
}
/**
* 处理终端位置上报(CMD=0x0E,终端发起,仅4G版本)
* 自动应答:保留字节 0x00
*/
private fun handlePositionReport(frame: ScaleFrame, rawHex: String) {
val resp = "${HEADER_HEX}0E0200${FOOTER_HEX}"
sendCmd(resp)
positionListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[终端位置上报]指令,终端ID${frame.terminalId},原始数据:$rawHex")
}
/**
* 处理温控信息上报(CMD=0x10,终端发起)
* 自动应答:保留字节 0x00
*/
private fun handleTempReport(frame: ScaleFrame, rawHex: String) {
val resp = "${HEADER_HEX}100200${FOOTER_HEX}"
sendCmd(resp)
tempReportListeners.forEach { it(frame) }
rawListeners[frame.cmd]?.forEach { it(frame) }
log("收到[温控信息上报]指令,终端ID${frame.terminalId},原始数据:$rawHex")
}
// ----------------------------------------------------------------
// 私有工具
// ----------------------------------------------------------------
private fun log(message: String) { private fun log(message: String) {
logListeners.forEach { it(message) } logListeners.forEach { it(message) }
} }
@@ -1,131 +1,91 @@
package com.shuwei.intelligent.shelves.utils package com.shuwei.intelligent.shelves.utils
import android.content.Context import android.content.Context
import com.shuwei.intelligent.shelves.utils.CrashHandler.Companion.CRASH_REPORTS_DIR import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.* import java.io.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
/** /**
* 文件日志记录器 * 文件日志记录器(异步版)
* 功能:保存日志到本地文件,每条数据一行,每天生成新文件 *
* log() 仅投递原始消息到 Channel(任意线程安全),
* 格式化与文件写入在单一后台协程顺序执行,避免 SimpleDateFormat 多线程竞争。
*/ */
class FileLogger(private val context: Context) { class FileLogger(context: Context) {
companion object { companion object {
private const val LOG_DIR = "logs"
private const val FILE_PREFIX = "log_" private const val FILE_PREFIX = "log_"
private const val FILE_EXTENSION = ".txt" private const val FILE_EXTENSION = ".txt"
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) /** 全局单线程日志写入 */
private val logChannel = Channel<RawEntry>(capacity = Channel.UNLIMITED)
private val writerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
init {
writerScope.launch {
// 单协程顺序消费:SimpleDateFormat 在此线程安全
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
for (entry in logChannel) {
try {
val fileName = "${FILE_PREFIX}${dateFormat.format(Date())}${FILE_EXTENSION}"
val timestamp = timeFormat.format(Date())
val logLine = "[$timestamp] [${entry.level}] ${entry.message}\n"
val output = entry.appContext.openFileOutput(
fileName, Context.MODE_APPEND
)
BufferedWriter(OutputStreamWriter(output)).use { it.write(logLine) }
} catch (_: Exception) {
// 写入失败静默丢弃
}
}
}
}
private data class RawEntry(
val appContext: Context,
val message: String,
val level: LogLevel
)
} }
/** 持有 ApplicationContext 避免 Activity 泄漏 */
private val appContext: Context = context.applicationContext
/** /**
* 记录日志 * 记录日志(异步,线程安全)
* @param message 日志消息
* @param level 日志级别
*/ */
fun log(message: String, level: LogLevel = LogLevel.INFO) { fun log(message: String, level: LogLevel = LogLevel.INFO) {
try { logChannel.trySend(RawEntry(appContext, message, level))
val logEntry = formatLogEntry(message, level)
writeToFile(logEntry)
} catch (e: IOException) {
e.printStackTrace()
}
} }
/**
* 格式化日志条目
*/
private fun formatLogEntry(message: String, level: LogLevel): String {
val timestamp = timeFormat.format(Date())
return "[$timestamp] [$level] $message\n"
}
/**
* 写入文件
*/
private fun writeToFile(logEntry: String) {
// val path = File(context.filesDir, LOG_DIR)
// if (path.exists().not()) {
// path.mkdirs()
// }
val fileName = getCurrentDateFileName()
// val file = File(path, fileName)
// if (file.exists().not()) {
// file.createNewFile()
// }
val output = context.openFileOutput(fileName, Context.MODE_APPEND)
val writer = BufferedWriter(OutputStreamWriter(output))
writer.use {
it.write(logEntry)
}
}
/**
* 获取当前日期对应的文件名
*/
private fun getCurrentDateFileName(): String {
val date = dateFormat.format(Date())
return "${FILE_PREFIX}${date}${FILE_EXTENSION}"
}
/**
* 读取指定日期的日志文件
* @param date 日期字符串,格式:yyyy-MM-dd
*/
fun readLogsByDate(date: String): List<String> { fun readLogsByDate(date: String): List<String> {
val fileName = "${FILE_PREFIX}${date}${FILE_EXTENSION}" return readLogsByName("${FILE_PREFIX}${date}${FILE_EXTENSION}")
return readLogsByName(fileName)
} }
fun readLogsByName(fileName: String): List<String> { fun readLogsByName(fileName: String): List<String> {
try { try {
val input = context.openFileInput(fileName) val input = appContext.openFileInput(fileName)
val reader = BufferedReader(InputStreamReader(input)) val reader = BufferedReader(InputStreamReader(input))
val list: MutableList<String> = reader.useLines { lines -> return reader.useLines { lines -> lines.toList().toMutableList() }
lines.toList().toMutableList() } catch (_: FileNotFoundException) {
}
// val input2 = FileInputStream(getCrashDir(context))
// val reader2 = BufferedReader(InputStreamReader(input2))
// val list2: MutableList<String> = reader2.useLines {lines ->
// lines.toList().toMutableList()
// }
// list.addAll(list2)
return list
} catch (e: FileNotFoundException) {
return emptyList() return emptyList()
} }
} }
public fun getCrashDir(context: Context):File { fun getCrashDir(context: Context): File {
//val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR) var crashDir = File(context.filesDir, "crash_reports")
var crashDir = File(context.filesDir, CRASH_REPORTS_DIR) if (!crashDir.exists()) crashDir.mkdirs()
if (!crashDir.exists()) { if (!crashDir.exists()) crashDir = File(context.cacheDir, "crash_reports")
crashDir.mkdirs()
}
if (!(crashDir.exists())) {
crashDir = File(context.cacheDir, CRASH_REPORTS_DIR)
}
return crashDir return crashDir
} }
/**
* 获取所有日志文件列表
*/
fun getLogFiles(): List<String> { fun getLogFiles(): List<String> {
return context.fileList() return appContext.fileList()
.filter { it.startsWith(FILE_PREFIX) && it.endsWith(FILE_EXTENSION) } .filter { it.startsWith(FILE_PREFIX) && it.endsWith(FILE_EXTENSION) }
//.map { it.replace(FILE_EXTENSION, "") }
} }
/** enum class LogLevel { DEBUG, INFO, WARN, ERROR }
* 日志级别枚举
*/
enum class LogLevel {
DEBUG, INFO, WARN, ERROR
}
} }
@@ -74,6 +74,9 @@ class HomeV3Activity : BaseActivity() {
private val viewModel: NetViewModelV3 by viewModels() private val viewModel: NetViewModelV3 by viewModels()
/** 记录每台秤的上次重量(克),用于跳过无变化的 UI 刷新和日志 */
private val lastWeightGrams = mutableMapOf<Int, Int>()
private fun initScaleManager() { private fun initScaleManager() {
lifecycleScope.launch { lifecycleScope.launch {
val ok = ScaleManager val ok = ScaleManager
@@ -81,6 +84,9 @@ class HomeV3Activity : BaseActivity() {
.onHeartbeat { _, data -> .onHeartbeat { _, data ->
data.weights.forEach { weight -> data.weights.forEach { weight ->
val pos = scaleIndexToPosition[weight.index] ?: return@forEach val pos = scaleIndexToPosition[weight.index] ?: return@forEach
// 重量不变则跳过 UI 刷新、日志和 EventBus,避免主线程无效计算
if (lastWeightGrams[weight.index] == weight.grams) return@forEach
lastWeightGrams[weight.index] = weight.grams
val model = list[pos] val model = list[pos]
model.weight = weight.grams / 1000.0 model.weight = weight.grams / 1000.0
shelfAdapter.notifyItemChanged(pos) shelfAdapter.notifyItemChanged(pos)
@@ -55,6 +55,11 @@ class ShelfV3Activity : BaseActivity() {
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss" const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
} }
/** 缓存的日期格式化器,避免 onClockTick 每秒 new 对象 */
private val clockFormatter by lazy {
SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
}
private lateinit var binding: ActivityShelfV3Binding private lateinit var binding: ActivityShelfV3Binding
private var shelfModel: SlotModel? = null private var shelfModel: SlotModel? = null
@@ -327,8 +332,7 @@ class ShelfV3Activity : BaseActivity() {
} }
override fun onClockTick() { override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA) val arr = clockFormatter.format(Date()).split("***")
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0]) updateLeftStatus(arr[0])
updateRightStatus(arr[1]) updateRightStatus(arr[1])
} }
@@ -42,10 +42,6 @@ class ShelfV3Adapter(list: MutableList<SlotModel>) :
// 格口编号 // 格口编号
binding.tvShelfName.text = item.slotNo.padStart(2, '0') binding.tvShelfName.text = item.slotNo.padStart(2, '0')
// // 临期预警(slotStatus=2)显示过期图标
// if (item.slotStatus.toInt() == 2) binding.ivStaleFood.visible()
// else binding.ivStaleFood.gone()
val weightG = item.weight ?: 0.0 val weightG = item.weight ?: 0.0
binding.tvFoodWeight.run { binding.tvFoodWeight.run {
setTextColor(getColor(R.color.food_weight_orange)) setTextColor(getColor(R.color.food_weight_orange))