news 2026/9/16 14:04:20

中医大夫助理Android源码解析:SQLite与ListView实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中医大夫助理Android源码解析:SQLite与ListView实践

简介:这是一份面向Android开发者与中医信息化方向学习者的完整项目源码,实现中医大夫在诊断、病历记录与中药知识查询方面的辅助功能。源码包共125个文件,压缩包约1.55MB,涵盖20个Java源文件、49个class文件、16个XML布局与配置文件、10个SQL数据库脚本,以及可直接安装的APK,便于对照分析界面定义、业务逻辑与数据存储的衔接。包内类文件按功能模块组织,包括中药详情、方剂查询、病历编辑与识别适配等,呈现一个中医诊疗助手的全貌;SQL脚本和数据库辅助类揭示了Android端本地存储的实现方式,适合作为健康医疗类应用的起步参考。当前已有172人学习下载,对于想系统掌握Android分模块开发的初、中级开发者而言,具备不错的阅读与复用价值。

1. 中医大夫助理 Android 源码包先读结构再谈改造

拿到android应用源码zz-doctor中医大夫助理信息系统源码.zip后,最先注意到的不是代码,而是resources.ap_zz_doctor.apkjarlist.cache和一排.class文件。这说明它不是能直接放进 Android Studio 的完整工程,而是 APK 构建中间产物或反编译导出目录。正因为如此,阅读它的价值反而更高:可以从DbHelperMedicineQueryRxRecipeDetailEdit这些类名反推一个中医大夫助理系统的数据流和界面层级。下面会先把整个结构还原出来,再把数据库、处方编辑、关键词高亮 Adapter 逐层拆开,适合做医疗记录类 Android 应用的开发者,也适合刚学 SQLite 与 ListView 联动的同学。

2. 从打包结构和类名反推 zz-doctor 的整体分层

先别急着找 Activity 入口。把 zip 拖到本地后,第一件事是按扩展名归类,弄清楚哪些是运行产物、哪些能辅助阅读。下面是我把文件列表映射回 APK 原始目录后的对照表。

文件APK 构建阶段对应位置用途
resources.ap_res/资源打包中间文件把布局、字符串、图片等资源编译后的二进制资源索引,配合zz_doctor.apk使用
zz_doctor.apkAPK 根目录安装包本体,内含classes.dexresources.arscAndroidManifest.xml
*.classbuild/intermediates/javac/...编译后的 Java 字节码,保留了类名、方法名和局部变量名,可用javap -c阅读
jarlist.cacheGradle 构建缓存记录项目依赖的 jar 列表,与运行时逻辑无关,可忽略

有了这个分层,下一步就能按类名恢复业务模块。.class文件不是.java,不能直接拿过来改,但它保留了完整的方法签名和字段结构,配合反编译插件,阅读体验接近源码。

2.1 按类命名还原模块边界

压缩包里列出的.class文件数量不多,恰好覆盖了一个小型“中医处方助手”的骨架:

  • DbHelper:数据库帮助类,负责 SQLite 建表、升级和增删改查。
  • WelcomeRxRecipeQuery:应用打开后的第一个查询页,可能兼作入口页。
  • MedicineQueryMedicineDetail:药材列表查询页和药材详情页。
  • RxRecipeDetailRxRecipeDetailEdit:处方详情页与处方编辑页。
  • IdentifiedStringAdapter:列表适配器,作用是把查询关键词在结果里标识出来。

这里没有看到网络请求相关的类,也没有ContentProviderService,说明它的业务几乎完全跑在本地。中医大夫在诊室使用这类工具时,经常处于没有稳定内网的环境,离线 SQLite 是最稳的方案。如果以后要做成团队协作版本,再往上层加 Retrofit 也不是难事,但现在这个项目核心是“单机知识库 + 处方记录”。

2.2 为什么 class 文件比 dex 更容易阅读

通常在已安装 APK 里拿到的是classes.dex,要看代码得先用jadxbaksmali反编译。而这份 zip 直接提供.class字节码,说明它来自 Gradle 的build/intermediates目录,或是某个即时编译缓存目录。.class文件保留了原来的包路径和局部变量表,用javap -c能直接看到精准到每条指令的字节码,也可以放进 IDEA 自带的反编译插件里转成可读的 Java 代码。

注意:.class不等于.java,它只是给你“读”的,不是给你“改”的。想看可维护的工程结构,仍然要自己根据这些类重新建模块。

模块边界清晰之后,数据层的设计就成了重点。

3. 数据层设计:从 DbHelper 倒推 SQLite 表结构

3.1 三张表的建模思路

