Android原生开发体重管理APP:Room+WorkManager实战 📅 发布时间:2026/9/16 14:24:50 👁 浏览次数: 简介本资源是一份面向高校移动应用开发课程学生的Android实践项目聚焦体重管理类APP的完整实现方案适用于Java语言基础扎实、正学习Android四大组件与UI开发的初学者进阶训练。压缩包共459个文件含30个核心Java源码、210个编译后class文件、96个布局与配置XML、46张图标PNG资源以及可直接安装的app-debug.apk和课程报告、答辩PPT等教学材料整体体积18.31MB结构清晰便于分模块理解MVC架构与Android Studio兼容Eclipse开发流程。已有350人学习下载读者可获得从环境搭建含SDK/AVD配置说明、代码调试、APK打包到课程汇报的全流程支撑材料尤其适合课程设计、毕业实训及Android入门项目复现。1. 为什么一个“体重管理APP”要从Android原生开发起步很多刚学移动开发的同学拿到“体重管理APP”这个课题时第一反应是找现成模板、拖拽UI、调用第三方SDK——结果交作业时发现数据不准、单位混乱、历史曲线跳变、BMI计算逻辑被硬编码进XML里连修改身高字段都要重编译。这不是功能缺陷而是对Android平台特性的误判。真正的体重管理场景核心不在界面有多炫而在本地数据一致性、传感器精度控制、后台任务可靠性这三点。比如用户晨起空腹称重后立刻记录系统必须拒绝重复提交5分钟内同体重值不入库又比如夜间自动同步到健康平台时若网络中断需在下次Wi-Fi连接后补传而非丢弃——这些都不是Flutter或React Native默认能兜住的边界。本实践聚焦Android原生开发链路用Room做本地持久化防丢数、WorkManager调度每日晨间提醒、ConstraintLayoutMaterial3组件构建可读性强的BMI卡片所有代码基于Android Studio Giraffe2023.2.1稳定版适配Android 12API 31及以上。适合已掌握Java/Kotlin基础、正从课程设计迈向真实项目交付的开发者。2. 用Android Studio搭建体重管理APP最小可运行骨架2.1 创建项目并锁定关键依赖版本新建Empty Activity项目时务必选择Minimum SDK为API 21Android 5.0而非默认的API 23。原因在于小米/华为等厂商旧机型仍大量运行Android 5.x而体重管理类APP的核心功能本地存储、通知、传感器在API 21已完备强行升到API 23会丢失约12%的国内存量设备覆盖。在app/build.gradle中声明以下依赖注意版本号与Android Studio Giraffe兼容dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 // Room数据库替代SQLite原始API implementation androidx.room:room-runtime:2.6.1 implementation androidx.room:room-ktx:2.6.1 kapt androidx.room:room-compiler:2.6.1 // 后台任务调度替代已废弃的AlarmManager implementation androidx.work:work-runtime-ktx:2.9.0 // 数据绑定避免findViewById冗余代码 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 }提示kapt插件必须启用在app/build.gradle顶部添加plugins { id kotlin-kapt }否则Room注解处理器无法生成DAO实现类编译时会报Unresolved reference: UserDao_Impl错误。2.2 定义体重数据实体与数据库结构体重管理的核心是时间戳数值元数据三元组而非简单数字。创建WeightEntry.kt实体类强制要求recordTime为毫秒级Long类型非Date对象避免时区转换歧义Entity(tableName weight_entries) data class WeightEntry( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name weight_kg) val weightKg: Double, // 精确到0.1kg ColumnInfo(name height_cm) val heightCm: Int, // 整数厘米值 ColumnInfo(name record_time) val recordTime: Long, // System.currentTimeMillis() ColumnInfo(name is_manual) val isManual: Boolean true, // true用户手动录入false蓝牙秤自动同步 ColumnInfo(name note) val note: String // 如晨起空腹 )对应DAO接口WeightDao.kt需提供按日期范围查询能力后续绘制周趋势图必需Dao interface WeightDao { Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(entry: WeightEntry): Long Query(SELECT * FROM weight_entries WHERE record_time BETWEEN :startTime AND :endTime ORDER BY record_time ASC) suspend fun getEntriesByTimeRange(startTime: Long, endTime: Long): ListWeightEntry Query(SELECT * FROM weight_entries ORDER BY record_time DESC LIMIT 1) suspend fun getLatestEntry(): WeightEntry? Query(DELETE FROM weight_entries WHERE id :id) suspend fun deleteById(id: Long) }2.3 初始化Room数据库并验证表结构在Application子类中初始化数据库避免Activity中重复创建关键点在于设置允许主线程查询仅限调试阶段正式版需用协程切线程class WeightApp : Application() { lateinit var database: WeightDatabase override fun onCreate() { super.onCreate() database Room.databaseBuilder( applicationContext, WeightDatabase::class.java, weight_database ) .allowMainThreadQueries() // 仅调试期开启正式版删除此行 .fallbackToDestructiveMigration() // 开发期快速迭代用上线前移除 .build() } }验证表是否创建成功在Logcat中过滤Room关键字正常应看到Created table: weight_entries日志。若出现table weight_entries already exists警告说明迁移策略生效此时需删除应用数据重试Settings → Apps → Your App → Storage → Clear Data。3. 实现BMI计算与可视化从公式到Material3卡片3.1 BMI计算逻辑封装与单位校验BMI公式为体重(kg) / 身高(m)²但用户输入常混用单位。在BMIUtils.kt中强制校验输入合法性object BMIUtils { fun calculateBMI(weightKg: Double, heightCm: Int): Double? { if (weightKg 0.0 || weightKg 300.0) return null // 体重超限 if (heightCm 50 || heightCm 280) return null // 身高超限 val heightM heightCm / 100.0 return kotlin.math.roundToLong(weightKg / (heightM * heightM) * 100.0) / 100.0 } fun getBMICategory(bmi: Double): String { return when { bmi 18.5 - 偏瘦 bmi 24.0 - 正常 bmi 28.0 - 超重 else - 肥胖 } } }注意calculateBMI返回Double?而非Double迫使调用方处理null如弹Toast提示身高/体重值异常杜绝静默失败。3.2 构建BMI状态卡片ConstraintLayout Material3动态色在activity_main.xml中用ConstraintLayout实现响应式卡片关键属性com.google.android.material.card.MaterialCardView android:idid/bmiCard android:layout_width0dp android:layout_heightwrap_content app:cardCornerRadius12dp app:cardElevation4dp app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintBottom_toBottomOfparent androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp TextView android:idid/bmiValue android:layout_widthwrap_content android:layout_heightwrap_content android:text22.4 android:textSize24sp android:textStylebold app:layout_constraintTop_toTopOfparent app:layout_constraintStart_toStartOfparent / TextView android:idid/bmiCategory android:layout_widthwrap_content android:layout_heightwrap_content android:text正常 android:textColor?attr/colorPrimary app:layout_constraintTop_toBottomOfid/bmiValue app:layout_constraintStart_toStartOfparent / !-- 动态色绑定根据BMI分类自动切换card背景 -- View android:idid/bmiIndicator android:layout_width4dp android:layout_height0dp android:background?attr/colorPrimary app:layout_constraintTop_toTopOfid/bmiCategory app:layout_constraintBottom_toBottomOfid/bmiCategory app:layout_constraintStart_toStartOfparent / /androidx.constraintlayout.widget.ConstraintLayout /com.google.android.material.card.MaterialCardView在MainActivity.kt中动态更新卡片颜色private fun updateBMICard(bmi: Double) { val category BMIUtils.getBMICategory(bmi) bmiValue.text bmi.toString() bmiCategory.text category // 根据分类设置indicator颜色需在colors.xml预定义colorBmiNormal等 val colorRes when (category) { 偏瘦 - R.color.colorBmiUnderweight 正常 - R.color.colorBmiNormal 超重 - R.color.colorBmiOverweight else - R.color.colorBmiObese } bmiIndicator.setBackgroundColor(ContextCompat.getColor(this, colorRes)) }3.3 周趋势图表用MPAndroidChart绘制折线图添加依赖implementation com.github.PhilJay:MPAndroidChart:v3.1.0后在布局中嵌入LineChartcom.github.mikephil.charting.charts.LineChart android:idid/weekChart android:layout_width0dp android:layout_height200dp app:layout_constraintTop_toBottomOfid/bmiCard app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintBottom_toBottomOfparent /绘制逻辑需处理X轴日期格式化避免显示毫秒值private fun setupWeekChart(entries: ListWeightEntry) { val entriesList entries.map { entry - Entry(entry.recordTime.toFloat(), entry.weightKg.toFloat()) }.sortedBy { it.x } // 按时间排序 val dataSet LineDataSet(entriesList, 本周体重).apply { setDrawValues(false) lineWidth 2f color ContextCompat.getColor(thisMainActivity, R.color.primary) setCircleColor(ContextCompat.getColor(thisMainActivity, R.color.primary)) circleRadius 4f } val chart weekChart.apply { description.isEnabled false xAxis.position XAxis.XAxisPosition.BOTTOM xAxis.valueFormatter object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return SimpleDateFormat(MM/dd, Locale.getDefault()).format(Date(value.toLong())) } } axisLeft.axisMinimum (entries.minOfOrNull { it.weightKg } ?: 0.0) - 1.0 axisRight.isEnabled false legend.isEnabled false } chart.data LineData(dataSet) chart.invalidate() // 触发重绘 }4. 后台任务与数据同步WorkManager调度晨间提醒与自动备份4.1 创建每日晨间提醒Worker体重管理的关键动作是晨起记录需在每天7:00触发通知。创建MorningReminderWorker.ktclass MorningReminderWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val now Calendar.getInstance() if (now.get(Calendar.HOUR_OF_DAY) 7 now.get(Calendar.HOUR_OF_DAY) 8) { // 发送通知需在AndroidManifest.xml声明POST_NOTIFICATIONS权限 sendMorningNotification() } return Result.success() } private fun sendMorningNotification() { val intent Intent(context, MainActivity::class.java).apply { flags Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } val pendingIntent PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_IMMUTABLE ) val notification NotificationCompat.Builder(context, morning_channel) .setContentTitle(⏰ 该记录今日体重啦) .setContentText(晨起空腹称重后点击此处快速录入) .setSmallIcon(R.drawable.ic_weight) .setContentIntent(pendingIntent) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) with(NotificationManagerCompat.from(context)) { notify(1, notification.build()) } } }4.2 注册周期性WorkRequest并处理Android 12限制在MainActivity.onCreate()中注册Worker注意Android 12对后台启动Activity的限制private fun scheduleMorningReminder() { val constraints Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 确保联网时才触发 .build() val workRequest PeriodicWorkRequestBuilderMorningReminderWorker(15, TimeUnit.MINUTES) .setConstraints(constraints) .build() WorkManager.getInstance(this).enqueueUniquePeriodicWork( morning_reminder, ExistingPeriodicWorkPolicy.KEEP, workRequest ) }关键参数说明15, TimeUnit.MINUTES实际最小间隔为15分钟Android系统强制非精确到7:00整点ExistingPeriodicWorkPolicy.KEEP避免重复注册导致多个Worker实例setRequiredNetworkType防止无网时弹出无效通知4.3 自动备份到外部存储适配Android 11分区存储Android 11起禁止直接写入/storage/emulated/0/android/data/目录需使用MediaStore或Storage Access Framework。本实践采用MediaStore保存CSV备份private fun backupToCsv() { val resolver contentResolver val values ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, weight_backup_${System.currentTimeMillis()}.csv) put(MediaStore.MediaColumns.MIME_TYPE, text/csv) put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS /WeightApp/) } val uri resolver.insert(MediaStore.Files.getContentUri(external), values) uri?.let { outputUri - resolver.openOutputStream(it)?.use { outputStream - // 写入CSV头和数据省略具体write逻辑 outputStream.write(date,weight_kg,height_cm,note\n.toByteArray()) } } }5. 真机调试与常见问题定位adb shell与Logcat实战技巧5.1 快速验证数据库内容不用第三方工具查Room表当怀疑数据未写入时避免安装SQLite浏览器直接用adb命令# 进入设备shell adb shell # 切换到应用私有目录包名需替换为你的实际包名 cd /data/data/com.yourpackage.weightapp/databases/ # 列出数据库文件 ls -l # 使用sqlite3命令行查看表结构 sqlite3 weight_database PRAGMA table_info(weight_entries); # 查询最新10条记录注意时间戳转为可读格式 sqlite3 weight_database SELECT datetime(record_time/1000, unixepoch), weight_kg, height_cm FROM weight_entries ORDER BY record_time DESC LIMIT 10;提示若提示sqlite3: not found说明设备未预装sqlite3常见于华为/小米精简ROM此时改用adb shell run-as com.yourpackage.weightapp cat databases/weight_database | hexdump -C查看二进制内容确认文件非空即可。5.2 解决WorkManager不触发的三大高频原因现象检查项命令/操作Worker完全不执行是否在Application.onCreate()中调用scheduleMorningReminder()在onCreate()首行加Log.d(WORK, Scheduling...)验证通知不显示Android 12是否授予POST_NOTIFICATIONS权限adb shell dumpsys notification周期性任务被系统休眠设备是否启用省电模式adb shell dumpsys battery查看mCharging和mDischarging状态5.3 BMI计算结果偏差排查表当用户反馈计算结果与医院报告不符时按此顺序验证检查点验证方法正确值示例输入单位用户输入身高是否为厘米非米175cm → 1.75m时间戳精度record_time是否为System.currentTimeMillis()171234567890113位BMI公式是否用weight / (height/100)^2而非weight / height^265kg / (175/100)² 21.22四舍五入是否保留2位小数非截断21.2222→21.22非21.22在BMIUtils.calculateBMI中插入日志Log.d(BMI_DEBUG, weight$weightKg, height$heightCm, heightM${heightCm/100.0}, result${weightKg/(heightM*heightM)})然后用adb logcat -s BMI_DEBUG实时捕获计算过程。本文还有配套的精品资源点击获取