news 2026/9/14 15:52:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 版本编译,如果本机没有 34,Android 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 必须声明成 exported="false",因为这个 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): List<Plan> }

注意这个查询用 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()) return@registerForActivityResult 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 的 setExactAndAllowWhileIdle,Android 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: List<String>, 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 FileProvider:Android 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:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>

file_paths.xml 里需要声明哪些目录可以被 FileProvider 暴露:

<paths> <external-path name="med_images" path="Pictures/MedAssistant" /> <cache-path name="med_cache" path="." /> </paths>

说明 ${applicationId} 会自动替换成 applicationId,最终 URI 是 content://com.example.medassistant.fileprovider/med_images/xxx.jpg。path="Pictures/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 改成 Integer,SQLite 不支持改列类型,必须新建一张 plan_new 表,把旧数据拷贝过去,再删除旧表重命名。这个流程非常容易出错,所以建议改动前先在本地用 SQLite 直接演练一遍。启动崩溃时看 Logcat,Room 会打印出 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 gradlew;Windows 下直接用 gradlew.bat。assembleDebug 只打 debug 包,不签名,适合验证能不能编译。第一次执行会下载 Gradle 发行版和所有依赖,耗时取决于网络,这和 Android Studio 里点 Run 没有本质区别。看到 BUILD SUCCESSFUL 后,再把 APK 装到模拟器上做冒烟测试。

5.2 用 adb 检查闹钟与通知链路

用药提醒不响的排查顺序是:权限、闹钟是否注册、广播是否收到、通知是否发出。先查闹钟队列里有没有本应用的 PendingIntent:

adb 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。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:51:00

书霸AI格式排版:官网www.shubaai.com

www.shubaai.com很多人以为论文排版只是调整字体、行距和页边距&#xff0c;真正动手后才发现&#xff0c;期刊论文的格式要求往往分散在标题层级、作者信息、摘要关键词、正文结构、参考文献和页眉页脚等多个细节里。一个标点、一个缩进&#xff0c;甚至一处中英文间距&#x…

作者头像 李华
网站建设 2026/9/14 15:50:35

AI Agent记忆系统实战:短期记忆、长期记忆与完整工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 15:50:27

烧录地址0、0x08000000、0x6000到底啥区别?一文讲透

你有没有遇到过这种情况&#xff1a;同一个工程&#xff0c;今天打开烧录软件让你填 0&#xff0c;明天看教程里的截图写的是 0x08000000&#xff0c;后天开始做 Bootloader 时又有人告诉你 App 要从 0x6000 开始烧。三个数都叫“烧录地址”&#xff0c;都像是老手随口蹦出来的…

作者头像 李华
网站建设 2026/9/14 15:50:08

华为Pura 80 Ultra与vivo X200 Ultra旗舰影像哲学深度对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 15:49:13

企业级AI效能管理:可度量、可治理的智能体落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 15:47:59

Bazel 如何用 mod 命令查看模块依赖图并定位模块依赖来源?

Bazel 如何用 mod 命令查看模块依赖图并定位模块依赖来源&#xff1f; 【免费下载链接】bazel a fast, scalable, multi-language and extensible build system 项目地址: https://gitcode.com/GitHub_Trending/ba/bazel 使用 bzlmod 管理外部依赖的项目里&#xff0c;M…

作者头像 李华