既然项目叫“中医大夫助理”,数据层一定要承载两类核心业务:查药材、记处方。DbHelperonCreate里最合理的做法是建三张表:medicine存药材字典,rx_recipe存处方主表,rx_recipe_detail存处方明细。下面是我按类名和行为补全的建表 SQL:

CREATE TABLE medicine ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, alias TEXT, property TEXT, meridian TEXT, efficacy TEXT, dosage TEXT, contraindication TEXT ); CREATE TABLE rx_recipe ( id INTEGER PRIMARY KEY AUTOINCREMENT, patient_name TEXT, diagnosis TEXT, title TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE rx_recipe_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_id INTEGER NOT NULL, medicine_id INTEGER NOT NULL, dose TEXT, note TEXT, FOREIGN KEY(recipe_id) REFERENCES rx_recipe(id), FOREIGN KEY(medicine_id) REFERENCES medicine(id) );

第一张表medicine的关键不在id,而在propertymeridianefficacy这些中医字段。字段文本一旦写得不统一,比如“微温”和“微 温”,查询端用LIKE '%温%'时结果会不稳定,所以入库前要做一次 trim。第二张表rx_recipe记录“给谁看、诊断是什么、什么时候开的方”,created_at使用 SQLite 的datetime('now','localtime'),避免自己在 Java 层拼时间字符串。第三张表rx_recipe_detail是对多对多的拆解,没有它,一个处方里多味药就只能塞进一个长字符串,后面想统计“哪味药最常用”就非常痛苦。

3.2 DbHelper 的初始化与升级逻辑

DbHelper继承SQLiteOpenHelper后,核心就是onCreateonUpgrade。常见写法是:

public class DbHelper extends SQLiteOpenHelper { private static final String DB_NAME = "zz_doctor.db"; private static final int DB_VERSION = 1; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE medicine (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, ...)"); db.execSQL("CREATE TABLE rx_recipe (id INTEGER PRIMARY KEY AUTOINCREMENT, patient_name TEXT, ...)"); db.execSQL("CREATE TABLE rx_recipe_detail (id INTEGER PRIMARY KEY AUTOINCREMENT, recipe_id INTEGER, ...)"); initMedicineData(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS rx_recipe_detail"); db.execSQL("DROP TABLE IF EXISTS rx_recipe"); db.execSQL("DROP TABLE IF EXISTS medicine"); onCreate(db); } }

这里有个重点:DB_NAME是数据库文件名,DB_VERSION是 Schema 版本号。以后如果只加一列,应该把DB_VERSION改成 2,再在onUpgrade里写ALTER TABLE迁移。示例代码里使用DROP TABLE再重建,只是演示项目为了快速重置数据,别直接抄进生产环境。一个医疗记录应用如果把历史处方一次性清掉,后果不是闪退能比的。

3.3 离线场景下内置药材数据的初始化

大夫打开应用时不可能等网络同步,所以onCreate里通常会写入一批常用药材作为本地字典。数量不大时,用ContentValues逐条插入就好:

private void initMedicineData(SQLiteDatabase db) { db.beginTransaction(); try { ContentValues cv = new ContentValues(); cv.put("name", "人参"); cv.put("property", "甘、微苦,微温"); cv.put("meridian", "归脾、肺、心、肾经"); cv.put("efficacy", "大补元气,复脉固脱"); db.insert("medicine", null, cv); cv.clear(); cv.put("name", "黄芪"); cv.put("property", "甘,微温"); cv.put("meridian", "归脾、肺经"); cv.put("efficacy", "补气升阳,固表止汗"); db.insert("medicine", null, cv); db.setTransactionSuccessful(); } finally { db.endTransaction(); } }

beginTransaction让几百条insert变成一个事务,如果中间某条数据因为约束失败,之前写入的也会一起回滚,不会出现“内置药材只插了一半”的情况。setTransactionSuccessful()一定要在finallyendTransaction()之前调用,这个顺序写反了,事务不会提交。

3.4 给界面层提供查询方法

MedicineQuery页面最终需要的是一个能按关键词查药材的方法:

public List<Medicine> queryMedicine(String keyword) { SQLiteDatabase db = getReadableDatabase(); List<Medicine> result = new ArrayList<>(); String sql = "SELECT * FROM medicine WHERE name LIKE ? OR alias LIKE ? OR efficacy LIKE ?"; String arg = "%" + keyword + "%"; Cursor cursor = db.rawQuery(sql, new String[]{arg, arg, arg}); while (cursor.moveToNext()) { Medicine m = new Medicine(); m.setId(cursor.getLong(cursor.getColumnIndexOrThrow("id"))); m.setName(cursor.getString(cursor.getColumnIndexOrThrow("name"))); m.setEfficacy(cursor.getString(cursor.getColumnIndexOrThrow("efficacy"))); result.add(m); } cursor.close(); return result; }

