简介:本资源是一套完整的Android平台个人健康管理应用毕业设计解决方案,面向计算机、软件工程等专业本科生,助力毕业设计选题落地与实践能力提升。系统涵盖健康数据记录、运动统计、饮食管理、提醒服务等核心功能,代码规范、结构清晰,已通过实际测试并获导师认可,可直接用于答辩与提交。压缩包共253个文件,含57个Java业务逻辑文件、79个XML界面布局与资源定义文件、32个PNG图标素材、15个SO本地库及Gradle构建配置等,完整覆盖安卓开发全链路,总大小14.31MB。目前已有40人学习下载,适合需要参考成熟架构、理解MVVM或MVC分层实践、掌握SQLite本地存储与Activity生命周期管理的学生。资源附带详细技术文档,包含需求分析、数据库设计、模块说明与部署指南,便于快速理解整体设计思路与关键实现细节。
1. 项目概述:从零构建一个Android健康管家
最近几年,身边关注健康数据的朋友越来越多,从简单的步数记录到睡眠监测、心率跟踪,大家似乎都希望有一个“数字健康助手”。作为一名在移动开发领域摸爬滚打多年的开发者,我观察到很多计算机相关专业的同学在做毕业设计时,都会选择“健康管理”这个方向。它既有实际应用价值,技术栈又涵盖了Android开发的多个核心模块,是个非常不错的练手项目。今天,我就以“Android平台健康管家系统”这个典型的毕业设计课题为例,和大家深入聊聊如何从零开始,构建一个功能完整、架构清晰、代码可维护的个人健康管理应用。这不仅仅是一份源码和文档的罗列,更是一次完整的项目实战复盘,我会分享在开发过程中遇到的技术选型思考、架构设计权衡以及那些文档里不会写的“踩坑”经验。
这个系统本质上是一个运行在Android手机上的个人健康数据中枢。它的核心目标是帮助用户便捷地记录、追踪、分析自己的健康指标,比如每日步数、睡眠时长、体重变化、心率、血压(模拟数据或蓝牙设备接入)等,并通过图表等形式进行可视化展示,最终给出简单的健康建议。对于开发者而言,完成这样一个项目,你需要串联起Android的UI设计、数据存储、后台服务、图表绘制、权限管理、甚至可能涉及与硬件(如手环)的蓝牙通信等多个知识点。下面,我们就来一步步拆解这个系统的设计与实现。
2. 系统核心功能与架构设计
2.1 核心需求解析与功能模块划分
在动手写代码之前,明确需求是第一步。一个合格的“健康管家”,至少应该具备以下核心功能模块:
- 用户与数据管理:这是应用的基石。包括用户注册登录、个人基本信息(年龄、身高、性别)的维护。更重要的是健康数据的增删改查,例如记录某天的体重、手动添加一次运动记录等。
- 健康数据采集:
- 自动采集:利用手机传感器,最典型的就是通过
SensorManager和StepCounter/StepDetector传感器实现步数统计。这是体现Android系统能力的关键点。 - 手动录入:提供友好的表单,让用户可以输入体重、血压、睡眠时长、饮水量、饮食记录等传感器无法直接获取的数据。
- 外部设备接入(进阶):通过蓝牙
BluetoothGATT协议,连接智能手环或体重秤,同步心率、睡眠详情等数据。这对于毕业设计而言是一个很好的加分项。
- 自动采集:利用手机传感器,最典型的就是通过
- 数据可视化与分析:枯燥的数字没有意义。我们需要使用图表库(如
MPAndroidChart)将历史数据绘制成折线图、柱状图,让用户直观地看到趋势变化(如体重变化曲线、每周运动量对比)。 - 健康提醒与目标管理:应用应具备一定的主动性。例如,利用
AlarmManager或WorkManager实现定时提醒喝水、久坐起身、服药等。同时,允许用户设置每日步数目标、体重目标,并在主页展示完成进度。 - 数据备份与安全:用户的数据是宝贵的。需要考虑如何将本地数据备份到云端(如使用Firebase Firestore)或导出为文件(如JSON或CSV格式)。同时,涉及个人敏感信息,要注意数据本地存储的加密(例如使用
EncryptedSharedPreferences或SQLCipher)。
基于以上功能,我们可以设计一个典型的分层架构,我个人在项目中采用的是清晰的三层架构,这能让代码结构更清晰,便于后期扩展和维护。
2.2 技术栈选型与架构设计
前端(Android客户端)技术选型:
- 开发语言与IDE:毫无疑问是
Kotlin+Android Studio。Kotlin的空安全、扩展函数等特性能极大提升开发效率和代码健壮性。Android Studio则是官方指定、生态最完善的工具。 - UI框架:使用
Jetpack Compose是现代Android开发的主流趋势,它声明式的写法效率极高。但如果考虑到部分学校教学可能仍基于View体系,或者为了兼容更广泛的参考资料,使用XML布局 +ViewBinding也是完全可行的成熟方案。本项目解析将基于View体系,因为其相关资源更丰富,原理更易于理解。 - 架构模式:强烈推荐
MVVM。ViewModel负责管理UI相关的数据,LiveData或StateFlow负责通知UI更新,Repository作为单一数据源,协调本地数据库和远程数据。这能有效避免Activity/Fragment变得臃肿,并提高可测试性。 - 异步处理:使用
Kotlin Coroutines。协程让异步代码写得像同步一样简洁,是处理数据库操作、网络请求的首选。 - 图表库:
MPAndroidChart功能强大、文档丰富,是绘制健康数据趋势图的不二之选。 - 数据库:本地持久化首选
Room。它是SQLite的抽象层,编译时检查SQL语句,用起来非常安全省心。
后端(可选,用于数据同步与备份)技术选型:对于毕业设计,如果时间精力有限,可以侧重客户端,云端备份作为进阶功能。如果要做,为了快速原型开发,我推荐使用Firebase平台(Firestore数据库、Authentication认证),它提供了Android端完善的SDK,无需自建服务器。如果想更深入后端技术,可以用Spring Boot+MySQL+阿里云/腾讯云服务器搭建一套简单的RESTful API。
整体架构图(逻辑层面):
用户界面层 (UI Layer) ├── Activity/Fragment (使用ViewBinding/DataBinding) └── 使用ViewModel观察数据 领域逻辑层 (Domain Layer) ├── Repository (数据仓库,决策数据来源:本地 or 网络) ├── Use Cases (交互器,封装复杂业务逻辑,毕业设计可简化) 数据层 (Data Layer) ├── 本地数据源:Room Database (存储健康记录、用户配置) ├── 本地数据源:SharedPreferences/EncryptedSharedPreferences (存储简单配置) └── 远程数据源:Firebase Firestore API 或 自建后端API (用于备份同步)注意:在项目初期,不要过度设计。对于第一个版本,可以先将
Repository和UseCase合并,重点保证功能跑通。架构是为效率和维护性服务的,而不是束缚。
3. 核心模块实现详解与踩坑实录
3.1 数据基石:使用Room构建本地数据库
健康数据的核心是存储。我们设计几个主要的实体类:
// 用户基本信息实体 @Entity(tableName = "user_profile") data class UserProfile( @PrimaryKey val uid: String = "default_user", val name: String = "", val gender: String = "", val birthDate: Long = 0L, // 使用时间戳存储 val height: Int = 0, // 厘米 val weightGoal: Float = 0f // 目标体重 ) // 健康记录实体,使用单表继承或分别建表。这里以单表为例,用type字段区分。 @Entity(tableName = "health_record") data class HealthRecord( @PrimaryKey(autoGenerate = true) val id: Long = 0, val type: String, // "STEP", "WEIGHT", "HEART_RATE", "SLEEP", "WATER" val value: Float, // 记录值 val unit: String, // 单位 val date: Long, // 记录日期(时间戳,方便按天聚合) val note: String = "" // 备注 )接下来定义Dao接口。这里有个关键技巧:查询最近7天的步数数据用于图表展示。
@Dao interface HealthRecordDao { @Insert suspend fun insert(record: HealthRecord) @Query("DELETE FROM health_record WHERE id = :id") suspend fun delete(id: Long) // 查询特定类型、特定日期的记录 @Query("SELECT * FROM health_record WHERE type = :type AND date = :date LIMIT 1") suspend fun getRecordByDate(type: String, date: Long): HealthRecord? // **关键查询**:获取最近N天某种类型的数据,用于绘制折线图 @Query(""" SELECT date, SUM(value) as totalValue FROM health_record WHERE type = :type AND date >= :startDate GROUP BY date ORDER BY date ASC """) fun getRecordsByTypeSinceDate(type: String, startDate: Long): Flow<List<DateValuePair>> // 返回一个Flow,这样UI可以实时响应数据变化 } // 定义一个数据类来接收聚合查询的结果 data class DateValuePair(val date: Long, val totalValue: Float)实操心得与避坑指南:
- 主键设计:像
HealthRecord这类记录,使用autoGenerate = true的ID作为主键是最简单的。不要用date和type的复合主键,因为同一天可能有多条同类型记录(如多次心率测量)。- 日期处理:存储日期时,我强烈建议使用
Long类型的时间戳(System.currentTimeMillis())。这比存yyyy-MM-dd字符串更便于计算和比较(例如,计算7天前的日期:val sevenDaysAgo = System.currentTimeMillis() - 7 * 24 * 3600 * 1000L)。在UI层显示时,再用SimpleDateFormat或java.time包格式化成易读的字符串。- 聚合查询:上面
getRecordsByDateSince查询中的GROUP BY date是关键。它能把同一天的所有记录(比如步数)汇总成一条数据,这正是折线图需要的。SUM(value)根据类型可能是求和(步数),也可能是求平均值(体重),业务逻辑需要在Repository层处理。- 使用Flow观察数据:
Dao方法返回Flow<List<T>>,这样在ViewModel中收集这个Flow,就能在数据库变化时自动更新UI图表,实现响应式。
3.2 自动采集核心:步数传感器与后台服务
自动计步是健康应用的亮点。Android提供了STEP_COUNTER和STEP_DETECTOR两种传感器。
- STEP_COUNTER:从设备开机以来累积的步数。重启后重置。适合读取一次获取总步数。
- STEP_DETECTOR:检测到单一步伐时触发一次。适合实时监听。
实现一个计步服务StepCounterService,继承自Service,并在后台监听传感器:
class StepCounterService : Service() { private lateinit var sensorManager: SensorManager private var stepSensor: Sensor? = null private var lastSavedStepCount = 0L private var bootStepCount = 0L // 用于补偿STEP_COUNTER重启重置 override fun onCreate() { super.onCreate() sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) // 从数据库或SharedPreferences读取上次保存的步数和开机步数 loadStepState() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { stepSensor?.let { sensorManager.registerListener( sensorListener, it, SensorManager.SENSOR_DELAY_NORMAL // 根据功耗调整频率 ) } ?: run { // 设备不支持计步,可以考虑使用加速度传感器模拟(不准) stopSelf() } // 返回START_STICKY,服务被杀死后尝试重启 return START_STICKY } private val sensorListener = object : SensorEventListener { override fun onSensorChanged(event: SensorEvent?) { event?.let { if (it.sensor.type == Sensor.TYPE_STEP_COUNTER) { val currentSteps = it.values[0].toLong() // 处理设备重启:如果当前步数小于上次保存的,说明重启了 if (currentSteps < lastSavedStepCount) { bootStepCount = lastSavedStepCount } val actualStepsSinceBoot = currentSteps - bootStepCount // 计算本次采集新增的步数 val newSteps = actualStepsSinceBoot - lastSavedStepCount if (newSteps > 0) { // 保存到数据库 saveStepsToDatabase(newSteps, System.currentTimeMillis()) lastSavedStepCount = actualStepsSinceBoot saveStepState() // 发送广播或使用LiveData通知UI更新 sendStepUpdateBroadcast(newSteps) } } } } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} } private fun saveStepsToDatabase(steps: Long, timestamp: Long) { // 使用Room的DAO,在协程中插入数据 CoroutineScope(Dispatchers.IO).launch { val record = HealthRecord( type = "STEP", value = steps.toFloat(), unit = "steps", date = getStartOfDayTimestamp(timestamp), // 转换为当天0点时间戳 note = "Auto recorded" ) healthRecordDao.insert(record) } } // 工具函数:获取某时间戳当天0点的时间戳 private fun getStartOfDayTimestamp(timestamp: Long): Long { val calendar = Calendar.getInstance().apply { timeInMillis = timestamp } calendar.set(Calendar.HOUR_OF_DAY, 0) calendar.set(Calendar.MINUTE, 0) calendar.set(Calendar.SECOND, 0) calendar.set(Calendar.MILLISECOND, 0) return calendar.timeInMillis } }注意事项与深度解析:
- 后台保活与功耗:这是一个经典难题。在Android 8.0以上,后台服务限制非常严格。单纯使用
startService很容易被系统杀死。更优的方案是:
- 使用
WorkManager:安排一个周期性任务(例如每30分钟一次),在任务执行时读取STEP_COUNTER的当前值,与上次记录做差,得到这段时间的步数。WorkManager能适应不同的系统版本和省电策略,是谷歌推荐的后台任务调度器。- 前台服务:如果需要近乎实时的步数更新(如锁屏显示),可以启动一个前台服务,并显示一个持续的通知。这需要向用户请求
FOREGROUND_SERVICE权限,并在通知栏告知用户应用正在计步。- 设备兼容性:务必检查
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)是否返回null。很多老旧或低端设备可能不支持硬件计步。要有降级方案,例如提示用户,或引导其开启手动记录模式。- 数据准确性处理:
STEP_COUNTER的值在设备重启后会重置。我们的代码中通过bootStepCount和lastSavedStepCount来补偿这一点,这是保证数据连续性的关键逻辑。需要将bootStepCount持久化到SharedPreferences。- 日期归并:
saveStepsToDatabase函数中的getStartOfDayTimestamp非常重要。传感器数据是按事件上报的,但我们在数据库里需要按天汇总。所以,无论一天内触发多少次onSensorChanged,我们都将步数累加到date字段为当天0点时间戳的那条记录上。这需要先在数据库中查询是否已有当天的记录,有则更新value,无则插入。为了简化,上面的示例是直接插入,实际Repository层应实现一个insertOrUpdateStep方法。
3.3 数据可视化:MPAndroidChart的集成与定制
数据存储后,需要用图表展示。MPAndroidChart功能强大,但初次使用配置稍显繁琐。
1. 添加依赖:在app/build.gradle.kts中添加:
dependencies { implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") }2. 在布局文件中加入LineChart:
<com.github.mikephil.charting.charts.LineChart android:id="@+id/lineChart" android:layout_width="match_parent" android:layout_height="300dp" />3. 在Fragment或Activity中配置图表并设置数据:
private fun setupStepChart(stepDataList: List<DateValuePair>) { val lineChart: LineChart = binding.lineChart // 1. 基础样式配置 lineChart.description.isEnabled = false // 隐藏描述文字 lineChart.setTouchEnabled(true) // 允许触摸交互 lineChart.isDragEnabled = true lineChart.setScaleEnabled(true) lineChart.setPinchZoom(true) lineChart.setDrawGridBackground(false) // 2. 配置X轴(日期) val xAxis: XAxis = lineChart.xAxis xAxis.position = XAxis.XAxisPosition.BOTTOM xAxis.granularity = 1f // 最小间隔为1 xAxis.labelCount = 7 // 显示7个标签 xAxis.valueFormatter = object : ValueFormatter() { // 将时间戳转换为“MM/dd”格式的日期 private val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault()) override fun getFormattedValue(value: Float): String { return dateFormat.format(Date(value.toLong())) } } xAxis.setDrawGridLines(false) // 不绘制网格线 // 3. 配置左侧Y轴(步数) val leftAxis: YAxis = lineChart.axisLeft leftAxis.axisMinimum = 0f // 从0开始 leftAxis.setDrawGridLines(true) leftAxis.granularity = 1000f // 步数间隔1000 leftAxis.valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return "${value.toInt()}步" } } lineChart.axisRight.isEnabled = false // 关闭右侧Y轴 // 4. 准备图表数据 val entries = ArrayList<Entry>() stepDataList.forEachIndexed { index, pair -> // X轴是索引(0,1,2...),Y轴是步数值。也可以直接用时间戳做X轴,但格式化更复杂。 entries.add(Entry(index.toFloat(), pair.totalValue)) } val dataSet = LineDataSet(entries, "七日步数趋势") dataSet.color = Color.BLUE dataSet.valueTextColor = Color.GRAY dataSet.lineWidth = 2f dataSet.setCircleColor(Color.BLUE) dataSet.circleRadius = 4f dataSet.setDrawCircleHole(false) dataSet.mode = LineDataSet.Mode.CUBIC_BEZIER // 曲线模式 dataSet.fillColor = Color.BLUE dataSet.setDrawFilled(true) // 填充曲线下方区域 dataSet.fillAlpha = 50 val lineData = LineData(dataSet) lineChart.data = lineData // 5. 刷新图表 lineChart.invalidate() }图表绘制避坑指南:
- 数据倒序问题:从数据库查出的数据通常是按日期升序排列的,最早的在最前面。但在图表上,我们通常希望最近的日子在右边。你可以在准备
entries时,将列表反转,或者通过xAxis.valueFormatter在格式化时进行映射。- 性能优化:如果数据点很多(比如一年的每日数据),直接绘制会导致卡顿。
MPAndroidChart提供了setVisibleXRangeMaximum和setVisibleXRangeMinimum来控制显示范围,并支持手势缩放和滑动查看历史。对于大量数据,应考虑在业务层先做聚合(如按周、月汇总)再传给图表。- 空数据处理:如果某天没有数据,在折线图上会形成断裂。一种处理方式是,在查询数据库后,补全最近7天所有日期的数据,没有的用0填充,保证图表连续。
- 内存泄漏:
LineChart对象持有Context引用。务必在Fragment的onDestroyView或Activity的onDestroy中调用lineChart.clear()和lineChart.data = null,防止内存泄漏。
3.4 用户界面与交互:主页与数据录入
主页是应用的门面,需要清晰展示核心数据概览,如今日步数、体重、饮水杯数,以及近期的趋势图表。
使用ViewPager2+Fragment构建主页Tab是一种灵活的方式。例如,第一个Tab是“今日概览”,第二个Tab是“健康图表”,第三个Tab是“目标与提醒”。
数据录入界面的设计要点是便捷。对于频繁操作(如记录一杯水),可以设计一个大按钮。对于复杂数据(如血压),则使用标准的表单。这里以记录体重为例,展示一个包含数据验证的对话框:
private fun showRecordWeightDialog() { val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_record_weight, null) val editTextWeight = dialogView.findViewById<EditText>(R.id.editTextWeight) val textViewUnit = dialogView.findViewById<TextView>(R.id.textViewUnit) AlertDialog.Builder(requireContext()) .setTitle("记录体重") .setView(dialogView) .setPositiveButton("保存") { _, _ -> val weightStr = editTextWeight.text.toString() if (weightStr.isNotEmpty()) { val weight = weightStr.toFloatOrNull() if (weight != null && weight > 0 && weight < 300) { // 简单验证 viewModel.saveWeightRecord(weight) Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, "请输入有效的体重值", Toast.LENGTH_SHORT).show() } } } .setNegativeButton("取消", null) .show() }在对应的ViewModel中:
class HealthRecordViewModel(private val repository: HealthRepository) : ViewModel() { fun saveWeightRecord(weight: Float) { viewModelScope.launch(Dispatchers.IO) { val record = HealthRecord( type = "WEIGHT", value = weight, unit = "kg", date = getStartOfDayTimestamp(System.currentTimeMillis()), note = "" ) repository.insertHealthRecord(record) } } }UI/UX设计心得:
- 即时反馈:任何用户操作(点击按钮、保存数据)后,都应给予明确的反馈,如
Toast或Snackbar。在ViewModel中保存数据后,可以通过LiveData或StateFlow通知UI更新主页的概览数据。- 数据验证:客户端验证必不可少。像体重、血压这些数值,必须在输入时检查范围、格式,避免无效数据进入数据库。这能提升用户体验和数据质量。
- 状态管理:使用
ViewModel配合LiveData/StateFlow来管理UI状态。例如,主页的“今日步数”应该是一个LiveData<Int>,当后台服务更新数据库后,Repository通知ViewModel,ViewModel更新这个LiveData,UI自动刷新。这才是响应式架构的魅力。
4. 进阶功能与毕业设计亮点打造
4.1 实现蓝牙连接与外部设备数据同步
如果想让你的毕业设计脱颖而出,集成蓝牙功能是一个巨大的亮点。这里概述连接蓝牙心率带或手环的基本流程:
- 权限声明:在
AndroidManifest.xml中添加BLUETOOTH,BLUETOOTH_ADMIN,ACCESS_FINE_LOCATION(Android 12+需要BLUETOOTH_SCAN,BLUETOOTH_CONNECT)权限。注意,ACCESS_FINE_LOCATION是运行时权限,需要动态申请。 - 设备扫描:使用
BluetoothAdapter的startDiscovery()或更推荐的BluetoothLeScanner的startScan()方法扫描低功耗蓝牙设备。通过ScanCallback接收结果,过滤出目标设备(通过设备名称ScanRecord.deviceName或服务UUIDScanRecord.serviceUuids)。 - 设备连接:找到设备后,通过
BluetoothDevice.connectGatt(context, false, gattCallback)建立GATT连接。这里的gattCallback是核心,它处理连接状态变化和数据通信。 - 发现服务与特征:连接成功后,在
onServicesDiscovered回调中,遍历BluetoothGatt的服务列表,找到健康设备通用的服务UUID(如心率服务0x180D)。 - 订阅通知:找到对应服务的特征(如心率测量特征
0x2A37),调用gatt.setCharacteristicNotification(characteristic, true)并写入描述符CCCD来启用通知。这样,设备就会在心率数据更新时主动推送。 - 解析数据:在
onCharacteristicChanged回调中接收原始字节数据,根据蓝牙官方规范进行解析。例如,心率数据的第一个字节是标志位,后面跟着心率值。
蓝牙开发深度避坑:
- 主线程阻塞:所有蓝牙回调(
onCharacteristicChanged等)都发生在主线程。如果解析数据逻辑复杂,务必切换到后台线程,否则会导致UI卡顿。- 连接管理与重连:蓝牙连接非常不稳定。必须实现完善的连接状态监听(
onConnectionStateChange)和错误处理。建议使用autoconnect参数,并实现一个指数退避的重连机制。- 资源释放:在
Activity/Fragment销毁时,务必调用gatt.disconnect()和gatt.close(),释放蓝牙资源,否则会导致后续连接失败。- 协议差异:不同厂家、不同型号的设备,其服务UUID、特征值、数据格式可能不同。最好针对一到两款特定设备进行开发,并在文档中说明兼容性。
4.2 数据备份与恢复:Room数据库导出为JSON
数据是无价的。实现一个本地备份功能非常实用。思路是:将Room数据库中的关键表数据查询出来,序列化成JSON文件,保存到公共下载目录。
suspend fun backupDataToJson(context: Context): Boolean { return withContext(Dispatchers.IO) { try { val records = healthRecordDao.getAllRecordsForBackup() // 自定义查询所有记录 val userProfile = userProfileDao.getUserProfile() val backupData = BackupData( userProfile = userProfile, healthRecords = records, backupTime = System.currentTimeMillis() ) val gson = GsonBuilder().setPrettyPrinting().create() val jsonString = gson.toJson(backupData) // 保存到文件 val fileName = "health_backup_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())}.json" val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val backupFile = File(downloadsDir, fileName) backupFile.writeText(jsonString) // 通知系统扫描文件,使其出现在文件管理器中 MediaScannerConnection.scanFile(context, arrayOf(backupFile.absolutePath), null, null) true } catch (e: Exception) { Log.e("Backup", "Backup failed", e) false } } }恢复功能则是读取JSON文件,解析成对象,然后清空现有数据库(或合并),再批量插入新数据。务必在恢复前提醒用户,此操作会覆盖当前数据。
4.3 使用WorkManager实现智能提醒
AlarmManager在低版本上可靠,但在Android 6.0+的省电模式下可能失效。WorkManager是谷歌推荐的解决方案,它会在合适的时机(满足约束条件时)执行后台任务。
实现一个定时提醒喝水的周期性工作:
- 定义Worker:
class DrinkWaterReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { // 这里执行提醒逻辑,例如发送一个通知 showReminderNotification() // 可以在这里检查今天喝水记录是否达标,决定是否提醒 return Result.success() } private fun showReminderNotification() { val channelId = "water_reminder" // 创建通知渠道(Android O+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(channelId, "喝水提醒", NotificationManager.IMPORTANCE_DEFAULT) val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(applicationContext, channelId) .setSmallIcon(R.drawable.ic_water_drop) .setContentTitle("该喝水啦!") .setContentText("长时间未喝水,记得补充水分哦~") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) .build() NotificationManagerCompat.from(applicationContext).notify(NOTIFICATION_ID, notification) } companion object { const val NOTIFICATION_ID = 1001 } }- 安排周期性工作:
fun scheduleDailyWaterReminder(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) // 不需要网络 .setRequiresBatteryNotLow(true) // 电量不低时执行 .build() val dailyReminderRequest = PeriodicWorkRequestBuilder<DrinkWaterReminderWorker>( 4, TimeUnit.HOURS, // 每4小时重复一次 15, TimeUnit.MINUTES // 允许有15分钟的灵活执行窗口 ).setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "daily_water_reminder", ExistingPeriodicWorkPolicy.KEEP, // 如果已有同名任务,保持原有 dailyReminderRequest ) }可以在应用启动时(如MainActivity的onCreate或一个Application类中)调用scheduleDailyWaterReminder。WorkManager会持久化这个任务,即使应用被杀死或设备重启,任务也会在条件满足时重新调度。
5. 毕业设计文档撰写要点与源码组织
一份优秀的毕业设计文档和清晰的源码结构,能极大提升你的项目印象分。
源码组织建议(按功能模块分包):
com.yourname.healthmanager ├── data │ ├── local │ │ ├── dao │ │ ├── entity │ │ └── HealthDatabase.kt │ ├── repository │ └── model (或 domain) ├── di (依赖注入,如使用Hilt) ├── ui │ ├── main (主页相关) │ ├── chart (图表相关) │ ├── record (数据录入相关) │ ├── setting (设置相关) │ └── viewmodel ├── service (后台服务,如StepCounterService) ├── utils (工具类,日期处理、权限检查等) └── work (WorkManager的Worker类)毕业设计文档核心章节:
- 绪论:阐述项目背景、研究意义、国内外现状。
- 相关技术介绍:简要介绍Android、Kotlin、Room、MVVM、MPAndroidChart、蓝牙BLE等关键技术。
- 系统需求分析:功能性需求(用例图)、非功能性需求。
- 系统设计:总体架构图、模块设计、数据库设计(ER图)、关键类图。
- 系统实现:这是重点。挑选2-3个核心模块(如计步服务、图表展示、数据持久化)详细说明,配上关键代码截图和解释。务必加入你自己的思考和遇到的问题及解决方案。
- 系统测试:描述测试环境,展示功能测试截图(如不同界面、图表)、性能测试(内存、CPU占用)结果。
- 总结与展望:总结项目完成情况、个人收获,指出不足之处(如UI美化、设备兼容性)和未来可扩展的方向(如接入AI健康建议、社交功能)。
个人经验之谈:在文档的“系统实现”部分,不要只是贴代码。要用文字描述清楚“这段代码解决了什么问题”、“为什么这么设计”、“有没有考虑过其他方案”。例如,在讲解计步服务时,可以对比STEP_COUNTER和STEP_DETECTOR的优劣,解释为何选择WorkManager来调度任务。这些思考过程是区分普通项目和优秀项目的关键。
6. 常见问题排查与性能优化
在实际开发中,你一定会遇到各种问题。这里记录几个高频问题及其排查思路:
Q1:计步服务不准确或步数为零?
- 检查权限:确保在Android 10及以上版本,已经在运行时申请了
ACTIVITY_RECOGNITION(身体活动识别)权限。这是STEP_COUNTER和STEP_DETECTOR传感器所必需的。 - 检查传感器支持:在代码中加入日志,打印
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)的结果,确认设备硬件支持。 - 检查后台限制:在手机系统的“电池优化”或“应用启动管理”中,确保你的应用没有被限制后台活动。可以向用户引导如何手动设置。
- 模拟器问题:Android模拟器通常没有真实的传感器,步数可能一直为0或不变。务必在真机上测试此功能。
Q2:Room数据库升级时,原有数据丢失?
- 正确实现Migration:当你修改了
Entity结构(如增加字段),必须提升@Database注解中的version,并提供Migration对象。Room提供了fallbackToDestructiveMigration,但切勿在生产环境中使用,它会直接清空数据库。应该编写Migration脚本来添加新列或创建新表。val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE health_record ADD COLUMN new_column TEXT DEFAULT ''") } } - 备份先行:在进行任何数据库结构变更前,最好先实现一个备份功能,让用户有机会导出数据。
Q3:图表加载大量数据时卡顿?
- 数据聚合:如前所述,不要一次性将365个数据点塞给图表。在业务层先做聚合,例如,如果查看“年趋势”,可以按周或月计算平均值后再传给图表。
- 限制数据量:使用
lineChart.setVisibleXRange(10f, 10f)等方法,初始只显示一部分数据,让用户通过手势缩放查看细节。 - 关闭不必要的动画:
lineChart.animateX(1000)这类动画在数据量大时开销大。可以考虑移除或缩短动画时间。
Q4:应用在后台被系统杀死后,提醒不工作?
- 检查WorkManager约束:确保你的
PeriodicWorkRequest没有设置过于严格的约束(如setRequiresCharging(true)),导致条件一直不满足。 - 检查省电策略:不同手机厂商(小米、华为、OPPO等)有强力的后台清理策略。需要在应用内引导用户将应用加入“白名单”或“允许后台活动”。
- 使用前台服务:对于需要高可靠性、即时性的提醒(如用药提醒),可以考虑在提醒触发时启动一个短暂的前台服务来发送通知。但这会带来额外的功耗和通知栏常驻,需权衡使用。
性能优化小贴士:
- 数据库操作异步化:所有Room的
Dao操作都必须在非UI线程执行。使用协程的Dispatchers.IO是最佳实践。 - 图片资源优化:应用图标和界面图片使用
WebP格式,并放在合适的drawable目录下(如-hdpi,-xxhdpi)。 - 避免内存泄漏:在
Fragment中观察LiveData时,使用viewLifecycleOwner而非this。及时取消协程(在ViewModel的viewModelScope中启动的协程会自动管理)。在自定义View或使用Chart等第三方库时,注意在onDestroy中释放资源。
本文还有配套的精品资源,点击获取