ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android健康管理系统源码解析:SQLite数据存储与计步传感器实战

Android健康管理系统源码解析:SQLite数据存储与计步传感器实战

简介:这是一套面向计算机相关专业学生与Android开发学习者的毕业设计完整项目包,以「健康管家」个人健康管理系统为核心,涵盖源码与配套文档说明,可直接用于毕业设计、课程设计或期末大作业。项目围绕健康数据记录、运动跟踪、饮食管理、健康提醒、数据分析报告与个性化设置等模块展开,涉及Android编程、数据库管理与数据分析等综合技能,适合希望以真实案例深化移动开发实践的学习者。压缩包共210个文件,约14.33MB,以79个xml布局与配置、57个java业务代码、32个png与8个jpg界面素材为主,另含15个so库、4个jar依赖及gradle构建脚本,目录结构完整、层次清晰。目前已有68人学习关注。通过研读源码与文档,读者可掌握功能完整、界面友好的Android应用构建流程,理解数据采集、分析与隐私保护的设计思路,并借鉴其模块划分与扩展方式,为自身项目开发提供可复用的参考方案。

1. 健康管家类 Android 应用:从一份毕业设计源码里能学到什么

很多同学拿到「Android个人健康管理系统」这个题目时,第一反应是去搜现成源码,找到一份能跑的工程,改改图标、换换名字就交差。但真正做过一轮的人会发现,这类项目里最值钱的部分不是界面,而是数据怎么存、权限怎么申请、后台任务怎么调度。健康管家这个场景天然带着几个硬骨头:步数、心率、睡眠这些数据要么来自传感器,要么来自手动录入,还要做本地持久化和统计展示。它适合两类人:一类是正在做基于 Java 的毕业设计选题、需要一份能讲清楚架构的 Android 工程;另一类是刚学完 Android 基础、想找一个完整业务闭环练手的开发者。这一章先把这类系统的边界讲清楚,后面几章再拆实现。

2. 健康管家源码的模块划分与数据流设计

2.1 为什么先定模块再写代码

拿到一份健康管理系统源码,如果直接打开 MainActivity 往下读,很容易迷路。我一般会先看目录结构,判断它是按功能分包还是按层分包。常见的做法是ui、data、model、util四层,ui放 Activity 和 Fragment,data放数据库帮助类和 Repository,model放实体类,util放日期格式化、单位换算这类工具。健康管家的业务模块通常拆成五块:用户档案、运动记录、饮食记录、体征记录、统计报表。这五块共享一个用户 ID,所有查询都带这个条件,否则多用户场景下数据会串。

模块定下来之后,数据流就清晰了:界面层调用 Repository,Repository 决定是读本地 SQLite 还是走网络,实体类负责在两者之间转换。毕业设计里绝大多数是纯本地存储,少数会接一个简单的后端。纯本地的好处是不依赖服务器,答辩时不会因为网络问题翻车;坏处是数据没法跨设备同步,这一点在写论文时要主动说明,别等老师问。

2.2 用 SQLite 建表的完整脚本

健康数据的特点是字段多、查询维度固定,用 SQLite 比用文件存储靠谱得多。下面这段建表脚本可以直接抄进DBHelper的onCreate里,注意每张表都带user_id和record_date,方便按用户和日期做聚合。

// 用户表:一个设备上可以有多个档案,比如家庭成员共用 public static final String CREATE_USER = "CREATE TABLE t_user (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "gender INTEGER DEFAULT 0," // 0 未知 1 男 2 女 + "birth_year INTEGER," + "height_cm REAL," + "weight_kg REAL," + "create_time INTEGER)"; // 运动记录表:步数、距离、时长、消耗卡路里 public static final String CREATE_SPORT = "CREATE TABLE t_sport (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "user_id INTEGER NOT NULL," + "record_date TEXT NOT NULL," // 格式 yyyy-MM-dd + "step_count INTEGER DEFAULT 0," + "distance_m REAL DEFAULT 0," + "duration_min INTEGER DEFAULT 0," + "calorie REAL DEFAULT 0)"; // 体征记录表:心率、血压、血糖、体温 public static final String CREATE_VITAL = "CREATE TABLE t_vital (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "user_id INTEGER NOT NULL," + "record_date TEXT NOT NULL," + "heart_rate INTEGER," + "systolic INTEGER," // 收缩压 + "diastolic INTEGER," // 舒张压 + "blood_sugar REAL," + "temperature REAL)";

建表时有两个参数要特别注意。record_date用 TEXT 存yyyy-MM-dd,不要用时间戳,因为统计报表按天分组时字符串比较比时间戳转换方便得多。user_id上要建索引,否则数据量到几千条以后,按用户查询会明显变慢。索引语句是CREATE INDEX idx_sport_user ON t_sport(user_id, record_date),放在建表之后执行。

2.3 Repository 层怎么写才不臃肿

很多源码把增删改查全写在 Activity 里,一个类上千行,改一处崩三处。正确做法是抽一个 Repository,把 SQL 操作封在里面,界面只拿结果。下面是一个运动记录的插入和按日查询示例。

