Android Room按日/月/年聚合会话数据并计算指标平均值
嘿,这个需求我之前做健康数据可视化项目的时候刚好碰过,给你梳理几个落地的步骤,应该能帮到你~
核心是利用SQLite的日期函数,结合Room的DAO来实现按日/月/年的分组统计。假设你的实体结构大概是这样:
Session:包含id、startTime(建议存Long型毫秒数,或者用TypeConverter转LocalDateTime)- 关联表比如
StepRecord、BloodPressureRecord、SpO2Record,都通过sessionId关联到Session
那我们可以直接在DAO里写分组查询语句:
按日统计的DAO方法
@Dao interface SessionDao { @Query(""" SELECT -- 把毫秒转成秒后用date()函数提取日期字符串(格式:YYYY-MM-DD) date(s.startTime / 1000, 'unixepoch') as date, AVG(st.stepCount) as avgSteps, AVG(bp.systolic) as avgSystolic, AVG(bp.diastolic) as avgDiastolic, AVG(sp.level) as avgSpO2 FROM Session s -- 用LEFT JOIN避免某个Session没有某类数据时被过滤掉 LEFT JOIN StepRecord st ON s.id = st.sessionId LEFT JOIN BloodPressureRecord bp ON s.id = bp.sessionId LEFT JOIN SpO2Record sp ON s.id = sp.sessionId -- 按日期分组 GROUP BY date(s.startTime / 1000, 'unixepoch') -- 按时间升序排列,方便图表展示 ORDER BY date ASC """) suspend fun getDailyStats(): List<DailyStats> }
按月份/年份统计的变种
只需要把date()函数换成strftime()来提取不同的时间粒度:
- 按月份:
strftime('%Y-%m', s.startTime / 1000, 'unixepoch')(格式:YYYY-MM) - 按年份:
strftime('%Y', s.startTime / 1000, 'unixepoch')(格式:YYYY)
对应的DAO方法可以复制上面的,修改分组字段和返回的数据类即可。
需要一个数据类来接收上面查询的结果,比如按日统计的:
data class DailyStats( val date: String, // 格式:YYYY-MM-DD val avgSteps: Double?, // 可能为null,如果当天没有步数数据 val avgSystolic: Double?, val avgDiastolic: Double?, val avgSpO2: Double? )
如果是按月/年统计,只需要把类名改成MonthlyStats/YearlyStats,date字段对应不同的格式就行。
如果你的Session.startTime用的是LocalDateTime而不是Long,记得给Room加TypeConverter来转换:
class DateTimeConverter { @TypeConverter fun fromLocalDateTime(dateTime: LocalDateTime): Long { return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() } @TypeConverter fun toLocalDateTime(timestamp: Long): LocalDateTime { return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDateTime() } }
然后在你的RoomDatabase类上加上@TypeConverters(DateTimeConverter::class),这样Room就能自动处理LocalDateTime和Long的转换了。
拿到统计数据后,就可以转换成你用的图表库(比如MPAndroidChart、Jetpack Compose的Canvas/第三方图表库)需要的格式了。
比如用MPAndroidChart的话,把每日平均步数转成Entry列表:
// 假设你已经拿到了dailyStats列表 val stepEntries = dailyStats.mapNotNull { stats -> stats.avgSteps?.let { // X轴用日期的索引,或者直接用日期字符串当标签 Entry(stats.date.toXAxisIndex().toFloat(), it.toFloat()) } } // 然后把Entry设置给LineDataSet,再添加到Chart里 val stepDataSet = LineDataSet(stepEntries, "平均步数") // 这里可以设置线条颜色、点样式等 stepDataSet.color = Color.BLUE stepDataSet.setCircleColor(Color.BLUE) val data = LineData(stepDataSet) binding.lineChart.data = data binding.lineChart.invalidate()
如果是用Compose的图表库(比如CoilChart),逻辑类似,把数据转换成库要求的模型即可。
默认的查询只会返回有Session的日期/月份/年份,如果你的图表需要连续显示时间(比如某天没有数据也要显示0),可以自己补全缺失的区间:
比如补全每日的缺失日期:
fun fillMissingDailyStats( existingStats: List<DailyStats>, startDate: LocalDate, endDate: LocalDate ): List<DailyStats> { val statsMap = existingStats.associateBy { LocalDate.parse(it.date) } val filledStats = mutableListOf<DailyStats>() var currentDate = startDate while (!currentDate.isAfter(endDate)) { val stats = statsMap[currentDate] ?: DailyStats( date = currentDate.toString(), avgSteps = 0.0, avgSystolic = 0.0, avgDiastolic = 0.0, avgSpO2 = 0.0 ) filledStats.add(stats) currentDate = currentDate.plusDays(1) } return filledStats }
这样处理后,图表就能显示连续的时间轴,不会有断层啦~
内容的提问来源于stack exchange,提问作者Joe Jankowiak