LIKE的三个参数分别绑定到namealiasefficacy,这样搜“人参”能命中名字,搜“补气”能命中功效。数据库量级在一两千条时这个写法响应很快,但如果以后把全国中草药库都塞进去,就要考虑给nameefficacy建索引,或者改用 SQLite 的 FTS5 全文检索。

4. 处方查询与编辑链路:WelcomeRxRecipeQuery 到 RxRecipeDetailEdit

4.1 Welcome 不是闪屏,而是工作台

初次看到WelcomeRxRecipeQuery时,容易把它当成欢迎页。仔细看命名,WelcomeRxRecipeQuery拆开是 Welcome + RxRecipe + Query,更像是“进入应用后的处方查询首页”。这类类在代码里通常继承Activity,布局由搜索框、ListView 和若干按钮组成。搜索框输入药名或功效关键词后,调用DbHelper.queryMedicine()拿到结果,再把结果交给IdentifiedStringAdapter

public class WelcomeRxRecipeQuery extends Activity { private EditText searchInput; private ListView resultList; private DbHelper dbHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_welcome); dbHelper = new DbHelper(this); searchInput = findViewById(R.id.search_input); resultList = findViewById(R.id.result_list); resultList.setOnItemClickListener((parent, view, position, id) -> { Intent intent = new Intent(this, RxRecipeDetail.class); intent.putExtra("recipe_id", id); startActivity(intent); }); } public void onSearchClick(View view) { String keyword = searchInput.getText().toString().trim(); List<Medicine> medicines = dbHelper.queryMedicine(keyword); resultList.setAdapter(new IdentifiedStringAdapter(this, medicines, keyword)); } }

这段代码里最容易踩坑的是OnItemClickListenerid参数。如果ListView用的是ArrayAdapter,这个id只是列表索引,不是数据库主键。正确做法是把Medicine.getId()先放进实体对象,然后在点击事件里通过position取出来,再放到Intent。如果项目里用的是CursorAdapterid才能直接当成数据库_id使用。

4.2 详情与编辑页通过 recipe_id 联动

处方详情页RxRecipeDetail要做的不是编辑,而是展示。它根据recipe_id查出主表字段和明细列表,并在点击“编辑”时跳到RxRecipeDetailEdit。编辑页保存成功后调用setResult(RESULT_OK),详情页收到结果后重查数据库并刷新列表。这里的关键是不要把编辑结果通过 Intent 原样传回,因为明细是一个集合,序列化麻烦,而数据库本来就是唯一事实源。

public class RxRecipeDetailEdit extends Activity { private DbHelper dbHelper; private long recipeId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); recipeId = getIntent().getLongExtra("recipe_id", -1L); dbHelper = new DbHelper(this); if (recipeId == -1L) { finish(); return; } loadRecipeDetail(recipeId); } public void onSaveClick(View view) { String title = titleInput.getText().toString(); String diagnosis = diagnosisInput.getText().toString(); dbHelper.updateRecipe(recipeId, title, diagnosis); setResult(RESULT_OK); finish(); } }

getLongExtra("recipe_id", -1L)里的-1L是哨兵值。如果某个入口忘记传recipe_id,页面会在onCreate里直接finish(),避免对不存在的处方做无意义的更新。这种防御性写法在多个页面互相跳转时很有用,尤其是接手别人留下的工程时。

4.3 明细节点的“先删后插”事务写法

编辑页最大的风险是明细不一致。假设处方原本有 3 味药,用户在编辑页删掉第 2 味,又新增了 2 味。如果不做全量同步,很容易出现某一味药在界面上看不到、数据库里还留着的情况。常见做法是“先删后插”:先删除该处方在rx_recipe_detail里的所有行,再把当前列表重新插入。这样写最直观,适合每个处方明细不超过 20 条的场景。

SQLiteDatabase db = dbHelper.getWritableDatabase(); db.beginTransaction(); try { db.delete("rx_recipe_detail", "recipe_id = ?", new String[]{String.valueOf(recipeId)}); for (RxRecipeDetailItem item : items) { ContentValues cv = new ContentValues(); cv.put("recipe_id", recipeId); cv.put("medicine_id", item.medicineId); cv.put("dose", item.dose); cv.put("note", item.note); db.insert("rx_recipe_detail", null, cv); } db.setTransactionSuccessful(); } finally { db.endTransaction(); }

这段逻辑必须放进事务。没有事务时,如果删除成功但插入中间失败,处方会变成“只剩部分药材”的脏数据。加了事务后,删除和插入要么全部成功,要么全部回滚。db.beginTransaction()db.endTransaction()必须成对出现,使用try/finally而不是try/catch,因为endTransaction()无论是否异常都要执行。

