ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android服药提醒APP开发:从需求分析到MVVM架构与AlarmManager实战

Android服药提醒APP开发:从需求分析到MVVM架构与AlarmManager实战 简介本资源是一套面向计算机专业本科生的Android毕业设计实战项目聚焦老年人健康管理场景解决独居或慢性病老人漏服、错服药物的现实问题。项目采用Android原生开发技术栈基于Java语言实现涵盖首页展示、个性化用药提醒设置、服药记录查询及个人中心等核心模块具备完整的前后端交互逻辑与本地数据管理能力。压缩包共5个文件包含前端源码、后台服务代码、数据库SQL脚本、详细使用说明文档及README指引总大小46.04MB结构清晰便于分模块学习与调试。已有296人下载学习适合Android初学者巩固Activity生命周期、AlarmManager定时机制、SQLite数据存储及UI适配等关键技术点亦可作为课程设计或毕设答辩的完整参考方案。1. 项目概述一个源于真实需求的毕业设计选题做毕业设计那会儿选题是个让人头疼的事儿。既要能体现大学四年所学又得有点实际价值不能是那种“为了做而做”的空中楼阁。当时我注意到家里的长辈特别是爷爷奶奶每天要吃好几种药时间、剂量都不一样全靠一个小药盒和记忆力经常漏服或者记不清吃没吃过。这让我意识到一个简单、易用、专门为老年人设计的服药提醒应用或许是个不错的切入点。它不只是一个简单的闹钟更是一个结合了Android开发、用户体验设计、数据持久化等多个知识点的综合性项目非常适合作为计算机或软件工程专业的毕业设计。这个“老年人服药提醒APP”的核心目标非常明确帮助认知能力可能下降、对智能手机操作不熟悉的老年用户准时、准确地服用药物。它不是一个功能繁杂的健康管理平台而是聚焦于“提醒”这一核心功能力求在交互上做到极致的简洁和友好。整个项目从需求分析、UI/UX设计、编码实现到测试打包完整地走了一遍软件开发的流程。最终我不仅完成了一个能实际运行的应用还整理出了详细的源码和使用说明打包成了那个经典的“(基于android的毕业设计)老年人服药提醒APP(源码使用说明).zip”文件。这份资料对于正在寻找毕设灵感或者想学习如何开发一个具有人文关怀的实用型APP的同学来说应该会很有帮助。2. 核心需求与设计思路拆解2.1 目标用户画像与核心痛点分析在动手写一行代码之前我们必须先搞清楚我们的用户是谁以及他们最需要什么。这个APP的目标用户是60岁以上的老年人他们可能具有以下特征视力可能老化字体太小、颜色对比度低、图标过于抽象都会造成阅读困难。操作不熟练对智能手机的触控操作如长按、滑动不熟悉容易误触。记忆力减退容易忘记是否吃过药或者记错服药时间。可能患有多种慢性病需要同时服用多种药物每种药物的服用周期如一天三次、隔天一次、剂量如1片、半片都可能不同。对复杂功能有抵触心理功能太多、界面太花哨会让他们感到困惑和害怕。基于以上分析我们提炼出几个最核心的需求痛点极简交互操作步骤尽可能少点击区域要大避免复杂的导航。清晰可视字体必须足够大关键信息如药名、时间要突出显示。强提醒能力提醒必须足够醒目和持久能穿透老年人的日常生活。服药记录与确认提供简单的“已服药”确认机制并保留历史记录解决“吃没吃过”的疑虑。家属协同可选但重要允许子女远程为父母添加药品和提醒并查看服药记录。注意很多同学做毕设容易陷入“技术炫技”的误区给老年人APP加上图表分析、社交分享等复杂功能这完全是本末倒置。我们的设计必须做减法一切以“降低认知负荷”和“减少操作步骤”为最高原则。2.2 技术栈选型与架构考量明确了需求接下来就要选择实现的技术方案。作为一个Android毕业设计技术选型需要在“体现技术能力”和“保证项目可控性”之间取得平衡。开发环境与语言毫无疑问Android Studio是官方且最主流的IDE。语言选择Kotlin是更优解。虽然很多教学资料仍用Java但Kotlin语法更简洁、空安全特性好能减少很多潜在的崩溃NullPointerException这对于一个需要稳定运行的提醒应用至关重要。在毕设答辩时使用Kotlin也能体现你对技术趋势的关注。架构模式推荐采用MVVMModel-View-ViewModel架构。这是目前Android官方推荐的应用架构。它将界面逻辑View与业务逻辑和数据ViewModel Model分离使得代码更清晰、更易于测试和维护。对于服药提醒这个业务逻辑相对清晰的项目MVVM能很好地管理药品数据、提醒时间和UI状态。数据持久化药品信息、服药计划、历史记录都需要本地存储。这里有两个主流选择Room Persistence Library它是SQLite的抽象层属于Android Jetpack组件与MVVM架构和LiveData/Flow能完美结合。它通过编译时检查SQL语句避免了大量模板代码是存储结构化数据如药品表、记录表的最佳实践。SharedPreferences适合存储简单的键值对比如用户设置是否开启声音提醒、默认提醒提前时间等。取舍虽然直接使用SQLite也能体现能力但Room更现代在毕设中采用能加分。因此核心数据药品、记录用Room简单配置用SharedPreferences是比较合理的方案。定时提醒实现这是项目的技术核心。我们不能用简单的Timer或Handler因为应用进程可能被系统回收。必须使用系统级的调度机制。AlarmManager这是传统的系统闹钟服务可以设置在特定时间触发即使应用未运行或设备休眠。我们可以用它来在设定的服药时间点触发一个通知Notification。WorkManager属于Jetpack组件用于处理可延迟的、保证执行的后台任务。它更适合处理非精确时间的任务如每天同步一次数据。对于精确到分钟的药物提醒AlarmManager仍然是更直接可靠的选择。前台服务Foreground Service如果希望提醒更“顽固”比如在提醒时持续播放声音直到用户操作可以考虑启动一个前台服务。但这会消耗更多资源且需要向用户申请“常驻通知”权限对老年人可能造成困扰。因此优先采用高优先级通知Priority: HIGH配合AlarmManager是更优雅的方案。2.3 功能模块设计根据核心需求我们将APP划分为以下几个主要模块药品管理模块添加/编辑药品输入药名、规格、图片可选方便识别、服用剂量、备注如“餐后服用”。药品列表以卡片或大列表形式展示所有药品一目了然。删除药品简单的删除操作最好有二次确认。服药计划模块与药品绑定设置提醒时间为每种药添加一个或多个每日固定提醒时间如08:00 12:30 19:00。设置服药周期每天、隔天、每周特定几天等。计划概览以时间线或日历形式展示今天的所有服药计划。提醒与通知模块到点触发在设定时间通过AlarmManager触发发送一个高优先级的系统通知。通知内容清晰显示药名、剂量、服用说明。通知操作按钮至少包括“已服用”和“稍后提醒”如10分钟后。提醒方式震动、响铃可自定义铃声且铃声应持久直到用户处理。服药记录模块自动记录用户点击通知的“已服用”后自动生成一条带有时间戳的记录。手动补录允许用户手动添加过去某次的服药记录应对忘记点击的情况。记录查看按日或按药品查看历史记录界面简洁。设置模块通用设置默认提醒提前时间、是否震动、铃声选择。数据管理备份与恢复服药数据可导出为JSON文件。3. 核心功能实现细节与避坑指南3.1 数据库设计与Room实战数据库是整个APP的数据基石设计的好坏直接影响后续开发的复杂度。实体Entity设计我们至少需要两张表Medicine药品和MedicineRecord服药记录。Medicine表包含药品的静态信息MedicineRecord表则记录每次服药的动作。// Medicine.kt Entity(tableName medicines) data class Medicine( PrimaryKey(autoGenerate true) val id: Long 0, val name: String, // 药品名称 val dosage: String, // 剂量如“一次一片” val imageUri: String? null, // 药品图片本地URI可选 val note: String? null // 备注 ) // MedicineRecord.kt Entity(tableName medicine_records, foreignKeys [ForeignKey( entity Medicine::class, parentColumns [id], childColumns [medicineId], onDelete ForeignKey.CASCADE // 药品删除时关联记录也删除 )] ) data class MedicineRecord( PrimaryKey(autoGenerate true) val id: Long 0, val medicineId: Long, // 关联的药品ID val takenTime: Long, // 服药时间戳System.currentTimeMillis() val isTakenOnTime: Boolean true // 是否准时服用点击通知 vs 手动补录 )数据访问对象DAO设计DAO定义了操作数据库的方法。这里的关键是处理药品与记录的关系查询。// MedicineDao.kt Dao interface MedicineDao { Insert suspend fun insert(medicine: Medicine): Long Update suspend fun update(medicine: Medicine) Delete suspend fun delete(medicine: Medicine) Query(SELECT * FROM medicines ORDER BY name ASC) fun getAllMedicines(): FlowListMedicine // 使用Flow便于在UI中观察数据变化 // 查询某药品的所有记录 Query(SELECT * FROM medicine_records WHERE medicineId :medId ORDER BY takenTime DESC) fun getRecordsForMedicine(medId: Long): FlowListMedicineRecord }数据库类Database这是Room的入口需要定义数据库版本和包含的实体列表。// AppDatabase.kt Database(entities [Medicine::class, MedicineRecord::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { abstract fun medicineDao(): MedicineDao companion object { // 单例模式避免重复打开数据库 Volatile private var INSTANCE: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, medicine_reminder_db // 数据库文件名 ).build() INSTANCE instance instance } } } }实操心得Room的ForeignKey和onDelete CASCADE非常有用。它保证了数据的一致性当你删除一种药品时所有与之关联的服药记录会被自动清理避免了“孤儿记录”。另外DAO方法返回FlowListT而不是LiveDataListT是更现代的做法配合ViewModel中的stateFlow或sharedFlow能更好地处理UI状态。3.2 精准定时提醒AlarmManager的正确姿势实现定时提醒是项目的核心也是容易踩坑的地方。1. 设置闹钟我们需要为每个药品的每个提醒时间点设置一个独立的闹钟。关键是要生成一个唯一的requestCode通常可以用(药品ID * 100 时间索引)的方式来构造确保能精准取消或更新某个特定提醒。// ReminderScheduler.kt (工具类) object ReminderScheduler { fun scheduleReminder(context: Context, medicineId: Long, medicineName: String, triggerTimeMillis: Long) { val alarmManager context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent Intent(context, ReminderReceiver::class.java).apply { putExtra(medicine_id, medicineId) putExtra(medicine_name, medicineName) } // 使用PendingIntent并确保FLAG_UPDATE_CURRENT和唯一的requestCode val requestCode (medicineId.toString() triggerTimeMillis.toString()).hashCode() val pendingIntent PendingIntent.getBroadcast( context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // Android 12 需要FLAG_IMMUTABLE ) // 对于精确的提醒使用 setExactAndAllowWhileIdle (API 23) // 它即使在低电耗模式下也能工作是设置重要闹钟的推荐方式 if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, // 使用真实时间并唤醒设备 triggerTimeMillis, pendingIntent ) } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pendingIntent) } // 可以将这个requestCode存下来用于后续取消 } }2. 接收广播并发送通知创建一个BroadcastReceiver如ReminderReceiver来接收AlarmManager触发的广播并在这里构建和发送通知。// ReminderReceiver.kt class ReminderReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val medicineId intent.getLongExtra(medicine_id, -1L) val medicineName intent.getStringExtra(medicine_name) ?: 服药时间到 // 创建通知渠道 (Android 8.0 必需) createNotificationChannel(context) val notificationManager NotificationManagerCompat.from(context) // 构建一个点击后打开App的Intent val openAppIntent Intent(context, MainActivity::class.java).apply { flags Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } val pendingOpenAppIntent PendingIntent.getActivity( context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) // 构建“已服用”和“稍后提醒”的Action按钮 val takenAction NotificationCompat.Action.Builder( null, // 图标如果不需要可以传null但最好提供一个 已服用, getTakenPendingIntent(context, medicineId) // 这个PendingIntent会触发记录服药和取消本次提醒的逻辑 ).build() val snoozeAction NotificationCompat.Action.Builder( null, 10分钟后, getSnoozePendingIntent(context, medicineId, medicineName) // 这个PendingIntent会重新设置一个10分钟后的闹钟 ).build() // 构建通知 val notification NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_pill_notification) // 一个药丸图标 .setContentTitle(服药提醒) .setContentText(该服用$medicineName 了) .setPriority(NotificationCompat.PRIORITY_HIGH) // 高优先级 .setCategory(NotificationCompat.CATEGORY_REMINDER) .setContentIntent(pendingOpenAppIntent) // 点击通知主体打开App .setAutoCancel(true) // 点击后自动消失 .setOngoing(true) // 设置为持续通知直到用户操作慎用可能引起反感 .setSound(Settings.System.DEFAULT_NOTIFICATION_URI) // 使用默认通知音 .setVibrate(longArrayOf(0, 500, 200, 500)) // 震动模式等待0ms震动500ms暂停200ms再震动500ms .addAction(takenAction) .addAction(snoozeAction) .build() // 发送通知使用一个唯一的ID避免覆盖 notificationManager.notify(medicineId.toInt(), notification) } private fun createNotificationChannel(context: Context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val channel NotificationChannel( CHANNEL_ID, 服药提醒, NotificationManager.IMPORTANCE_HIGH // 高重要性会有声音和弹出 ).apply { description 用于接收服药时间提醒 enableVibration(true) vibrationPattern longArrayOf(0, 500, 200, 500) } val notificationManager context.getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) } } companion object { const val CHANNEL_ID medicine_reminder_channel } }避坑指南Android 8.0 (API 26) 以上必须创建通知渠道否则通知不会显示。渠道创建一次即可通常放在Application类或主Activity中。PendingIntent的Flag从Android 12开始必须指定FLAG_IMMUTABLE或FLAG_MUTABLE。对于大多数广播和Activity启动FLAG_IMMUTABLE是安全的。闹钟的精确性setExactAndAllowWhileIdle()是保证提醒准时触发的关键但它有触发频率限制每15分钟最多一次。对于服药提醒这完全足够。通知ID使用medicineId作为通知ID是个好方法这样同一个药品的重复提醒会更新通知而不是创建一堆。后台限制从Android 10开始后台启动Activity受到限制。我们的“打开App”的PendingIntent可能在某些情况下失效。更稳健的做法是点击通知后先启动一个前台服务再由服务启动Activity但这增加了复杂度。对于毕设说明你了解这个限制即可。3.3 为老年人设计的UI/UX实践界面是老年用户接触APP的唯一途径必须精心设计。1. 色彩与对比度背景与文字使用高对比度配色例如纯白色背景搭配深灰色#333333或黑色文字。避免使用浅灰文字。重要操作按钮使用鲜艳、明确的颜色如绿色确认/完成和红色删除/警告。确保色盲用户也能通过形状和文字区分。状态指示例如“已服药”用绿色打勾“未服药”用红色时钟图标清晰明了。2. 字体与布局字体大小所有正文文字至少使用18sp标题使用24sp或更大。在res/values/dimens.xml中定义尺寸常量便于全局调整。按钮尺寸遵循Material Design的触摸目标最小尺寸48dp x 48dp。我们的按钮应该更大例如高度设置为60dp。布局简化一个界面只做一个主要任务。添加药品的界面就只放名称、剂量、时间等必要输入框其他高级设置可以收起或放到二级页面。使用CardView用卡片将不同的药品或信息组隔开增加视觉层次感和可读性。3. 交互优化减少输入尽可能提供选择器而不是文本框。例如服药时间用TimePickerDialog剂量可以用下拉选择“1片”、“2片”、“半片”等。明确的反馈任何操作点击、长按都要有即时反馈如按钮的颜色变化、轻微的震动performHapticFeedback或简短的提示音。避免手势操作尽量不要依赖左滑删除、长按编辑等隐藏手势。为每个项目提供明显的“编辑”和“删除”按钮。语音输入辅助在输入药名时可以启用麦克风图标调用系统语音识别方便不擅长打字的用户。一个简单的药品列表项布局示例!-- item_medicine.xml -- androidx.cardview.widget.CardView android:layout_widthmatch_parent android:layout_heightwrap_content app:cardElevation4dp android:layout_margin8dp LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding16dp TextView android:idid/tvMedicineName android:layout_widthwrap_content android:layout_heightwrap_content android:textSize24sp !-- 大字体 -- android:textStylebold android:textColorcolor/primary_text_dark tools:text阿司匹林肠溶片 / TextView android:idid/tvMedicineDosage android:layout_widthwrap_content android:layout_heightwrap_content android:textSize20sp android:layout_marginTop8dp android:textColorcolor/secondary_text_dark tools:text剂量一次一片一日三次 / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop12dp !-- 大而明显的操作按钮 -- Button android:idid/btnEdit android:layout_width0dp android:layout_height60dp !-- 高触摸目标 -- android:layout_weight1 android:layout_marginEnd4dp android:textSize18sp android:text编辑 android:backgroundTintcolor/colorPrimary / Button android:idid/btnDelete android:layout_width0dp android:layout_height60dp android:layout_weight1 android:layout_marginStart4dp android:textSize18sp android:text删除 android:backgroundTintcolor/colorAccent / /LinearLayout /LinearLayout /androidx.cardview.widget.CardView4. 关键业务流程与代码实现串联4.1 从添加药品到设置提醒的完整流程让我们串联起一个核心用户旅程用户添加一种新药并设置提醒。1. 添加药品界面 (AddMedicineActivity/ViewModel):用户输入药名、剂量并点击“设置提醒时间”。这里我们跳转到一个专门设置时间的界面或者使用底部弹窗BottomSheetDialogFragment来添加多个时间点。2. 数据保存与闹钟设置当用户在时间设置界面点击“保存”后我们需要做两件事将药品存入数据库并为每个设置的时间点安排闹钟。// AddMedicineViewModel.kt class AddMedicineViewModel(private val medicineRepository: MedicineRepository) : ViewModel() { fun saveMedicine(medicine: Medicine, reminderTimes: ListLong) { viewModelScope.launch { // 1. 将药品存入数据库并获取自动生成的ID val newMedicineId medicineRepository.insertMedicine(medicine) // 2. 为每个提醒时间设置闹钟 reminderTimes.forEach { triggerTime - // 计算下一次触发的时间戳如果设置的是每天08:00需要计算出下一个08:00的具体时间戳 val nextTriggerTime calculateNextTriggerTime(triggerTime) ReminderScheduler.scheduleReminder( context getApplication(), medicineId newMedicineId, medicineName medicine.name, triggerTimeMillis nextTriggerTime ) // 3. 可选将提醒时间关系也存入数据库用于界面展示和下次计算 medicineRepository.insertReminderSchedule(newMedicineId, triggerTime) } } } private fun calculateNextTriggerTime(timeOfDay: Long): Long { // timeOfDay 是用户选择的“时分”转换成的毫秒数从当天00:00开始的偏移 val calendar Calendar.getInstance().apply { timeInMillis System.currentTimeMillis() set(Calendar.HOUR_OF_DAY, TimeUnit.MILLISECONDS.toHours(timeOfDay).toInt()) set(Calendar.MINUTE, (TimeUnit.MILLISECONDS.toMinutes(timeOfDay) % 60).toInt()) set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) } // 如果设置的时间已经过了今天就设置为明天 if (calendar.timeInMillis System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_YEAR, 1) } return calendar.timeInMillis } }3. 主界面展示今日计划 (MainActivity/ViewModel):主界面使用RecyclerView展示药品列表或者更友好地展示一个“今日服药计划”时间线。这需要查询数据库并筛选出今天有提醒的药品。// MainViewModel.kt class MainViewModel(private val medicineRepository: MedicineRepository) : ViewModel() { // 使用StateFlow来驱动UI更新 val todaySchedule: StateFlowListTodayScheduleItem medicineRepository.getTodaySchedule() .stateIn( scope viewModelScope, started SharingStarted.WhileSubscribed(5000), initialValue emptyList() ) } // TodayScheduleItem 是一个数据类包含了药品信息和对应的下一个提醒时间 data class TodayScheduleItem( val medicine: Medicine, val nextReminderTime: String, // 格式化后的时间如“08:00” val isTaken: Boolean // 今天是否已服用 )4.2 通知交互与状态更新当提醒通知弹出用户点击“已服用”按钮时需要完成以下逻辑1. 处理“已服用”点击我们在ReminderReceiver中为“已服用”按钮设置的PendingIntent应该指向一个服务如IntentService或另一个BroadcastReceiver用于在后台处理逻辑。// TakenActionService.kt class TakenActionService : IntentService(TakenActionService) { override fun onHandleIntent(intent: Intent?) { intent?.let { val medicineId it.getLongExtra(medicine_id, -1L) if (medicineId ! -1L) { // 1. 在数据库中插入一条服药记录 val record MedicineRecord( medicineId medicineId, takenTime System.currentTimeMillis(), isTakenOnTime true // 因为是点击通知服用的算准时 ) // 使用Room需要在协程或线程中执行 runBlocking { AppDatabase.getDatabase(this).medicineRecordDao().insert(record) } // 2. 取消当前药品的本次提醒闹钟避免重复提醒 // 我们需要知道是哪个具体的闹钟被触发这需要我们在设置闹钟时保存requestCode // 这里简化处理取消该药品所有今天的闹钟并设置明天的闹钟 cancelAndRescheduleReminder(medicineId) // 3. 更新通知可选将当前通知内容改为“已服用”或者直接取消通知 val notificationManager NotificationManagerCompat.from(this) notificationManager.cancel(medicineId.toInt()) } } } }2. 处理“稍后提醒”“稍后提醒”的逻辑相对简单就是取消当前闹钟并重新设置一个10分钟后的新闹钟。// 在ReminderScheduler中新增一个方法 fun scheduleSnooze(context: Context, medicineId: Long, medicineName: String, delayMinutes: Int) { val triggerTime System.currentTimeMillis() TimeUnit.MINUTES.toMillis(delayMinutes.toLong()) scheduleReminder(context, medicineId, medicineName, triggerTime) }5. 开发中常见问题与调试技巧5.1 AlarmManager不触发或不准时这是开发提醒类应用最常见的问题。问题闹钟到了时间没响。排查步骤检查PendingIntent确保PendingIntent的requestCode是唯一的。如果两个闹钟使用了相同的requestCode和PendingIntent后设置的会覆盖先设置的。使用(medicineId, time)组合生成唯一码。检查触发时间triggerTimeMillis必须是基于RTC_WAKEUP或ELAPSED_REALTIME_WAKEUP的正确时间戳。用Log.d()打印出你计算出的时间戳和当前时间戳进行对比。检查广播接收器确保ReminderReceiver在AndroidManifest.xml中正确注册。receiver android:name.receiver.ReminderReceiver android:enabledtrue android:exportedfalse / !-- 通常不需要导出 --检查Doze模式Android 6.0的Doze模式会延迟网络和CPU活动。使用setExactAndAllowWhileIdle()可以突破部分限制但仍有最小间隔。可以在设置中为你的应用关闭电池优化需要引导用户操作。使用adb命令调试在终端输入adb shell dumpsys alarm可以查看系统当前所有的闹钟设置检查你的闹钟是否在其中以及下一次触发时间是否正确。5.2 后台限制与通知不显示问题应用在后台或被清理后通知不出现。解决方案确保通知渠道已创建在App启动时如Application的onCreate或主Activity创建通知渠道。前台服务保活对于可靠性要求极高的场景可以考虑在触发提醒时启动一个前台服务。前台服务会有一个常驻通知告诉用户应用正在运行提醒功能。但这会消耗更多电量需要权衡。引导用户设置在应用内友好地提示用户将你的APP加入“白名单”或“不受电池优化限制”的列表。这可以通过Intent跳转到系统的电池优化设置页面来实现。val intent Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) intent.data Uri.parse(package:$packageName) startActivity(intent)5.3 数据库操作导致主线程阻塞或UI卡顿问题在主线程执行Room的插入、查询操作导致应用无响应ANR。黄金法则永远不要在UI线程执行数据库操作正确做法使用协程Coroutines在ViewModel或Repository层使用viewModelScope.launch或CoroutineScope(Dispatchers.IO).launch来执行挂起suspend的DAO操作。使用返回Flow的查询Room支持DAO方法返回FlowT。在ViewModel中收集这个Flow并更新到StateFlow或LiveDataUI通过观察这些数据容器来更新。这样数据库查询会在后台线程自动进行。// 在ViewModel中 init { viewModelScope.launch { medicineRepository.getAllMedicinesFlow().collect { medicineList - _uiState.value uiState.value.copy(medicines medicineList) } } }5.4 应用被杀死后数据与闹钟的恢复问题用户强制停止应用或重启手机后之前设置的闹钟失效。解决方案监听设备重启广播并在应用再次启动时重新设置所有闹钟。在AndroidManifest.xml中注册一个监听BOOT_COMPLETED广播的接收器。uses-permission android:nameandroid.permission.RECEIVE_BOOT_COMPLETED/ receiver android:name.receiver.BootReceiver android:enabledtrue android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / action android:nameandroid.intent.action.QUICKBOOT_POWERON / !-- 部分厂商 -- /intent-filter /receiver在BootReceiver的onReceive中从数据库读取所有有效的服药计划并重新调用ReminderScheduler.scheduleReminder为每个计划设置闹钟。同样在应用主Activity启动时例如在MainActivity的onCreate中也可以执行一次闹钟恢复逻辑以应对应用被系统意外终止的情况。5.5 针对老年人的测试要点字体缩放在手机设置中调大系统字体测试你的APP布局是否会错乱、文字是否被截断。屏幕阅读器开启TalkBackAndroid内置屏幕阅读器测试所有按钮和文本是否都有合理的contentDescription能否通过滑动和双击顺利操作。颜色反转/高对比度模式开启这些辅助功能检查你的UI是否仍然清晰可辨。真实场景模拟将手机放在一边等待闹钟触发。观察通知的醒目程度是否容易被忽略铃声和震动是否足够。尝试在嘈杂环境中测试。开发这样一个APP技术实现只是基础更重要的是对特定用户群体的深刻理解和关怀。每一次代码编写和界面设计都要问自己“我的爷爷奶奶能用得顺手吗” 这个过程让我深刻体会到好的软件不仅是功能的堆砌更是用户体验的打磨。这份毕业设计源码和说明希望能为你提供一个扎实的起点你可以在此基础上增加更多贴心的功能比如用药历史统计图表供子女查看、与智能药盒的蓝牙连接等让它变得更加完善和实用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进