ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Android医药助手源码实战:从药箱管理到精准用药提醒

Android医药助手源码实战:从药箱管理到精准用药提醒 简介面向Android初、中级开发者的医药助手项目源码适合研究医疗健康类App的药品查询、用药提醒、健康资讯等典型业务模块。压缩包共113个文件仅1.09MB以Java源码、XML界面、SQLite数据库脚本为主并含class/apk/dex编译产物及调试脚本。源码覆盖Activity与Fragment界面、SQLite与ContentProvider数据存储、网络请求与JSON解析、AlarmManager定时提醒、运行时权限申请等关键点并展示Gradle工程组织、第三方库集成与调试思路。研读后可理解Android从界面、数据、网络到系统服务的完整开发流程为独立开发同类应用打下基础。目前已有60人学习下载。1. 拿到“Android 医药助手源码.zip”之后先搞清楚这包代码解决什么问题“Android 医药助手源码.zip”解压出来后里面是一套完整的 Android Studio 工程而不是一篇需求文档。医药助手要解决的现实问题很具体家里老人每天吃三四盒药时间点不一样药品还有效期和库存子女不在身边就需要一个能记录药箱、扫码识别药品、到点提醒、漏服后重新计划的工具。这类源码的完整度通常很高适合做医疗健康类 App 的二次开发也适合想对照真实工程学 Room、AlarmManager、ZXing 的安卓工程师。与其反复找 Android Studio 安装教程不如先把 zip 里的 Gradle 构建跑通再沿着数据表一步步看实现。2. Android 医药助手源码的工程结构与数据模型拆解2.1 从 Gradle 配置反推项目基线解压 zip 后先不要急着点 Run。先看根目录 gradle-wrapper.properties里面 distributionUrl 决定当前工程需要的 Gradle 版本。如果本机没有对应版本Android Studio 会花很长时间下载让人误以为程序卡死的场景通常是 distributionUrl 指向了不存在的镜像地址或者仓库里的 gradle-wrapper.jar 被拦截了。再看模块的 build.gradle能反推出工程的目标 API 和依赖库。// app/build.gradle android { compileSdk 34 defaultConfig { applicationId com.example.medassistant minSdk 23 targetSdk 34 versionCode 1 versionName 1.0.0 } } dependencies { implementation androidx.room:room-runtime:2.6.1 kapt androidx.room:room-compiler:2.6.1 implementation androidx.work:work-runtime:2.9.0 implementation com.journeyapps:zxing-android-embedded:4.3.0 }这段配置里compileSdk 表示用哪个 Android SDK 版本编译如果本机没有 34Android Studio 会弹出 SDK 缺失提示所以先打开 SDK Manager 装上对应的 Platform 和 Build-Tools。minSdk 23 意味着最低兼容 Android 6.0运行时权限、Doze 省电、后台限制都要自己处理。targetSdk 34 则让系统按 Android 14 的规则约束通知权限和精确闹钟这和医药助手的提醒功能强相关。Room 负责本地药箱数据WorkManager 做周期性的临期检查ZXing 提供扫码页面。如果 zip 里带了两个 module优先看被主工程 implementation 引用的那个数据层通常在主 module。2.1.1 清单文件里的组件线索AndroidManifest.xml 能看出模块边界。除了启动 Activity医药助手源码里一般会有 CaptureActivity、RemindReceiver 和一个后台 Service。CaptureActivity 声明时要注意用 android:screenOrientation 锁定竖屏不然扫码页会跟随传感器来回重建。RemindReceiver 必须声明成 exportedfalse因为这个 Receiver 只接收 App 内部闹钟广播不需要暴露给系统。如果 Manifest 里看到了 RECEIVE_BOOT_COMPLETED 权限说明工程已经考虑重启手机后要重排闹钟这块很容易被忽略后面验证章节会讲到。2.2 药箱与用药计划怎么用 Room 建模医药助手第一版的数据模型不需要云端直接落本地 SQLite 就够。常见建模方式是两张表medicine 保存药箱里的药品信息plan 保存每日用药计划。药品可能有同品牌不同规格所以 barcode 不是主键用自增 id 做主键药品名称和规格单独存。plan 通过 medicineId 外键关联到药品打开 App 时要按用药计划展示今天的提醒列表。Entity(tableName medicine) data class Medicine( PrimaryKey(autoGenerate true) val id: Long 0, val name: String, val spec: String?, val barcode: String?, val approvalNumber: String?, val expireDate: String?, val stock: Int ) Entity( tableName plan, foreignKeys [ForeignKey( entity Medicine::class, parentColumns [id], childColumns [medicineId], onDelete ForeignKey.CASCADE )], indices [Index(medicineId)] ) data class Plan( PrimaryKey(autoGenerate true) val id: Long 0, val medicineId: Long, val timeTag: String, val dose: String, val startDate: Long, val endDate: Long, val remindEnabled: Boolean true )Room 外键的作用很直接当用户把药品从药箱删除时关联的用药计划会自动级联删除避免留下没有药品的孤儿提醒。单独给 plan 表的 medicineId 建索引是为了一次查询出计划后再 join medicine 取药品名称避免小表全表扫描。timeTag 用字符串 08:00 而不是 timestamp是因为用药计划要每天重复跨天计算时字符串排序和格式化都方便。如果用户服用的是隔天药还需要加 weekMask 字段用一个整数按 bit 位表示星期几。查询“今天需要提醒的所有用药计划”时常见 DAO 写法如下Dao interface PlanDao { Query( SELECT p.* FROM plan p INNER JOIN medicine m ON p.medicineId m.id WHERE p.remindEnabled 1 AND p.startDate :endOfDay AND p.endDate :startOfDay ORDER BY p.timeTag ASC ) fun findPlansByDate(startOfDay: Long, endOfDay: Long): ListPlan }注意这个查询用 endOfDay 和 startOfDay 两个边界而不是传入一个 today 然后和时间段比较。原因是 plan 里存的是疗程起止日期不是提醒的具体时间点假设一个药从 3 月 1 日吃到 3 月 7 日查询 3 月 6 日的计划时必须用 3 月 6 日 0 点到 23 点 59 分才能正确命中区间。很多初学者只传当天日期用 endDate today结果跨天或跨月就漏了。| 字段 | 类型 | 设计说明 | | medicineId | Long | 关联 medicine 表CASCADE 级联删除 | | timeTag | String | 固定 HH:mm 格式用于跨天提醒 | | startDate / endDate | Long | 疗程起止当天的零点时间戳 | | remindEnabled | Boolean | 用户手动暂停提醒时不删计划只改这个字段 |如果只是几十种药、计划字段多后续还要做统计报表用 SharedPreferences 存 JSON 没法按条件查询也无法做数据库升级。Room 的意义在于把“药箱”“计划”“服药记录”三张表的关系固化下来查询逻辑交给 SQL 处理。3. Android 医药助手源码的核心模块扫码、提醒、漏服重算3.1 药品条码扫码录入的三种实现路径药品录入是用户行为门槛最高的功能。市面上常见源码里条码扫描有两种主流方案一种是引入 zxing-android-embedded把 CaptureActivity 跳转进来另一种是 CameraX 配合 ML Kit 的 BarcodeScanning。我更倾向于保留 zxing因为医药助手的页面大多不需要实时扫描叠加 AR 框业务上“对准药盒、响一声、返回条码”就够了。zxing-android-embedded 在 build.gradle 里加一行依赖代码里用 ActivityResultLauncher 就能拿到结果省去自己处理相机权限和取景框的功夫。private val scanLauncher registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result - if (result.resultCode Activity.RESULT_OK) { val code result.data?.getStringExtra(IntentIntegrator.SCAN_RESULT) if (code.isNullOrBlank()) returnregisterForActivityResult lookupMedicineByBarcode(code) } } fun launchScan() { scanLauncher.launch(Intent(this, CaptureActivity::class.java)) }这里 SCAN_RESULT 是 zxing 库固定返回的 Key拿到的是药品包装上的 EAN-13 或 Code 128 条形码内容。如果扫码后没有任何回调先看 CaptureActivity 有没有在 Manifest 里注册以及 onActivityResult 的 resultCode 判断是否正确。有的源码会把扫码结果回调放在 onNewIntent 里因为 CaptureActivity 在 Android 10 以后由于屏幕旋转重建了 Activity导致 onActivityResult 拿不到数据这种情况下建议给 CaptureActivity 加上 android:configChanges 限制屏幕旋转或者改用 ActivityResultLauncher 替代。药品条码有两种需要区分的存储位置外包装的 EAN-13 是中国商品条码码里不包含药品通用名和规格批准文号“国药准字 XXXX”在包装上往往印成可读文本而不是条码。所以扫码命中后源码里要维护一个 barcode 到药品名称的映射表或者把常用药数据库打包在 assets 里。在线查询不建议每次都请求网络离线优先是医药助手的常见做法。3.2 用药提醒AlarmManager 还是 WorkManager用药提醒是这个 App 存在的理由所以调度方案要和普通新闻类通知区分开。WorkManager 的最小间隔 15 分钟在 Doze 模式下会被拉长不适合“早上 8 点必须响”的服药场景。精确提醒要用 AlarmManager 的 setExactAndAllowWhileIdleAndroid 12 以后还需要申请 SCHEDULE_EXACT_ALARM 权限。源码里如果发现用了 setRepeating基本上在 Android 12 的真机上会不准因为系统会对重复闹钟进行漂移。fun scheduleRemind(context: Context, planId: Long, timeMillis: Long) { val alarmManager context.getSystemService(AlarmManager::class.java) val intent Intent(context, RemindReceiver::class.java).apply { action com.example.medassistant.ACTION_REMIND putExtra(planId, planId) data Uri.parse(medassistant://remind/$planId) } val pendingIntent PendingIntent.getBroadcast( context, planId.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, timeMillis, pendingIntent ) }参数说明requestCode 传 planId保证每条用药计划的 Intent 能互相区分如果两条计划用同一个 requestCode后设置的会覆盖前面的提醒。data 参数也很重要PendingIntent 比较 Intent 是否相同时会忽略 extra但会比较 action 和 data所以加一个带 planId 的 URI 能把它们彻底区分开。FLAG_IMMUTABLE 是 Android 12 对可变性的强制要求不加会抛 SecurityException如果目标 SDK 还在 30 以下也要主动加上避免未来升级踩坑。AlarmManager 在部分国产 ROM 上表现不一致尤其是 MIUI 和 EMUI需要在用户第一次设置提醒时就引导打开“自启动”或“后台弹窗”权限。这不是源码能彻底解决的问题只能靠运行时检测和跳设置页。| 场景 | 推荐方案 | 原因 | | 到点服药提醒 | AlarmManager.setExactAndAllowWhileIdle | 精确到分钟Doze 下也能响 | | 药品临期检查和库存补货通知 | WorkManager | 后台任务不需要精确到秒 | | 重启手机后恢复提醒 | BOOT_COMPLETED 广播重排 | 闹钟不会持久化保存 |提醒到达后RemindReceiver 里要发出通知并调度第二天。常见做法是用 NotificationManager 的 notify把 planId 映射成通知 ID避免重复。如果要支持用户点通知后进入某个药品详情页PendingIntent.getActivity 里还要带上药品 id。3.3 漏服重算算法与服药日历绘制漏服重算没有统一规则源码里常见的实现是“记录每次应服药时间到了下个服药时点还没标记已服就自动记为漏服”。要想重算下一次提醒不能简单地在当前时间上加 24 小时因为每日服药点可能有两个甚至三个。函数如下fun nextRemindTime( now: Long, timeTags: ListString, reminderIntervalDays: Int 1 ): Long { val todayTags timeTags.mapNotNull { tag - runCatching { val hm tag.split(:) Calendar.getInstance().apply { timeInMillis now set(Calendar.HOUR_OF_DAY, hm[0].toInt()) set(Calendar.MINUTE, hm[1].toInt()) set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) }.timeInMillis }.getOrNull() }.filter { it now } val base if (todayTags.isNotEmpty()) { todayTags.min() } else { val first timeTags.first() val hm first.split(:) Calendar.getInstance().apply { timeInMillis now reminderIntervalDays * 24 * 3600 * 1000L set(Calendar.HOUR_OF_DAY, hm[0].toInt()) set(Calendar.MINUTE, hm[1].toInt()) set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0) }.timeInMillis } return base }参数说明timeTags 必须按时间排序比如 [08:00, 20:00]now 是 System.currentTimeMillis。函数首先取今天还没过的时点选最早一个如果今天所有时点都过了就跳到下一个服药日的第一时点。reminderIntervalDays 支持隔天服药这是 weekMask 之外的另一套简化逻辑。漏服后真正要做的不是立刻补一次而是把漏服状态写入 record 表并弹出提示由用户决定是否补服自动补服容易造成重复用药。服药日历绘制通常用 RecyclerView GridLayoutManager一周 7 列。状态有三种已服、待提醒、漏服。源码里不要只画当前日期要在页面上显示每个 Cell 的 timeTag 和 record 的 takenTime如果只展示数字用户无法区分“今天有两次药”和“重复展示了同一行”。4. Android 医药助手源码里最容易改坏的三处适配4.1 FileProviderAndroid 7.0 后的 Uri 授权很多医药助手源码会带“处方拍照”或“导出用药记录”功能。只要 Activity 里用 Uri.parse(file:///sdcard/...) 交给相机 App在 Android 7.0 以上就会立刻崩出 FileUriExposedException。解决方案是 FileProvider它用一个 content:// URI 代表真实文件路径再对被调用的外部 App 临时授予读写权限。这个改错率非常高常见的坑是把网上别人工程里的 content://com.tencent.wework.fileprovider/external_path/android/data/com某个企业 IM 的 FileProvider 路径直接复制过来却没有改成自己应用的 authority。provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerfile_paths.xml 里需要声明哪些目录可以被 FileProvider 暴露paths external-path namemed_images pathPictures/MedAssistant / cache-path namemed_cache path. / /paths说明 ${applicationId} 会自动替换成 applicationId最终 URI 是 content://com.example.medassistant.fileprovider/med_images/xxx.jpg。pathPictures/MedAssistant 比 path. 安全得多因为前者只暴露业务目录后者会把整个 SD 卡都暴露给外部组件。拍照回调时应该用 FileProvider.getUriForFile(context, authority, photoFile) 生成 uri而不是自己拼字符串。如果日志里出现了 content://com.tencent.wework.fileprovider 这样的路径先检查是不是直接把别人样例里的 FileProvider 抄了进来再检查 getUriForFile 的第二个参数和 manifest 里的 authority 是否一致。4.2 Android 12/13 的闹钟与通知权限申请Android 12 开始调用 setExactAndAllowWhileIdle 之前必须检查 canScheduleExactAlarms()。如果没做检查直接调系统不会像运行时权限那样当场崩溃但闹钟会被静默拒绝用户那边表现为“App 开了通知到点却不响”。最常见的问题是开发者在 Android 12 以下设备调试时一切正常换到 Android 13 就发现提醒不响于是去查 Service 代码查了半天发现是权限没申请。第一次创建提醒时要加一个权限引导fun ensureExactAlarmPermission(activity: Activity) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { val alarmManager activity.getSystemService(AlarmManager::class.java) if (!alarmManager.canScheduleExactAlarms()) { activity.startActivity( Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { data Uri.parse(package:${activity.packageName}) } ) } } }Android 13 以后通知本身也要运行时权限。医药助手启动时要同时申请 POST_NOTIFICATIONS不然 RemindReceiver 发出的通知会被系统吃掉if (Build.VERSION.SDK_INT 33) { activity.requestPermissions(arrayOf(android.permission.POST_NOTIFICATIONS), 1001) }这两个权限不能合并成一个弹窗。SCHEDULE_EXACT_ALARM 只能跳到系统设置页手动打开POST_NOTIFICATIONS 则是标准运行时权限用 requestPermissions 弹窗。如果 targetSdk 是 34 且应用没有闹钟或日历功能Android 14 还会收紧 USE_EXACT_ALARM 的授予医药助手一般无法声明自己是闹钟应用所以正确的生产方式是在 Manifest 里声明 SCHEDULE_EXACT_ALARM并在设置页放一个检测按钮反复提示用户打开。这里还要记得在 Manifest 中声明 SCHEDULE_EXACT_ALARM 才会显示设置项不然 ACTION_REQUEST_SCHEDULE_EXACT_ALARM 直接没反应。4.3 Room 数据库升级版本号与 Migration改数据表结构是二次开发里最频繁的操作。最典型场景是给 plan 表加一个 weekMask 字段但却忘了把数据库版本从 1 改成 2甚至改了版本却没有写 Migration。前者会直接崩后者会丢用户数据。要注意 Room 不允许在迁移后仅靠降级恢复。val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE plan ADD COLUMN weekMask INTEGER NOT NULL DEFAULT 127) } } Room.databaseBuilder(context, AppDatabase::class.java, med_assistant.db) .addMigrations(MIGRATION_1_2) .build()说明ALTER TABLE 只能加字段如果要把 plan 表原有的 timeTag 从 String 改成 IntegerSQLite 不支持改列类型必须新建一张 plan_new 表把旧数据拷贝过去再删除旧表重命名。这个流程非常容易出错所以建议改动前先在本地用 SQLite 直接演练一遍。启动崩溃时看 LogcatRoom 会打印出 expected version 和 found version找到这两个值就知道应用代码版本和数据库实际版本差多少。如果只是拿源码包学习不想保留旧数据可以用 fallbackToDestructiveMigration()但对着用户发布的 App 绝不能写这行它会在升级后清空整个药箱。提示assembleDebug 只解决编译问题不解决运行时权限适配。Android 12 设备上做第一轮提醒测试前先确认系统设置里“闹钟和提醒”开关已打开。5. 用 Android 医药助手源码做二次开发前先跑这三遍验证5.1 用 Gradle 命令把 debug 包跑起来在 Android Studio 之外先跑一遍命令行构建能最快暴露源码缺失的 SDK 和 Gradle 环境问题。解压 zip 后打开终端进入工程根目录执行./gradlew :app:assembleDebug adb install -r app/build/outputs/apk/debug/app-debug.apk如果 zip 里的 gradlew 没有执行权限Linux 和 macOS 下先 chmod x gradlewWindows 下直接用 gradlew.bat。assembleDebug 只打 debug 包不签名适合验证能不能编译。第一次执行会下载 Gradle 发行版和所有依赖耗时取决于网络这和 Android Studio 里点 Run 没有本质区别。看到 BUILD SUCCESSFUL 后再把 APK 装到模拟器上做冒烟测试。5.2 用 adb 检查闹钟与通知链路用药提醒不响的排查顺序是权限、闹钟是否注册、广播是否收到、通知是否发出。先查闹钟队列里有没有本应用的 PendingIntentadb shell dumpsys alarm | grep -E medassistant如果 grep 不到任何输出说明 scheduleRemind 没有执行或者 PendingIntent 被相同 requestCode 覆盖了。接着查通知发出时是否被系统拦截查看通知相关内容adb shell dumpsys notification --noredact | head -60观察是否有你的包名出现以及 importance 字段是不是 IMPORTANCE_HIGH。通知渠道默认 importance 在医药助手源码里往往被忽略需要手动设置成 IMPORTANCE_HIGH 才会在 Android 8.0 以后有声音和横幅。5.3 给数据库迁移写一个最简单的验证用例第 4.3 节里的 Migration 建议用代码验证。做法是启动一个指定数据库名称的 Room 实例通过 MigrationTestHelper 先迁移到版本 2再插入一条旧数据确认 weekMask 默认值加上且没有抛 IllegalStateException。把上面的两条 adb 命令打成一个小脚本每次改动提醒功能后直接跑一遍就能在回归测试前发现一半以上“到点不响”的假 Bug。本文还有配套的精品资源点击获取
返回列表