简介:本资源是一套面向计算机专业本科生的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也能体现你对技术趋势的关注。
- 架构模式:推荐采用MVVM(Model-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(): Flow<List<Medicine>> // 使用Flow,便于在UI中观察数据变化 // 查询某药品的所有记录 @Query("SELECT * FROM medicine_records WHERE medicineId = :medId ORDER BY takenTime DESC") fun getRecordsForMedicine(medId: Long): Flow<List<MedicineRecord>> }数据库类(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方法返回Flow<List<T>>而不是LiveData<List<T>>是更现代的做法,配合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_width="match_parent" android:layout_height="wrap_content" app:cardElevation="4dp" android:layout_margin="8dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/tvMedicineName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" <!-- 大字体 --> android:textStyle="bold" android:textColor="@color/primary_text_dark" tools:text="阿司匹林肠溶片" /> <TextView android:id="@+id/tvMedicineDosage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:layout_marginTop="8dp" android:textColor="@color/secondary_text_dark" tools:text="剂量:一次一片,一日三次" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="12dp"> <!-- 大而明显的操作按钮 --> <Button android:id="@+id/btnEdit" android:layout_width="0dp" android:layout_height="60dp" <!-- 高触摸目标 --> android:layout_weight="1" android:layout_marginEnd="4dp" android:textSize="18sp" android:text="编辑" android:backgroundTint="@color/colorPrimary" /> <Button android:id="@+id/btnDelete" android:layout_width="0dp" android:layout_height="60dp" android:layout_weight="1" android:layout_marginStart="4dp" android:textSize="18sp" android:text="删除" android:backgroundTint="@color/colorAccent" /> </LinearLayout> </LinearLayout> </androidx.cardview.widget.CardView>4. 关键业务流程与代码实现串联
4.1 从添加药品到设置提醒的完整流程
让我们串联起一个核心用户旅程:用户添加一种新药并设置提醒。
1. 添加药品界面 (AddMedicineActivity/ViewModel):用户输入药名、剂量,并点击“设置提醒时间”。这里,我们跳转到一个专门设置时间的界面,或者使用底部弹窗(BottomSheetDialogFragment)来添加多个时间点。
2. 数据保存与闹钟设置:当用户在时间设置界面点击“保存”后,我们需要做两件事:将药品存入数据库,并为每个设置的时间点安排闹钟。
// AddMedicineViewModel.kt class AddMedicineViewModel(private val medicineRepository: MedicineRepository) : ViewModel() { fun saveMedicine(medicine: Medicine, reminderTimes: List<Long>) { 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: StateFlow<List<TodayScheduleItem>> = 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:enabled="true" android:exported="false" /> <!-- 通常不需要导出 --> - 检查Doze模式:Android 6.0+的Doze模式会延迟网络和CPU活动。使用
setExactAndAllowWhileIdle()可以突破部分限制,但仍有最小间隔。可以在设置中为你的应用关闭电池优化(需要引导用户操作)。 - 使用adb命令调试:在终端输入
adb shell dumpsys alarm可以查看系统当前所有的闹钟设置,检查你的闹钟是否在其中,以及下一次触发时间是否正确。
- 检查PendingIntent:确保
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)
- 确保通知渠道已创建:在App启动时(如
5.3 数据库操作导致主线程阻塞或UI卡顿
- 问题:在主线程执行Room的插入、查询操作,导致应用无响应(ANR)。
- 黄金法则:永远不要在UI线程执行数据库操作!
- 正确做法:
- 使用协程(Coroutines):在ViewModel或Repository层,使用
viewModelScope.launch或CoroutineScope(Dispatchers.IO).launch来执行挂起(suspend)的DAO操作。 - 使用返回Flow的查询:Room支持DAO方法返回
Flow<T>。在ViewModel中收集这个Flow,并更新到StateFlow或LiveData,UI通过观察这些数据容器来更新。这样数据库查询会在后台线程自动进行。
// 在ViewModel中 init { viewModelScope.launch { medicineRepository.getAllMedicinesFlow().collect { medicineList -> _uiState.value = uiState.value.copy(medicines = medicineList) } } } - 使用协程(Coroutines):在ViewModel或Repository层,使用
5.4 应用被杀死后数据与闹钟的恢复
- 问题:用户强制停止应用或重启手机后,之前设置的闹钟失效。
- 解决方案:监听设备重启广播,并在应用再次启动时重新设置所有闹钟。
- 在
AndroidManifest.xml中注册一个监听BOOT_COMPLETED广播的接收器。<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <receiver android:name=".receiver.BootReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" /> <!-- 部分厂商 --> </intent-filter> </receiver> - 在
BootReceiver的onReceive中,从数据库读取所有有效的服药计划,并重新调用ReminderScheduler.scheduleReminder为每个计划设置闹钟。 - 同样,在应用主Activity启动时(例如在
MainActivity的onCreate中),也可以执行一次闹钟恢复逻辑,以应对应用被系统意外终止的情况。
- 在
5.5 针对老年人的测试要点
- 字体缩放:在手机设置中调大系统字体,测试你的APP布局是否会错乱、文字是否被截断。
- 屏幕阅读器:开启TalkBack(Android内置屏幕阅读器),测试所有按钮和文本是否都有合理的
contentDescription,能否通过滑动和双击顺利操作。 - 颜色反转/高对比度模式:开启这些辅助功能,检查你的UI是否仍然清晰可辨。
- 真实场景模拟:将手机放在一边,等待闹钟触发。观察通知的醒目程度(是否容易被忽略),铃声和震动是否足够。尝试在嘈杂环境中测试。
开发这样一个APP,技术实现只是基础,更重要的是对特定用户群体的深刻理解和关怀。每一次代码编写和界面设计,都要问自己:“我的爷爷奶奶能用得顺手吗?” 这个过程让我深刻体会到,好的软件不仅是功能的堆砌,更是用户体验的打磨。这份毕业设计源码和说明,希望能为你提供一个扎实的起点,你可以在此基础上增加更多贴心的功能,比如用药历史统计图表(供子女查看)、与智能药盒的蓝牙连接等,让它变得更加完善和实用。
本文还有配套的精品资源,点击获取