public class SportRepository { private final DBHelper helper; public SportRepository(Context context) { this.helper = DBHelper.getInstance(context); } // 插入或更新当天记录:同一天同一用户只保留一条 public void saveToday(int userId, String date, int step, float distance) { SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("user_id", userId); cv.put("record_date", date); cv.put("step_count", step); cv.put("distance_m", distance); // 先删后插,避免唯一约束冲突,逻辑简单不易错 db.delete("t_sport", "user_id=? AND record_date=?", new String[]{String.valueOf(userId), date}); db.insert("t_sport", null, cv); db.close(); } // 查询最近 7 天的步数,返回日期到步数的映射 public Map<String, Integer> queryLast7Days(int userId) { Map<String, Integer> result = new LinkedHashMap<>(); SQLiteDatabase db = helper.getReadableDatabase(); Cursor c = db.rawQuery( "SELECT record_date, step_count FROM t_sport " + "WHERE user_id=? ORDER BY record_date DESC LIMIT 7", new String[]{String.valueOf(userId)}); while (c.moveToNext()) { result.put(c.getString(0), c.getInt(1)); } c.close(); db.close(); return result; } }

saveToday用先删后插而不是replace,是因为replace在存在外键关联时行为不好预测,毕业设计里没必要冒这个险。queryLast7Days返回LinkedHashMap是为了保持日期顺序,画折线图时直接遍历就行。注意Cursor和SQLiteDatabase都要关,不关的话在低端机上跑久了会报Too many open files,这个坑我在真机上遇到过。

3. 权限申请与传感器数据采集的落地细节

3.1 Android 6.0 之后的运行时权限怎么处理

健康管家要用到计步传感器和存储权限,从 Android 6.0 开始这些都得动态申请。常见做法是在进入主界面时检查ACTIVITY_RECOGNITION和WRITE_EXTERNAL_STORAGE,没有就弹窗。注意ACTIVITY_RECOGNITION是 Android 10 才独立出来的权限,之前的版本计步不需要它,所以代码里要判断版本号。

private void requestHealthPermissions() { List<String> need = new ArrayList<>(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { need.add(Manifest.permission.ACTIVITY_RECOGNITION); } if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { need.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } if (!need.isEmpty()) { ActivityCompat.requestPermissions(this, need.toArray(new String[0]), REQ_CODE); } }

参数上要留意:requestPermissions的请求码自己定,回调里用同一个码判断。用户拒绝后不要反复弹,第二次拒绝就该引导去设置页,否则系统会直接静默拒绝,表现为「代码没报错但权限就是没有」,这是典型的玄学问题,其实是系统策略。

3.2 计步传感器的三种模式与选择

Android 的计步传感器有两种:TYPE_STEP_COUNTER返回开机以来的总步数,TYPE_STEP_DETECTOR每次检测到一步就触发一次。健康管家一般用STEP_COUNTER,因为它省电且数据稳定。注册监听时用SENSOR_DELAY_NORMAL就够,用SENSOR_DELAY_FASTEST会明显增加耗电,答辩演示时看不出来,但真机放一晚上掉电很快。

private SensorManager sensorManager; private Sensor stepSensor; private int baseStep = -1; // 首次读到的总步数作为基准 private final SensorEventListener stepListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { float total = event.values[0]; if (baseStep < 0) { baseStep = (int) total; // 第一次记录基准 return; } int todayStep = (int) total - baseStep; tvStep.setText(String.valueOf(todayStep)); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }; // 注册 sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER); if (stepSensor != null) { sensorManager.registerListener(stepListener, stepSensor, SensorManager.SENSOR_DELAY_NORMAL); }

baseStep这个基准值必须持久化,否则应用重启后步数会从零重新算。我一般把它存进 SharedPreferences,键名带日期,跨天时重置。onAccuracyChanged空实现即可,但别删,删了编译不过。

3.3 前台服务保活与电量优化的取舍

如果希望退到后台还能计步,得用前台服务,并在通知栏常驻一条通知。Android 8.0 之后后台服务受限,不开前台服务的话,锁屏几分钟后监听就断了。但前台服务也有代价:通知栏一直有条通知,用户体验一般。毕业设计里我的建议是只在「运动记录」页面开启监听,离开页面就注销,这样既省电又不用处理复杂的保活逻辑。真要后台计步,就老老实实写前台服务,别用各种黑科技,那些在答辩现场演示时容易翻车。

4. 统计报表与图表展示:把数据变成能看懂的东西

4.1 用 MPAndroidChart 画折线图的配置要点

统计页是健康管家的门面,老师第一眼就看这个。常见做法是用 MPAndroidChart 画最近 7 天步数折线图。引入依赖后在布局里放LineChart,然后配置数据。下面这段是核心配置。

LineChart chart = findViewById(R.id.chart_step); List<Entry> entries = new ArrayList<>(); Map<String, Integer> data = repo.queryLast7Days(userId); int i = 0; for (Map.Entry<String, Integer> e : data.entrySet()) { entries.add(new Entry(i++, e.getValue())); } LineDataSet set = new LineDataSet(entries, "步数"); set.setColor(Color.parseColor("#4CAF50")); set.setCircleColor(Color.parseColor("#4CAF50")); set.setLineWidth(2f); set.setDrawValues(true); // 显示数值 set.setValueTextSize(10f); LineData lineData = new LineData(set); chart.setData(lineData); chart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); chart.getXAxis().setGranularity(1f); // 每个点占一格 chart.getAxisRight().setEnabled(false); // 关掉右轴,避免干扰 chart.getDescription().setEnabled(false); chart.invalidate(); // 刷新

setGranularity(1f)很关键,不设的话 X 轴会按浮点间隔画,7 个点可能只显示 3 个标签。setDrawValues(true)会把数值标在点上,数据少的时候好看,数据多了会挤,可以按需关掉。invalidate()别忘了,改完数据不刷新是新手最常见的「图不出来」原因。

4.2 按周和按月聚合的 SQL 写法

日报之外,通常还要周报和月报。周报按strftime('%W', record_date)分组,月报按substr(record_date, 1, 7)分组。下面这条查月均步数。

SELECT substr(record_date, 1, 7) AS month, AVG(step_count) AS avg_step, SUM(calorie) AS total_calorie FROM t_sport WHERE user_id = ? GROUP BY month ORDER BY month DESC LIMIT 6;

substr(record_date, 1, 7)取出yyyy-MM,比用日期函数转换更直接。AVG和SUM一起查,一次拿到月均步数和总消耗。注意LIMIT 6只取最近半年,数据多了图表也画不下。

4.3 空数据和异常值的处理

真实使用中经常出现某天没记录的情况,直接画图会断线。我的做法是在查询结果里补零:先算出最近 7 天的日期列表,再和查询结果做映射,缺的补 0。异常值方面,步数偶尔会跳到一个极大值,比如传感器抖动导致某分钟几千步,可以在插入前做个上限判断,超过 50000 步的丢弃。这些细节写进论文的「数据处理」一节,比单纯贴界面截图有说服力。

5. 避坑与常见问题排查

5.1 数据库升级后旧表字段丢失

现象:改了实体类加了一个字段,重新运行后查询报no such column。原因是DBHelper的版本号没改,onUpgrade没触发。解决:把DATABASE_VERSION加一,在onUpgrade里写ALTER TABLE加字段,或者简单点直接DROP重建,但重建会丢数据,正式项目别这么干。

5.2 计步数据在部分机型上始终为零

现象:代码逻辑没问题,但某些手机上步数一直是 0。原因是这些机型没有TYPE_STEP_COUNTER硬件传感器,或者被系统省电策略屏蔽了。解决:注册前先判断stepSensor != null,为空时降级为手动录入,并在界面上给个提示,别让用户对着零发呆。

5.3 图表在低版本 Android 上崩溃

现象:在 Android 7 以下的设备上打开统计页直接闪退。原因是 MPAndroidChart 新版本用了 Java 8 的 API,低版本不兼容。解决:在build.gradle里开desugaring,或者把minSdkVersion提到 21 以上。毕业设计一般要求兼容到 5.0,所以 desugaring 更稳妥。

5.4 权限被拒后功能静默失效

现象:用户点了拒绝,之后所有计步相关功能都不工作,但没有任何报错。原因是代码里只判断了权限是否授予,没处理拒绝分支。解决:在onRequestPermissionsResult里判断grantResults,被拒时弹一个说明对话框,告诉用户去设置里开,并提供一个跳转按钮。

5.5 导出数据时文件路径写错

现象:点导出没反应,或者提示成功但找不到文件。原因是 Android 10 之后分区存储生效,直接往/sdcard/写会被拦。解决:用getExternalFilesDir(null)拿应用私有目录,路径是/storage/emulated/0/Android/data/包名/files/,不需要额外权限,导出后通过FileProvider分享出去。

6. 从能跑到能讲:把源码变成答辩时的底气

一份健康管家源码能跑起来只是起点,答辩时老师更关心你为什么这么设计。我的习惯是给每个模块准备一句话的「设计理由」:为什么用 SQLite 不用文件,因为要按日期聚合;为什么用前台服务,因为后台监听会被杀;为什么先删后插,因为避免唯一约束冲突。这些理由比代码本身更能体现你的思考。

再进一步,可以给系统加一个「数据校验」的小功能:每次插入前检查心率是否在 30 到 220 之间,血压是否在合理范围,超出就提示用户确认。这个功能代码量不大,但能体现你对健康数据严肃性的理解。验证方法也简单,手动造几条异常数据,看系统是否拦截,截图放进论文的测试章节。

最后说个我自己的教训:第一次做这类项目时,我把所有逻辑堆在 Activity 里,答辩前想改一个查询条件,结果牵动五个文件,熬到凌晨三点。后来重构成 Repository 模式,同样的改动只动一个方法。所以如果你现在手里的源码还是一团乱麻,别急着加功能,先花半天把数据层抽出来,后面会省下大量时间。希望帮到你。

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

返回列表