5. IdentifiedStringAdapter 的高亮实现与验证方法

5.1 用 SpannableString 做命中关键词标识

IdentifiedStringAdapter是一个处理“带标识文本”的 BaseAdapter。在列表项里把命中的关键词用底色或颜色区分出来,比直接 setText 更直观,也方便大夫在搜索结果里快速定位是哪里命中了“补气”或者“温”。

public class IdentifiedStringAdapter extends BaseAdapter { private final Context context; private final List<Medicine> data; private final String keyword; private static class ViewHolder { TextView nameView; TextView efficacyView; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = LayoutInflater.from(context).inflate(R.layout.item_medicine, parent, false); holder = new ViewHolder(); holder.nameView = convertView.findViewById(R.id.medicine_name); holder.efficacyView = convertView.findViewById(R.id.medicine_efficacy); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Medicine medicine = data.get(position); holder.nameView.setText(highlight(medicine.getName(), keyword)); holder.efficacyView.setText(highlight(medicine.getEfficacy(), keyword)); return convertView; } private SpannableString highlight(String text, String keyword) { SpannableString spannable = new SpannableString(text); if (keyword != null && !keyword.isEmpty()) { int index = text.indexOf(keyword); while (index >= 0) { spannable.setSpan( new BackgroundColorSpan(0x33FF9800), index, index + keyword.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); index = text.indexOf(keyword, index + keyword.length()); } } return spannable; } }

convertView复用不能省,否则快速滚动时列表会频繁 inflate,出现卡顿。关键词高亮用BackgroundColorSpan而不用ForegroundColorSpan,是因为底色不会和药名原有的语义颜色冲突。SPAN_EXCLUSIVE_EXCLUSIVE表示新插入内容不会被带上同一套 Span,避免多次刷新后颜色叠成一块。

5.2 安装后用 adb 验证页面栈

资源包里的zz_doctor.apk可以直接装到模拟器验证。先拿到真实包名:

aapt dump badging zz_doctor.apk | grep package

再从AndroidManifest.xml里找到WelcomeRxRecipeQuery对应的完整类名,大概率是包名.WelcomeRxRecipeQuery,然后启动:

adb shell am start -n 包名/.WelcomeRxRecipeQuery

如果界面正常弹出,说明这个类确实是可启动的入口页面。想确认跳转顺序到底是不是从WelcomeRxRecipeQueryRxRecipeDetail,可以打开应用点击一条查询结果,再执行:

adb shell dumpsys activity activities | grep -E "topResumedActivity|mFocusedApp"

输出里会显示当前前台 Activity 的完整类名。用这个方法比对每一步点击后的 Activity 变化,就能验证你在源码里推断的页面链路是否和实际安装包一致。

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

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

双层车辆路径问题(2E-VRP)MATLAB实现与ABC算法解析

简介&#xff1a;本资源是面向物流优化研究者与运筹学初学者的双层车辆路径问题&#xff08;2E-VRP&#xff09;Matlab求解方案&#xff0c;聚焦城市多级配送场景下的成本与路径协同优化&#xff0c;适用于高校课程设计、科研建模及智能算法实践。压缩包共31个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/16 14:02:09

Flask项目CSRF防护原理与实践指南

1. Flask项目中的CSRF攻击与防护原理跨站请求伪造(CSRF)是一种常见的Web安全威胁&#xff0c;攻击者诱骗用户在已认证的Web应用中执行非预期的操作。想象一下这样的场景&#xff1a;用户登录了银行网站后&#xff0c;又访问了恶意网站&#xff0c;后者悄悄向银行网站发送转账请…

作者头像 李华
网站建设 2026/9/16 14:00:10

多微信管理系统源码解析:ThinkPHP6多应用与队列实践

简介&#xff1a;一套基于ThinkPHP6框架的多微信管理系统源码&#xff0c;前端采用X-admin2.2与layui2.5.x&#xff0c;面向需要同时运营多个微信公众号、并将微信支付对接到对应企业商户的PHP开发者。无需接入微信开放平台即可完成多公众号管理与支付路由&#xff0c;框架结构…

作者头像 李华
网站建设 2026/9/16 13:56:57

agents-cli 的 variables.tf 全解:8 个关键 Terraform 变量详解

agents-cli 的 variables.tf 全解&#xff1a;8 个关键 Terraform 变量详解 【免费下载链接】agents-cli The CLI and skills that turn any coding assistant into an expert at creating, evaluating, and deploying AI agents on Google Cloud. 项目地址: https://gitcode…

作者头像 李华