简介:基于Android的个人健康管理系统——健康管家,是一份面向计算机专业学生与实战学习者的毕业设计源码及文档说明,适用于课程设计或期末大作业。系统围绕个人健康管理搭建了用户信息管理、健康数据记录、运动跟踪、饮食管理、健康提醒、数据分析报告与个性化设置等模块,并涉及Android界面布局、本地数据库存储、第三方SDK集成和智能穿戴数据同步,能够帮助开发者掌握从需求拆分、界面搭建到数据持久化的完整开发流程。压缩包共210个文件、14.33MB,主要包含57个Java源码、79个XML资源、32个PNG及8个JPG图片资源,以及JAR/SO依赖库、Gradle构建配置和说明文档,目录结构清晰,按功能模块划分便于阅读和二次开发。项目已经严格调试,可导入运行,目前已有68人学习下载。读者可获得完整可运行的Android工程、配套文档说明和典型功能实现思路,通过研读源码能深入理解健康数据录入、图表统计、提醒机制等关键逻辑,对毕业设计答辩、课程实践和Android进阶开发都有直接帮助。
1. 一个能答辩、能跑通、能被导师追问的健康管家:毕业设计到底在做什么
拿到这套源码,第一反应基本都是解压、导入工程、跑起来,先把界面看一遍。但真正决定它能不能过答辩的,从来不是界面多漂亮,而是数据能不能自洽、逻辑能不能扛住追问、演示能不能在真机上稳定跑完。Android个人健康管理系统这类应用,核心不是“花哨”,而是把身高、体重、血压、步数、用药记录这些散数据,组织成一个能查询、能统计、能展示的闭环。它适合要交Android毕业设计或课程设计的同学,也适合想快速搭一个可演示原型的开发者。照着这套思路做下来,你得到的不只是一个能运行的App,还有一套说得清的数据库设计和一套答得上来的逻辑说明。
2. 先把数据层立住:自建健康档案表与Room数据库的接入
2.1 为什么毕业设计别急着接第三方SDK
很多同学一上来就搜“健康管理SDK”“云存储”,觉得把数据扔到云端显得高级。但毕设场景里,接第三方SDK反而把自己坑了。云服务要注册账号体系,账号体系要手机验证码,这等于把论文核心从“你做了什么”变成“你调用了什么”,答辩时一句“请在无网环境演示”就能让整套方案卡壳。尤其线下答辩的教室网络经常不稳定,依赖云端的功能一旦超时,演示体验直接崩。
常见做法是本地SQLite,进一步用Room封装。Room是Jetpack官方ORM,android studio新建工程时直接能拉依赖,编译期做SQL校验,表结构写错了立刻报错,比裸写SQLiteHelper友好太多。选Room的另一个理由在数据量:个人健康记录一天最多几十条,一年也就几万条,SQLite单表在十万级记录下加索引后毫秒级返回,足够满足毕设演示。反过来,如果上MySQL,老师会追问“客户端直连数据库的密码放哪了”“服务器挂了怎么办”,这些问题比功能本身还难答。所以,本地数据库加Room封装,是“健康管家”最稳妥的技术底座。
2.2 用Room建四张核心表:SQL脚本可以直接抄
健康管家需要的最小表集合是四张,我一般这么设计:
- user_info:用户档案,存身高、体重、出生年、性别等基础指标
- health_record:健康记录,存血压、心率、体重,每条带测量时间
- step_record:步数记录,按天存一个总步数,便于做日统计
- medication_reminder:用药提醒,药名、剂量、时间、是否已服用
对应SQL如下,直接拷进Room的预创建脚本或sqlite3命令行都能跑:
CREATE TABLE user_info (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT NOT NULL DEFAULT '健康管家用户',
gender INTEGER NOT NULL DEFAULT 0,
birth_year INTEGER NOT NULL DEFAULT 2000,
height_cm REAL NOT NULL DEFAULT 170.0,
weight_kg REAL NOT NULL DEFAULT 60.0,
create_time INTEGER NOT NULL
);
CREATE TABLE health_record (
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
systolic INTEGER NOT NULL DEFAULT 0,
diastolic INTEGER NOT NULL DEFAULT 0,
heart_rate INTEGER NOT NULL DEFAULT 0,
weight_kg REAL NOT NULL DEFAULT 0,
measure_time INTEGER NOT NULL,
note TEXT DEFAULT '',
FOREIGN KEY(user_id) REFERENCES user_info(user_id) ON DELETE CASCADE
);
CREATE TABLE step_record (
step_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
step_count INTEGER NOT NULL DEFAULT 0,
record_date INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES user_info(user_id) ON DELETE CASCADE
);
CREATE TABLE medication_reminder (
reminder_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
med_name TEXT NOT NULL,
dosage TEXT DEFAULT '',
remind_time INTEGER NOT NULL,
is_taken INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES user_info(user_id) ON DELETE CASCADE
);
说明:时间戳统一用 System.currentTimeMillis() 写入,排序、算差值、按天聚合都方便;外键统一挂 user_id,这样后面按人统计时一条SQL就够。gender 用 0/1 代替字符串,避免中文编码细节在导出文档时出幺蛾子。record_date 存的是当天零点的毫秒时间戳,而不是日期字符串,这样比较“今天”时直接用区间查询,不用做字符串格式化。
接着写Kotlin实体类,以 health_record 为例:
@Entity(tableName = "health_record")
data class HealthRecord(
@PrimaryKey(autoGenerate = true)
val recordId: Long = 0,
val userId: Long = 1,
val systolic: Int = 0,
val diastolic: Int = 0,
val heartRate: Int = 0,
val weightKg: Double = 0.0,
val measureTime: Long = System.currentTimeMillis(),
val note: String = ""
)
这里有个细节:实体字段用驼峰,表列名用下划线,Room 映射时用 @ColumnInfo(name = "record_id") 指定也行,但我会在项目初始化阶段就统一成“实体字段名=表列名=数据类字段名”三对齐,后面写 DAO 时SQL一眼能看懂,不用来回对着实体找列名。实体里 recordId 给默认值 0,是配合 autoGenerate 的习惯写法,插入时传 0 表示交给自增。
数据库类如下:
@Database(
entities = [UserInfo::class, HealthRecord::class, StepRecord::class, MedicationReminder::class],
version = 1,
exportSchema = false
)
abstract class HealthDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun healthDao(): HealthDao
abstract fun stepDao(): StepDao
abstract fun reminderDao(): ReminderDao
companion object {
@Volatile
private var INSTANCE: HealthDatabase? = null
fun getInstance(context: Context): HealthDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
HealthDatabase::class.java,
"health_manager.db"
).build().also { INSTANCE = it }
}
}
}
参数说明:version=1 是首次建库,后面加字段要升级 version 并写 Migration,这一点第 4 章专门讲;exportSchema=false 是关闭 schema 导出,毕设不需要生成 json 文件。databaseBuilder 里传 applicationContext,防止 Activity 引用泄漏,这也是 Android 官方一直强调的写法。
2.3 从DAO到Repository:ViewModel不直接碰数据库
直接让 ViewModel 持有数据库操作句柄,短期没问题,但后面做单元测试、换假数据源都会变得很难搞。常见做法是中间夹一层 Repository。以步数记录为例,DAO 接口:
@Dao
interface StepDao {
@Insert
suspend fun insertStep(record: StepRecord): Long
@Query("SELECT * FROM step_record WHERE user_id = :userId AND record_date = :date")
suspend fun getStepByDate(userId: Long, date: Long): StepRecord?
@Query("SELECT * FROM step_record WHERE user_id = :userId ORDER BY record_date DESC LIMIT :limit")
suspend fun getRecentSteps(userId: Long, limit: Int): List<StepRecord>
}
Repository 层:
class StepRepository(private val stepDao: StepDao) {
suspend fun addStepCount(userId: Long, date: Long, count: Int) {
val existing = stepDao.getStepByDate(userId, date)
if (existing == null) {
stepDao.insertStep(
StepRecord(userId = userId, stepCount = count, recordDate = date)
)
} else {
// 同一天多次计步回调,做累加而不是覆盖
val updated = existing.copy(
stepCount = existing.stepCount + count
)
stepDao.updateStep(updated)
}
}
}
注意这里的逻辑:传感器是多次回调的,直接 insert 会造成同一天多条记录,后面统计周报时数据会翻倍。所以每次回调都先查当天记录,有则更新,没有才插入。这个 case 在答辩被追问“你的步数是怎么去重的”时能直接答上。Repository 做数据聚合,ViewModel 通过协程调用:
viewModelScope.launch {
stepRepository.addStepCount(1, startOfToday, acceleratorSteps)
}
参数说明:startOfToday 用 LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() 生成,注意带上系统时区,否则在东八区晚上 23 点,“今天”的边界会错位到第二天凌晨。这个时区 bug 很隐蔽,最好是写成一个工具函数,并且用单元测试固定住。
2.4 依赖注入用ServiceLocator就够:别为毕设上Hilt
很多模板工程一上来就是 Hilt、Koin、DataStore 全套,结果 gradle 同步半小时,版本冲突改一天。毕设不需要这些。用最简单的 ServiceLocator 单例:
object ServiceLocator {
lateinit var healthDatabase: HealthDatabase
val stepRepository: StepRepository by lazy {
StepRepository(healthDatabase.stepDao())
}
fun init(context: Context) {
healthDatabase = HealthDatabase.getInstance(context)
}
}
在 Application.onCreate 里调 ServiceLocator.init(this) ,Activity 里直接取 ServiceLocator.stepRepository 。这个方案够用、好解释,而且答辩被问“为什么不用 Hilt”时,可以说“当前工程模块少,构造链直接可见,避免注解注入带来黑匣子”。这句话比背一遍 Hilt 的定义有说服力得多。Hilt 的价值在模块多、依赖关系复杂的工程里才体现得出来,毕设这种两三个页面的工程上了 Hilt 反而是给自己加负担。
2.5 首启初始化与演示数据预填充:让导师打开App就有内容可看
空数据库的 App 演示效果很差,首页一打开全是 0,导师会觉得功能没做完。常见做法是在 Room 建库回调里插入一条默认用户和几天的演示健康记录:
Room.databaseBuilder(context, HealthDatabase::class.java, "health_manager.db")
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 首启建库时插入默认用户,必须放到子线程
Executors.newSingleThreadExecutor().execute {
db.execSQL(
"INSERT INTO user_info(nickname, gender, birth_year, height_cm, weight_kg, create_time) " +
"VALUES('演示用户', 0, 2000, 170.0, 62.0, ${System.currentTimeMillis()})"
)
}
}
}).build()
为什么要用 Callback 而不是在主界面里插?因为建库时机正好在第一次访问数据库时触发,Callback 保证这段逻辑只执行一次,不会因为页面重建而重复插入。这里的关键坑是 execSQL 不能在主线程跑,否则首启会卡顿甚至触发 StrictMode 崩溃。插入的数据量控制在几十条以内,不要生成几百条,否则首启耗时明显,答辩现场会很尴尬。
3. 功能模块怎么拆:从计步器到BMI计算器的Android实现
3.1 数据采集:SensorManager与Activity生命周期的绑定
健康管家最核心的采集点是步数。Android 这边有两类计步传感器:TYPE_STEP_COUNTER 返回开机以来的累计步数,重启手机后清零,适合长期跟踪;TYPE_STEP_DETECTOR 每走一步回调一次,适合实时步频计算。常见做法是用 TYPE_STEP_COUNTER,每次取当前累计值减掉上次存档值,差值就是这段时间的真实步数。
class StepSensorManager(
private val context: Context
) : SensorEventListener {
private val sensorManager =
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var stepCounterSensor: Sensor? =
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
private var lastTotalSteps = 0f
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_STEP_COUNTER) {
val total = event.values[0]
val delta = (total - lastTotalSteps).toInt().coerceAtLeast(0)
lastTotalSteps = total
onStepDelta(delta)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
这里的 lastTotalSteps 必须在 onResume 里重新赋值,否则 Activity 从后台回来,累计值已经变了很多,差值会被重复累加:
override fun onResume() {
super.onResume()
lastTotalSteps = 0f
sensorManager.registerListener(
listener,
stepCounterSensor,
SensorManager.SENSOR_DELAY_NORMAL
)
}
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(listener)
}
参数说明:SENSOR_DELAY_NORMAL 适合计步场景,100 毫秒左右的延迟足够,SENSOR_DELAY_FASTEST 会让 CPU 频繁唤醒,耗电而且没必要。关键是 onPause 必须和 onResume 成对出现,只注册不注销的话,页面退到后台传感器还在回调,内存泄漏的提示会直接出现在 Logcat 里,答辩演示时长一长,手机发烫就解释不清了。
3.2 数据展示:列表、进度条与九宫格功能入口
列表页用 RecyclerView,每个 item 是一天健康记录,右侧一个 ProgressBar 显示步数距离目标(默认 8000 步)的完成度。这个“进度条+列表”的组合在 android studio 里布局预览很方便,直接在 xml 里写死宽度调试,跑起来再用真实数据撑开:
<ProgressBar
android:id="@+id/progressStep"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="8000"
android:progress="0"
app:layout_constraintWidth_percent="0.7" />
Adapter 里绑定数据时:
fun bind(record: StepRecord) {
binding.progressStep.progress = record.stepCount.coerceAtMost(8000)
binding.textStep.text = "${record.stepCount} / 8000 步"
}
为什么用 coerceAtMost?因为 ProgressBar 的 max 是 8000,步数超过上限不截断会显示异常进度;而文本里要保留真实数值,所以标准做法是“文本显示真实值,进度条只负责视觉比例”。这个细节很小,但连续三天步数超过 8000 时,进度条满格后多出来的部分会往右溢出,布局就乱了。
首页功能区可以用 GridView 做九宫格入口,对应“计步”“健康档案”“用药提醒”“统计图表”四个模块,图标用 androidx.appcompat 自带的资源或一张本地图片就行。九宫格的好处是模块边界清晰,答辩讲页面结构时一句“功能区是九宫格入口”就能带过,不用花时间解释跳转逻辑。每个入口跳对应 Activity,用 Intent 显式跳转,千万不要用隐式 Intent 加 action 字符串,否则装到别的设备上找不到 Activity 直接崩。
3.3 BMI计算与健康建议:阈值规则比模型实在
BMI = weight(kg) / height(m)^2,这个计算器几乎每个健康类毕设都有,容易掉进两个坑:一是身高在界面上填的是 cm,公式里忘除以 100;二是体重输入异常值没有拦截。计算函数:
fun calculateBmi(weightKg: Double, heightCm: Double): Double {
require(heightCm > 0) { "身高必须大于0" }
val heightM = heightCm / 100.0
return weightKg / (heightM * heightM)
}
fun bmiAdvice(bmi: Double): String {
return when {
bmi < 18.5 -> "偏瘦,建议增加优质蛋白摄入"
bmi < 24.0 -> "体重正常,继续保持"
bmi < 28.0 -> "偏胖,建议每周3次有氧运动"
else -> "肥胖风险,建议控制热量并咨询营养师"
}
}
阈值用国内成人标准:18.5 以下偏瘦,18.5~23.9 正常,24~27.9 偏胖,28 以上肥胖。这些值是写死在代码里的,如果想做成“可配置”,就把它们搬进 companion object 常量,导出文档时解释“阈值做成常量是为了后续调整”。输入校验在 UI 层做:体重限 20~200kg,身高限 80~250cm,超出范围直接 Toast 提示,不进计算逻辑。校验写完补一个单元测试,把边界值 18.5、24、28 各测一遍,论文里可以写“核心算法经过边界测试”,这也是加分项。
3.4 编辑、删除与撤销:把操作做成闭环
健康记录的编辑是 EntryActivity 带 recordId 跳转,删除用 AlertDialog 确认:
fun confirmDelete(record: HealthRecord) {
AlertDialog.Builder(context)
.setTitle("删除这条记录?")
.setMessage("删除后不可恢复,确定继续?")
.setPositiveButton("删除") { _, _ ->
viewModel.deleteRecord(record)
}
.setNegativeButton("取消", null)
.show()
}
如果想让项目看起来更有工程感,可以加一层软删除:health_record 表加一个 deleted 字段,默认 0,删除时置 1,查询时过滤 deleted = 0。这不算过度设计,因为归档数据能保住统计口径一致性,比如删除一条体重记录会影响 BMI 走势图。对毕设来说,软删除代码量不大,却能在论文“技术亮点”里占一段,性价比很高。硬删除和软删除的取舍一句话就能讲清楚:硬删除适合聊天记录这类不需要追溯的数据,健康数据有连续性,软删除更合适。
4. 毕业设计验收高频翻车点:步数归零、闪退与数据迁移
这一章是血泪经验区,身边的同组同学和往年答辩现场我都见了一遍。
4.1 血泪经验一:传感器权限开了却不回调,步数永远是0
现象:真机上运行计步页面,onSensorChanged 一次都不触发,步数一直显示 0。
原因:TYPE_STEP_COUNTER 这类计步传感器不需要运行时权限,但模拟器基本不配备真实计步硬件。大部分 Android Studio 模拟器只提供虚拟加速度计,TYPE_STEP_COUNTER 返回 null,代码直接注册就会静默失败。
解决:注册前先判空,空则降级。标准做法是先尝试 TYPE_STEP_COUNTER,拿不到就换 TYPE_STEP_DETECTOR,再不行就提示“当前设备不支持计步,请手动输入”。手动输入这个兜底入口非常关键,演示时设备不给力,还能靠手动数据走完流程。
4.2 翻车现场二:选头像时FileProvider路径崩溃
现象:点击头像从相册选图,照片选完返回时 App 直接闪退,Logcat 报 FileProvider 相关的 SecurityException,路径形如 content://com.tencent.mobileqq.sharefileprovider/external_files/... 或 provider 路径不存在。
原因:Android 13 起分区存储收紧,旧代码里用 FileProvider.getUriForFile 拼接 content:// 路径时,URI 的 authority 或 path 和 Manifest 里配置对不上,直接把文件路径塞进 URI 返回会炸。
解决:头像选择直接用 Activity Result API 的 GetContent,返回的 uri 不需要 FileProvider 介入:
private val pickImage =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null) {
binding.avatar.setImageURI(uri)
}
}
binding.avatar.setOnClickListener {
pickImage.launch("image/*")
}
这个写法不需要在 Manifest 里配 FileProvider,也不用手动申请相册权限。如果你维护的老工程仍然要用 FileProvider,注意两点:Manifest 里配置的 path 要和代码 getUriForFile 传入的路径严格一致;Android 11 之后不允许直接访问 Android/data 目录,不配置会直接报 NoSuchFile。选头像这个功能看着小,却是答辩现场翻车概率最高的环节。
4.3 踩坑实录三:升级App后数据库直接崩,Room迁移没写
现象:开发到后期改了表结构,加了血糖字段,直接安装新版本打开 App 崩溃,报错关键词是 SQLiteException: no such column: blood_glucose。
原因:Room 版本号没升,或者 version 升了但没写 Migration。Room 的机制很严格:版本相同但表结构变了,安装时会校验失败;版本不同却没有迁移策略,它会直接抛 IllegalStateException,而不是静默清库。
解决:版本号加一,再写迁移:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE health_record ADD COLUMN blood_glucose REAL NOT NULL DEFAULT 0"
)
}
}
然后在 databaseBuilder 里加 .addMigrations(MIGRATION_1_2) 。注意 ALTER TABLE 只能加列,改列名或删列要新建表并把旧数据 copy 过去,不能直接 ALTER。如果赶时间,也有 fallbackToDestructiveMigration() 一键清库重来,但代价是用户数据全部丢失。毕设演示前可以用,答辩时绝对不要提这招,老师一句“用户数据怎么办”就把方案击穿了。
4.4 玄学问题四:模拟器上定位漂移、步数乱跳,真机却正常
现象:模拟器上报的位置和传感器数据忽快忽慢,步数有时一下涨几百,过了几秒又掉回去;同样的代码放到真机上就稳定。
原因:模拟器本身没有真实传感器和 GPS 硬件,位置由桌面控制台虚拟注入,步数传感器用虚拟数据源,读数没有真实物理事件驱动,自然不稳定。
解决:上下文里区分模拟器和真机,检测方式看 Build.FINGERPRINT,常见模拟器特征包含 generic、emulator、sdk_gphone 这些字段。演示时建议全程用真机,并且提前走两圈让设备产生几组真实步数,再切换到演示模式。模拟器只用于截图和写文档。也可以给源码加一个“演示模式”开关,打开后用固定数据填充,文档里注明“演示模式用于界面验证,正式数据仍走传感器和数据库”。这个开关写出来不是造假,是测试技巧。
4.5 演示前的5分钟自检清单
列几条这些年攒下的检查项,照着做一遍能避开大部分翻车:
- 卸载重装一次,验证首启初始化能插入默认演示数据,二次启动不重复插入
- 关闭开发者选项里的“不保留活动”,从 Launcher 冷启动,确保首页不会白屏
- Logcat 过滤
AndroidRuntime,确认没有任何 fatal exception - 把手机切到飞行模式再开 WiFi 关掉,验证离线可用,避免答辩教室网络抽风
- 提前准备好一条“预填充演示数据”的截图,万一现场真机连不上,横竖有备图能讲
5. 让健康管家“会说话”:健康评分、演示数据与验证技巧
5.1 生成可复现的模拟数据:曲线不再锯齿
真实用户演示时体重曲线忽高忽低不好看,用固定种子的 Random 生成 30 天平滑数据:
val random = Random(42)
val baseWeight = 62.0
val weights = (0 until 30).map { dayOffset ->
baseWeight + (random.nextDouble() - 0.5) * 1.2
}
seed 固定为 42,每次生成结果完全一样,重启 App 曲线不变。答辩时可以说这是固定种子随机生成器生成的演示数据,用于验证 UI,正式数据走 Room 持久化。这个说辞比“我录了假数据”有说服力得多。随机种子是编程课里的常用概念,导师不会觉得这是偷懒,反而会认为你懂可控实验。
5.2 健康评分:把身体指标折算成一个进度条分数
一个可解释性强的评分规则比玄学模型更讨喜。设计成 100 分制:BMI 在正常范围记 40 分,每偏离一级扣 8 分;步数达标率记 30 分;用药按时率记 30 分:
fun healthScore(
bmi: Double,
stepRatio: Double,
medTakenRate: Double
): Int {
val bmiScore = when {
bmi in 18.5..24.0 -> 40
bmi in 24.0..28.0 -> 32
else -> 24
}
val stepScore = (stepRatio.coerceIn(0.0, 1.0) * 30).toInt()
val medScore = (medTakenRate.coerceIn(0.0, 1.0) * 30).toInt()
return (bmiScore + stepScore + medScore).coerceIn(0, 100)
}
阈值写成常量而不是埋在条件里,方便后续调整。这个算法放主页顶部,配合一个横向进度条显示分数,导师打开 App 第一眼就能看到“健康度”这个核心概念。论文里解释权重设定理由:BMI 反映长期状态,权重最高;步数反映日常活动量;用药依从性反映慢病管理质量,三者覆盖了健康管理的三个维度。这一段至少能撑一页 Paper。
5.3 用统一Logcat过滤验证数据链路
演示前总是手忙脚乱找问题,多半是日志打得太散。统一一个 TAG,所有关键链路都往里打:
private const val TAG = "HealthManager"
Log.d(TAG, "step: sensor=null, fallback to manual input")
Log.d(TAG, "db: user created=true, userId=1")
Log.d(TAG, "score: bmi=$bmi, stepRatio=$stepRatio, score=$score")
验证时一条命令过滤:
adb logcat -s HealthManager:D
只看这个 TAG 的输出,数据从传感器到数据库到 UI 展示全链路一目了然。如果日志顺序乱了,就说明某一层的异步没串好,优先排查协程调度;如果某条日志根本没打出来,说明上游已经短路,问题在采集层。这套验证方法陪我熬过了很多次验收前夜,比盯着界面猜问题高效得多。
我个人的习惯是:每次改完数据层代码,先把旧版本的数据备份出来,再升级安装跑一遍迁移,日志确认无误后才会动 UI。这个习惯救过我至少三次,一次是表格加字段,一次是改主键策略,还有一次是切换计步传感器类型。把验证链路养成交给工具,而不是交给临场反应,项目就稳了。希望帮到你。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_33910191/article/details/169924039



