You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Room按日/月/年聚合会话数据并计算指标平均值

嘿,这个需求我之前做健康数据可视化项目的时候刚好碰过,给你梳理几个落地的步骤,应该能帮到你~

第一步:先搞定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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:21:10