去年我拿到《基于Android的智能老人生活辅助应用设计与实现》这个毕设题目时,第一反应不是“好不好做”,而是“终于不是商城和后台管理系统了”。说实话,计算机毕设里十有八九是点餐、购物、打卡,答辩PPT翻来翻去都是增删改查,而这个题目不一样:它要处理定位、短信、通知、后台任务、权限适配,还要把界面做到六十岁的人能看清、按得动。整个项目做下来,Android开发里最容易翻车的几个点我几乎踩了个遍,也正因为这样,答辩时能讲的东西远比想象中多。这篇文章就把我的选题思路、模块设计、核心源码逻辑、真机调试里遇到的诡异问题完整复盘一遍,给正在准备同类安卓毕设的同学一个可以直接参考的模板,也送给想给家里老人做点东西的朋友。
1. 老人生活辅助应用到底在解决什么问题:从生活场景倒推需求
1.1 为什么“老人+App”是一个被低估的真需求
这两年类似的选题特别多,有些同学会担心“会不会太老套”。我做完之后的感受是:题目老不老套不重要,关键是你能不能把需求讲清楚。老人辅助类应用和普通管理系统的本质区别在于,它的每一个功能都能对应到一个具体的、随时可能发生的真实场景。
先说一个最常见的场景:子女白天上班,老人在家独居,降压药一天吃三次,经常吃完就忘;出门买菜走远了一点,想联系又怕麻烦子女;万一在家里摔倒或者突发胸闷,手机就在旁边但按不准联系人。这些问题听起来琐碎,但落到App设计上就是几个硬指标:紧急求助必须一键触发、吃药提醒必须准点到达、子女远程必须知道老人大概位置、界面上的字必须够大。
我家里也有老人,所以做这个题目的时候不是先打开Android Studio写代码,而是先把场景写在本子上,一个一个问“这个场景在手机上怎么解决”。这个过程其实就是需求分析,也是论文里第一章最重要的内容。很多同学毕设做完了才发现功能堆了一堆但不知道解决什么问题,就是跳过了这一步。
1.2 需求拆分:四个核心模块是怎么定下来的
我把场景倒推出来的需求整理成了一张表格,后续整个项目的开发都是围�着这张表展开:
| 模块 | 具体功能 | 解决的真实场景 |
|---|---|---|
| 紧急求助 | 一键SOS、短信发送位置、快速拨号 | 老人突发不适或摔倒,需要第一时间联系子女 |
| 健康管理 | 血压/心率手动记录、数据图表展示 | 老人定期测血压,子女远程了解健康趋势 |
| 生活辅助 | 吃药提醒、喝水提醒、日程提醒 | 老人容易忘事,需要准点语音/通知提示 |
| 亲情互动 | 位置共享、状态查看、子女远程留言 | 子女想知道老人位置和最近状态,减少焦虑 |
这个结构不是从网上抄的,是根据“独居老人一天的生活”一条条列出来的。你甚至可以把这个推导过程写进论文的“需求分析”,答辩老师会觉得你确实思考过,而不是上来就写代码。
需要说明的是,健康管理这块我没有做蓝牙血压计对接,因为毕设周期有限,而且硬件对接会让整个项目复杂一个量级。我采用的是手动录入+图表展示,演示效果也足够直观。如果你有精力,预留一个接口以后扩展硬件测量是完全可以的,但不要一开始就铺太大。
2. 总体架构与数据流:从点击按钮到通知弹出来,中间发生了什么
2.1 为什么我坚持给毕设用三层架构
很多同学写安卓毕设喜欢“Activity里一把梭”,一个页面几百行代码,业务逻辑和界面混在一起。我当时就在想,这种写法开发起来确实快,但论文第四章的“系统设计”基本没法写,而且功能一多,光findViewById就能把人逼疯。
所以我老老实实用了三层结构:界面层(Activity、Fragment、XML布局)、业务层(Manager类,比如SosManager、RemindManager、HealthRecordManager)、数据层(Room数据库、SharedPreferences、以及预留的网络接口)。这个结构不是赶时髦,它有一个很现实的好处:答辩的时候,你可以清晰地告诉老师“界面层只负责展示,业务层封装逻辑,数据层专注存储”,这就体现了软件工程的基本素养。
当时项目里有一个特别典型的例子:吃药提醒跨了两个模块,SOS要读紧急联系人,健康记录要读用户信息。如果全部写在Activity里,改一个字段全得跟着动。拆成Manager之后,每个模块只通过方法调用,互不干扰。后面加功能的时候感受尤其明显,我在做“位置分享”时只改了几十个文件里的三个,其他完全不用动。
2.2 用一个SOS场景把数据流串起来
理解一个系统的运作,最快的方式就是追一条完整的数据链路。拿最核心的SOS功能举例,整个流程是这样的:
- 老人点击主界面“SOS”大按钮
- 设备震动反馈,防止老人以为没点到重复点击
- 系统获取当前GPS定位(优先拿最近一次缓存定位,保证速度)
- 拼接求助短信:紧急联系人电话 + 当前时间 + 经纬度
- 通过SmsManager发送短信,同时跳转到系统拨号界面
- 本次求助记录写入本地数据库的sos_record表,方便回溯
这个链路看起来简单,但每一步都有对应的代码模块:震动是Vibrator,定位是LocationManager,短信是SmsManager,记录是Room。答辩的时候拿这个流程一讲,老师立刻知道你的系统是完整跑通的,而不是零散Demo。
2.3 Room数据库表设计:字段不是越多越好
老人辅助应用的数据量其实不大,设计上应该以“够用、好懂”为原则。我当时设计了四张表,简单到可以直接看懂:
-- 用户表 CREATE TABLE user_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER, blood_type TEXT, emergency_contact TEXT, -- 紧急联系人电话 medical_history TEXT -- 病史备注 ); -- 健康记录表 CREATE TABLE health_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_type TEXT, -- blood_pressure / heart_rate value TEXT, -- 例如 "120/80" record_time TIMESTAMP ); -- 提醒记录表 CREATE TABLE remind_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, remind_type TEXT, -- medicine / water / schedule remind_time TEXT, is_repeat INTEGER, -- 是否重复 repeat_interval TEXT, enabled INTEGER ); -- SOS记录表 CREATE TABLE sos_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, sos_time TIMESTAMP, location TEXT, is_responded INTEGER -- 是否已处理 );这套表设计的核心思路是:每张表对应一个模块,每个字段都能在界面上找到入口。写论文画ER图也方便,直接把这些字段往图里填就行。不要为了显得高级加一堆外键,单机App的毕设项目完全没必要搞那么复杂。
3. 四个关键功能的源码落地:代码比你想的更简单
3.1 一键SOS:串起短信、拨号和定位的核心逻辑
SOS是整个项目的灵魂,也是演示时最容易出效果的地方。我用Java实现,核心逻辑可以压缩到几十行,先看主要代码骨架:
public class SosManager { public void triggerSos(Context context) { // 1. 震动反馈,防止误触也防止没按到 Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (vibrator != null && vibrator.hasVibrator()) { vibrator.vibrate(VibrationEffect.createOneShot(300, VibrationEffect.DEFAULT_AMPLITUDE)); } // 2. 获取最近一次可靠定位,优先速度快 Location location = getLastKnownLocation(context); String locationText; if (location != null) { locationText = "纬度:" + location.getLatitude() + ",经度:" + location.getLongitude(); } else { locationText = "位置获取失败,请电话联系"; } // 3. 从数据库拿紧急联系人电话 String contact = getUserEmergencyContact(context); if (TextUtils.isEmpty(contact)) { Toast.makeText(context, "请先在设置页填写紧急联系人", Toast.LENGTH_LONG).show(); return; } // 4. 发送求助短信 String message = "【紧急求助】我需要帮助!时间:" + System.currentTimeMillis() + ",位置:" + locationText; SmsManager smsManager = SmsManager.getDefault(); if (message.length() > 70) { ArrayList<String> parts = smsManager.divideMessage(message); smsManager.sendMultipartTextMessage(contact, null, parts, null, null); } else { smsManager.sendTextMessage(contact, null, message, null, null); } // 5. 跳转系统拨号界面作为二次保障 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + contact)); dialIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(dialIntent); // 6. 写入本地SOS记录表 saveSosRecord(context, locationText); } }这里有几个细节要注意。短信超过70个字必须拆分成多条发送,所以我用divideMessage做了一个判断。另一个是跳转拨号而不是直接拨号,这算一个防错设计,老人可能误触SOS,跳到拨号界面之后看一眼再确认,比一键就把电话打过去要安全,也体现了一个“辅助应用”应有的谨慎。
3.2 吃药提醒:我为什么用AlarmManager而不是WorkManager
定时提醒是Android开发里的老问题,我们当时就在AlarmManager和WorkManager之间纠结了很久。WorkManager更现代,解决的是“延迟可容忍的后台任务”,比如定期同步数据,但它不是为精确到分钟的提醒设计的。吃药提醒必须准点响,所以我最终用了AlarmManager。
// 设置一次性提醒 AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(this, RemindReceiver.class); intent.putExtra("remind_msg", "该吃降压药了"); PendingIntent pendingIntent = PendingIntent.getBroadcast( this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent );对应的BroadcastReceiver里收到广播之后,发起通知:
public class RemindReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String msg = intent.getStringExtra("remind_msg"); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel( "remind_channel", "用药提醒", NotificationManager.IMPORTANCE_HIGH); manager.createNotificationChannel(channel); Notification notification = new Notification.Builder(context, "remind_channel") .setContentTitle("生活辅助提醒") .setContentText(msg) .setSmallIcon(R.drawable.ic_remind) .setAutoCancel(true) .build(); manager.notify(1, notification); } }这段代码跑通之后我总结了一个经验:Android 8.0以上必须创建NotificationChannel,不然通知永远不显示;Android 12以上setExactAndAllowWhileIdle需要申请SCHEDULE_EXACT_ALARM权限。这条经验直接让我在后面少踩了一个大坑。
3.3 健康数据图表:用Room加MPAndroidChart做出“看得见”的成果
健康数据如果没有图表,就是一串数字,不仅老人看不懂,答辩老师也看不出工作量。我在项目里引入了MPAndroidChart库,这是Android端最常用的图表库,用它画血压折线图非常快。
LineChart lineChart = findViewById(R.id.lineChart); List<Entry> entries = new ArrayList<>(); // 从Room数据库读取最近7天的血压记录 for (HealthRecord record : dailyRecords) { float value = Float.parseFloat(record.value.split("/")[0]); // 收缩压 entries.add(new Entry(index, value)); } LineDataSet dataSet = new LineDataSet(entries, "收缩压趋势"); dataSet.setColor(Color.parseColor("#FF5722")); dataSet.setLineWidth(3f); dataSet.setCircleRadius(5f); LineData lineData = new LineData(dataSet); lineChart.setData(lineData); lineChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); lineChart.invalidate();MPAndroidChart的集成很简单,Gradle里加一行implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'就行。但这张图带来的答辩效果非常显著——只要演示时打开这个界面,老师一眼就能看出你处理了数据展示,而不是停留在简单的页面跳转。建议做一个“近7天血压/心率变化图”,再做一个“每日喝水/吃药完成统计图”,工作量可视化程度会更高。
3.4 适老化交互:字号、色块和防误触
老人端UI是整个系统最容易忽略、但最体现设计能力的地方。我做了三件事:全界面文字用18sp以上,能用24sp的地方不用18sp;SOS按钮单独做成一个圆角大色块,底色用红色,文字加粗;所有可点击按钮都加了300ms的防连续点击,避免老人手抖一下触发两次。
适老化设计这部分不需要太多代码,几条尺寸规范和颜色规范就够了,但它在论文里非常出彩。你可以单独开一节“界面适老化设计原则”,内容就是字号、对比度、点击区域尺寸、防误触策略。这一节几乎不用贴代码,却能让论文的实用性和人性化程度明显提升,答辩时也是一个很好的加分话题。
4. 开发顺序与工程准备:先做什么后做什么,能帮你少走弯路
4.1 环境配置:Android Studio版本和Gradle是最容易翻车的第一关
很多同学拿到新电脑打开Android Studio,一路点默认,结果新建工程就报错。我当时用的是Android Studio Hedgehog版本,配的Gradle 8.2,两个必须对应好,否则要么同步失败,要么跑起来之后编译报一堆乱七八糟的错。建议直接用Android Studio自带的新建项目向导,选“Empty Views Activity”模板,它会自动匹配好当前版本对应的Gradle,不要手动去改版本号。
另外一个非常实用的小建议:项目创建完先把依赖写好。我当时常用的核心依赖就这几个,大部分功能都能覆盖:
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.room:room-runtime:2.6.1' annotationProcessor 'androidx.room:room-compiler:2.6.1' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'Room的注解处理器在Java工程里用annotationProcessor,如果是Kotlin工程就要换kapt或者ksp,这个千万不能搞错,否则生成的代码找不到。
4.2 权限设计:把Android 12和Android 13的权限变化一次搞清楚
老人辅助应用涉及到的权限比普通App多,我把它们整理成了一张表,开发的时候对着表申请就行:
| 权限 | 用途 | 系统版本要求 |
|---|---|---|
| SEND_SMS | 发送SOS短信 | Android 6.0+动态申请 |
| ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION | 获取定位 | Android 6.0+动态申请 |
| SCHEDULE_EXACT_ALARM | 精确闹钟提醒 | Android 12+ |
| POST_NOTIFICATIONS | 显示通知 | Android 13+ |
| CALL_PHONE | 快速拨打联系人 | 跳拨号界面则不需要 |
权限申请有一个通用做法,用一个PermissionManager统一封装,通过ActivityCompat.requestPermissions申请,然后回调里做分支处理。不要在每一次调用的地方散落申请代码,否则调试一次就要把所有入口权限重新检查一遍,非常痛苦。
这段权限适配内容,在论文里也很值得写。你可以这样描述:“本系统针对Android 6.0至Android 13不同版本的权限机制进行了适配”,然后把上面这张表往论文里一放,工作量立刻清晰了。
4.3 UI适配:去搞一台几百块的低端手机测试
做老人辅助应用,界面适配比一般App更敏感。我自己测试用的是主力机,但后来发现两个明显问题:一是高分屏上看起来适中的字号,放到低端大屏机上变得偏小;二是复杂布局在低端机上有明显卡顿。
我的解决方法有两个:布局尽量用LinearLayout加weight或者ConstraintLayout,避免三层以上嵌套;列表用RecyclerView不用ScrollView包ListView。另外有条件的话,去二手平台收一台两三百块的国产低端机当测试机,性能和屏幕比例跟老人真实用的手机更接近。实际调试时发现的问题,会比你在模拟器上跑一个月都多。
5. 真机调试中的踩坑实录:这些问题不是你的代码写错了
5.1 华为和小米手机“杀后台”,提醒和SOS直接失效
这是我整个项目里最痛苦的一次排查。现象是:App在后台放半小时,闹钟提醒不响,SOS的短信也发不出去。当时第一反应是代码写错了,在Android Studio里看Logcat却没有任何异常,把App切回前台再测试,一切正常,这就非常诡异。
后来逐步排查出来的结论是:国产定制ROM(华为EMUI、小米MIUI)默认对非白名单应用有严格的后台限制,AlarmManager的精确闹钟被系统“冻结”了,BroadcastReceiver也收不到广播。这不是代码Bug,而是系统级策略。
解决方法是引导用户把App加入电池优化白名单。通过Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS这个Intent跳转到系统设置页,让用户手动开启“无限制”。同时在Semantic层面做了一层兜底:每次打开App时,检查是否在白名单里,如果不在,弹窗提醒。
这个排查过程非常值得写进论文的测试章节。你可以用它来展示“系统兼容性测试与问题修复”,比单纯写“应用运行稳定”有说服力得多。
5.2 定位结果“飘到省外”,罪魁祸首是getLastLocation缓存
SOS调试的时候还遇到过一个很离谱的问题:老人明明在家里,短信里的定位却显示到了几十公里外的另一个区,一度怀疑是定位SDK的问题。后来我在代码里打印了provider和time字段才发现,getLastLocation返回的是几小时前的GPS缓存,那个位置是之前测试时在户外留下的。
解决方法是拿位置后立即判断时间戳和精度:
Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (location != null && System.currentTimeMillis() - location.getTime() > 5 * 60 * 1000) { // 如果缓存时间超过5分钟,主动请求一次新定位 locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, locationListener, Looper.getMainLooper()); }另外还有一个心得:在室内环境纯用GPS几乎拿不到有效定位,一定要把GPS_PROVIDER和NETWORK_PROVIDER混合使用,或者直接集成高德/百度定位SDK。考虑到毕设项目不要太重,我选择了LocationManager加超时容忍策略,效果够用。如果你有精力,接入一个第三方定位SDK也可以,但注意版号合规和文档更新问题。
5.3 通知权限明明开了,通知就是不显示
做提醒功能的时候,有一个特别容易忽略的坑:Android 13开始,通知权限从安装时默认开启变成了运行时动态申请。我在Android 13的测试机上遇到的现象是,App里通知开关是打开的,但收不到任何通知。
排查下来发现有两个原因:一是没有创建NotificationChannel,或者创建了但importance级别是LOW,导致通知被系统静默;二是Android 13需要显式申请POST_NOTIFICATIONS权限,并且这个权限要在发送第一条通知之前完成申请。
这个坑属于“系统行为差异”而不是代码错误,遇到时不要慌。把创建渠道的代码放在Application启动时执行一次,把权限申请放在首页进入后的弹窗里,问题就解了。这次排查过程也让我养成了一个思维习惯:凡是“权限看着都在但功能失效”的,优先往系统版本行为差异上查。
6. 从“能跑”到“能答辩”:演示策略和论文素材怎么准备
6.1 真机演示一定要准备“故障预案”
答辩现场用真机演示是风险评估最高的一环。我当时提前把另一个模拟器也装好了,并且在演示前一天录制了完整的功能操作短视频,作为Plan B。这个建议真心强烈:真机演示时,短信发送偶尔会因为运营商延迟而看不到现象,网络定位可能因为室内环境失败,这些都不是你的代码问题,但如果现场没有Plan B,老师会认为是代码问题。
我的操作策略是:先让老师看到真机界面,快速演示吃药提醒的弹窗和健康图表的滑动效果,然后打开数据库查看日志记录,证明后台数据在持续写入。至于SOS短信,提前录好视频,现场说“短信发送受运营商影响,我们看一下完整录制效果”。让老师看到“救命”短信真实出现在收件箱里的视频,比现场等一条短信的演示体验好得多。
6.2 日志入库:让每一步操作都有据可查
答辩时最怕老师说“你这个系统好像没什么工作量”。我的应对办法是在项目里加了一张operation_log表,记录每一次关键操作的时间、模块、结果。比如SOS触发了、提醒弹出了、健康数据录入了,全部写入数据库。答辩的时候打开这张表,能看到测试期间几十条操作记录,这就是最直观的工作量证明。
CREATE TABLE operation_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, module_name TEXT, action_desc TEXT, result TEXT, log_time TIMESTAMP );当时项目里还有一个亮点功能是“子女远程留言”,因为时间关系我没有做实时推送,采用的是轮询服务器接口的方式。如果你有自己的服务器或者用第三方云数据库,可以把这部分做成简单的HTTP接口调用,演示时打开两个界面,一个老人端一个子女端,数据同步更新,效果会非常加分。
6.3 论文里这几张图最能体现工作量,代码少截
最后说论文。很多同学喜欢在论文里贴大段代码,其实答辩老师基本不看。真正能为论文加分的是这几张图:
系统架构图(三层结构画清楚)、SOS功能时序图(从点击到短信发送的流程)、数据库ER图、以及真机界面截图对照图。时序图推荐用Visio或者ProcessOn画,不要用代码生成工具,画得越简洁越好。界面截图一定要包含适老化设计的对比,字体大小、按钮颜色、点击区域这些都可以做前后对比,一篇论文有这些内容,工作量一下就立住了。
另外,不要把整个项目源码贴到论文里,只选核心代码片段,比如SosManager的核心方法、AlarmManager设置方法、Room数据库表的定义,每个片段控制在20行以内,配一句文字说明。老师要源码的时候,再通过附录或Git仓库提供完整工程,这样轻重得当,也给论文答辩留了更多可聊的空间。