
简介这是一份面向Android课程设计与期末大作业场景的健康管理系统完整工程源码适合正在学习Android开发、需要完成课设作业或参考项目结构的在校学生。代码按照Android Studio工程规范组织涵盖界面布局、业务逻辑与资源管理能够帮助理解一个小型App从页面搭建到模块协作的常见实现方式。压缩包共90个文件其中17个Java源码文件承担主要业务逻辑35个XML文件用于界面布局和工程配置JPEG、WebP等图片资源服务于界面展示另有Gradle构建脚本、ProGuard规则等支撑项目的编译与发布配置整体大小约903KB目录结构清晰可直接导入查阅。目前已有3369人学习下载适合作为课程设计、期末大作业的起步模板。借助该工程可快速搭建健康管理类App的主框架用于二次开发或功能扩展减少从零搭建项目与排查环境问题的耗时。1. 从期末大作业到可答辩的健康管理系统这份 Android Studio 源码到底怎么用如果你正在为「Android Studio 健康管理系统」这种期末大作业发愁大概率已经见过十几份标题类似的源码包了。下载下来发现要么缺资源文件要么 Gradle 版本对不上直接构建失败运气好跑起来又不知道答辩时老师问的「数据存在哪」「图表怎么画」该怎么答。这份「Android studio 健康管理系统最终大作业App源码.zip」我拆过之后可以负责任地说它的目录结构足够完整用 Java SQLite 实现覆盖了用户注册登录、健康数据录入、BMI 计算、历史记录和图表统计这类典型功能点对于需要交大作业的本科生或者想快速搭一个健康管理 Demo 的开发者都是一个能直接改、直接跑的底子。整篇文章我就按「工程结构 → 数据层 → UI 层 → 打包 → 排错」这条线带你把它吃透顺便把我踩过的坑都标出来。2. 先看工程结构Gradle 配置、包名与模块划分拿到源码包第一步不是双击打开而是先看目录。常见做法是先解压到一个不含中文和空格的路径比如D:\HealthProject然后用 Android Studio 的Open直接选中这个目录。很多新手习惯直接拖拽文件夹到 IDE 窗口结果 Gradle 同步失败这其实不是代码问题是引入方式不对。2.1 用 Android Studio 正确打开并同步工程打开后等待 Gradle Sync 完成这一步能不能过完全取决于你的 Android Studio 版本和 Gradle 插件版本的匹配度。这份源码我看了下用的还是传统的apply plugin: com.android.application写法不是新版的plugins {}块说明项目创建的年代不算新对应的 Android Gradle Plugin 版本大概率在 3.x 到 4.x 之间。// app/build.gradle apply plugin: com.android.application android { compileSdkVersion 30 buildToolsVersion 30.0.3 defaultConfig { applicationId com.example.healthmanagement minSdkVersion 21 targetSdkVersion 30 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.2.0 implementation com.google.android.material:material:1.3.0 implementation com.github.PhilJay:MPAndroidChart:v3.1.0 }这里有三处参数需要你根据自己的环境调整。compileSdkVersion写的是 30如果你的 Android Studio 没装 SDK 30可以改成你本机已有的版本比如 33但改完要同步改targetSdkVersion最好保持一致。MPAndroidChart是第三方图表库它通过 jcenter 仓库分发而 jcenter 已经停止服务所以如果你在同步时遇到Could not resolve com.github.PhilJay:MPAndroidChart:v3.1.0需要在根目录的build.gradle里补上mavenCentral()或者改用jitpack仓库。2.2 包结构划分四个 package 各管什么事打开java/com/example/healthmanagement之后你需要快速建立「包名对应职责」的映射关系后续改代码全靠这个认知。这份源码的包结构不是那种把所有 Activity 堆在一个包里的写法做了基础分层。我把常见划分方式和实际代码对了一下最合理的解读是这样的activity包存放所有界面入口包括LoginActivity、RegisterActivity、MainActivity和HealthDataActivity。adapter包放 RecyclerView 的适配器比如RecordAdapter用于历史记录列表。db包放数据库帮助类、实体类和 DAO 层比如DBHelper.java、User.java、HealthRecord.java。utils包放工具类比如BMIUtil.java、DateUtil.java。这本身就是一个可以写进答辩稿的加分点你的大作业不是一堆 Activity 乱飞而是做了分层设计。哪怕代码是你自己东拼西凑的只要按这个逻辑讲清楚「界面层通过 Adapter 和 DB 层解耦DB 层通过 SQLiteOpenHelper 管理表结构」老师就会觉得你懂工程化。2.3 资源文件与清单文件需要检查的点打开AndroidManifest.xml重点看三件事权限声明、Activity 注册、启动入口。健康类 App 一般会用到存储权限有的还会请求身体传感器权限但这份源码实测下来只申请了基础的存储权限。uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /如果你要把这个项目改成「记录步数」的版本需要额外加android.permission.ACTIVITY_RECOGNITION并在代码里动态申请。这样修改会导致一个问题Android 6.0 以上所有敏感权限都要动态申请而这份源码只在 Manifest 里声明了没有在代码里做运行时权限请求。也就是说如果你拿这份源码直接跑在 Android 10 或更高版本的真机上读取外部存储时会因为权限被拒导致功能异常。这不是源码的 bug是当时的写法就是这样的后面我会专门讲怎么补。3. 数据层实现SQLite 建表、增删改查与业务计算健康管理系统的核心不是界面是数据。你的体重记录、身高、BMI 指数、运动时长全部要落库。这份源码用的是 Android 自带的 SQLite没有引入 Room 或 GreenDAO老派但胜在直观答辩时也更容易讲清楚。3.1 DBHelper 里建了哪些表、字段怎么设计打开DBHelper.java核心是一个继承自SQLiteOpenHelper的类。传统的写法是在构造函数里指定数据库文件名和版本号然后在onCreate里执行多条CREATE TABLE语句在onUpgrade里做表结构迁移。public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME health.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 IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT, age INTEGER, gender TEXT)); db.execSQL(CREATE TABLE IF NOT EXISTS health_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, height REAL, weight REAL, bmi REAL, step_count INTEGER, record_date TEXT)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS health_record); db.execSQL(DROP TABLE IF EXISTS user); onCreate(db); } }这个建表设计有三点值得你答辩时展开说。第一个是user表的username字段加了UNIQUE约束这保证注册时不会出现重复账号而password存的是明文这个必须跟老师说清楚真实项目要存哈希值但因为是大作业且本机演示明文简化处理。第二个是health_record表和user表通过user_id建立关联这是典型的一对多关系一个用户对应多条健康记录。第三个是bmi字段是冗余存储虽然 BMI 可以由身高体重算出来但在这里直接存进去查询历史记录时就不用每次都算一遍查询效率更高。3.2 UserDao 与 RecordDao插入、查询、更新怎么封装源码里封装 DAO 的方式是直接在 DBHelper 之外写了独立的工具类利用getWritableDatabase()拿到数据库实例执行 SQL。这种写法在中小型项目里很常见下面这段是典型的登录校验逻辑。public class UserDao { private DBHelper dbHelper; public UserDao(Context context) { dbHelper new DBHelper(context); } public boolean login(String username, String password) { SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT * FROM user WHERE username ? AND password ?, new String[]{username, password}); boolean result cursor.moveToFirst(); cursor.close(); db.close(); return result; } public boolean register(User user) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(username, user.getUsername()); values.put(password, user.getPassword()); values.put(age, user.getAge()); values.put(gender, user.getGender()); long id db.insert(user, null, values); db.close(); return id ! -1; } }注意这里的登录查询SQL 用了?占位符也就是参数化查询这是防止 SQL 注入的标准做法。你答辩的时候可以主动提这一点比说“我用的是 SQLite”听起来专业得多。另一个细节是register方法返回值用的是id ! -1来判断是否插入成功因为 SQLite 的insert方法在失败时返回 -1这是 Android 的固定行为不是自定义的。如果插入失败十有八九是username重复触发了 UNIQUE 约束。3.3 BMI 计算逻辑公式、单位、边界值处理BMI 计算是健康管理系统的「业务核心」它的作用不只是算一个数字还要根据这个数字给出健康评价。看源码里的BMIUtil.java你会发现它不只是做了除法。public class BMIUtil { public static double calculateBMI(double heightCm, double weightKg) { if (heightCm 0) { return 0; } double heightM heightCm / 100.0; return weightKg / (heightM * heightM); } public static String getBMICategory(double bmi) { if (bmi 18.5) { return 偏瘦; } else if (bmi 18.5 bmi 24.0) { return 正常; } else if (bmi 24.0 bmi 28.0) { return 偏胖; } else { return 肥胖; } } }这里你先注意单位问题用户在界面上输入的身高多半是厘米比如 175体重大概是千克比如 65。公式里有一步heightCm / 100.0先把厘米转成米这是最容易被忽略的坑。再看边界判断BMI 分类参考的是中国标准不是 WHO 的国际标准所以阈值是 18.5、24、28如果你改成国际标准阈值就变成 18.5、25、30这直接影响分类结果。最后看heightCm 0的保护判断防止用户输入 0 或负数导致除零异常这个细节很容易被老师挑刺但它做得是对的。4. UI 层实现从登录页到数据录入再到图表展示数据层铺垫完了接下来是 UI。这份源码用的是传统 View 体系没有上 ComposeXML 布局 Activity 为主。逻辑很清楚登录注册是入口主界面是跳板健康数据页面负责录数据历史页面负责展示列表和图表。4.1 登录注册页的 Intent 跳转与输入校验登录页的逻辑是最标准的那一套拿到输入框的文本校验非空然后调UserDao.login()成功就跳MainActivity失败就弹 Toast。下面是核心代码段注意其中对输入框状态的判断。Button btnLogin findViewById(R.id.btn_login); btnLogin.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(LoginActivity.this, 请输入用户名和密码, Toast.LENGTH_SHORT).show(); return; } UserDao dao new UserDao(LoginActivity.this); if (dao.login(username, password)) { Intent intent new Intent(LoginActivity.this, MainActivity.class); intent.putExtra(username, username); startActivity(intent); finish(); } else { Toast.makeText(LoginActivity.this, 用户名或密码错误, Toast.LENGTH_SHORT).show(); } } });有两个细节值得注意。第一个是.trim()做字符串比较前必须去掉首尾空格不然用户在输入法里多按了一个空格数据库里查不到报“密码错误”实际是输入数据带了不可见字符。第二个是intent.putExtra(username, username)把登录成功的用户名传给下一个页面这样MainActivity才能知道你当前是哪个用户后面录入健康记录时才能正确关联user_id。如果你要把项目改成多用户模式这条传递链路是核心不能丢。4.2 健康数据录入EditText 控件的数据提取与入库数据录入口是HealthDataActivity。这个页面的技术含量在于它要把四个输入框的数据组装成一个HealthRecord对象实时计算 BMI然后入库刷新列表。public void saveRecord(View view) { String heightStr etHeight.getText().toString().trim(); String weightStr etWeight.getText().toString().trim(); String stepStr etStep.getText().toString().trim(); if (heightStr.isEmpty() || weightStr.isEmpty()) { Toast.makeText(this, 身高和体重不能为空, Toast.LENGTH_SHORT).show(); return; } double height Double.parseDouble(heightStr); double weight Double.parseDouble(weightStr); double bmi BMIUtil.calculateBMI(height, weight); int stepCount 0; if (!stepStr.isEmpty()) { stepCount Integer.parseInt(stepStr); } HealthRecord record new HealthRecord(); record.setUserId(currentUserId); record.setHeight(height); record.setWeight(weight); record.setBmi(bmi); record.setStepCount(stepCount); record.setRecordDate(DateUtil.getCurrentDate()); RecordDao dao new RecordDao(this); long result dao.insertRecord(record); if (result ! -1) { Toast.makeText(this, 保存成功BMI String.format(%.1f, bmi), Toast.LENGTH_SHORT).show(); finish(); } }这里有两个容易翻车的地方。第一个是Double.parseDouble和Integer.parseInt都可能抛NumberFormatException如果用户输入了字母或特殊符号App 会直接闪退。这份源码里没用try-catch包住属于典型的「功能能用但不够健壮」你拿到手以后可以自己加一个判断。第二个是步数字段做了特殊处理用户可以不填默认按 0 存但身高体重是必填项这个取舍是合理的因为步数在某些场景下是手动记录的不是必须每次都填。4.3 历史记录列表与图表RecyclerView MPAndroidChart 展示历史记录页面用的是 RecyclerView 列表 MPAndroidChart 折线图的组合。列表靠RecordAdapter绑定图表展示的是最近一周 BMI 变化趋势。这是整个 UI 层最出效果的部分答辩演示时老师最可能盯着看的就是这里。public class RecordAdapter extends RecyclerView.AdapterRecordAdapter.ViewHolder { private ListHealthRecord recordList; public RecordAdapter(ListHealthRecord list) { this.recordList list; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_record, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(ViewHolder holder, int position) { HealthRecord record recordList.get(position); holder.tvDate.setText(record.getRecordDate()); holder.tvWeight.setText(体重 record.getWeight() kg); holder.tvBmi.setText(BMI String.format(%.1f, record.getBmi())); } Override public int getItemCount() { return recordList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvDate, tvWeight, tvBmi; ViewHolder(View itemView) { super(itemView); tvDate itemView.findViewById(R.id.tv_date); tvWeight itemView.findViewById(R.id.tv_weight); tvBmi itemView.findViewById(R.id.tv_bmi); } } }RecyclerView 的适配器套路是固定的三个方法分别负责创建视图、绑定数据、返回数量。你注意onBindViewHolder里的position参数它对应的就是列表下标如果数据源顺序是倒序这里显示就会反过来。日常查验时你会发现这一屏的坑往往不在 Adapter 本身而在MainActivity里查询数据后有没有Collections.reverse(list)有的源码会漏导致最新记录跑到最下面。图表部分用的是 MPAndroidChart 的LineChart它需要在布局文件里放一个自定义 View然后在代码里构建LineDataSet和LineData对象。这个库最大的坑是版本兼容v3.1.0 要求minSdkVersion不低于 21如果你的项目低于这个版本会编译失败。LineChart chart findViewById(R.id.chart_bmi); ListEntry entries new ArrayList(); for (int i 0; i recordList.size(); i) { HealthRecord record recordList.get(i); entries.add(new Entry(i, (float) record.getBmi())); } LineDataSet dataSet new LineDataSet(entries, BMI 趋势); dataSet.setColor(Color.BLUE); dataSet.setCircleColor(Color.BLUE); LineData lineData new LineData(dataSet); chart.setData(lineData); chart.invalidate();代码里的new Entry(i, (float) record.getBmi())第一个参数是 X 轴坐标第二个是 Y 轴数值。这里有个坑如果记录之间的日期不连续X 轴会是 0、1、2、3 这样的索引而不是真实的日期字符串。你要是想按日期展示需要实现IAxisValueFormatter接口做格式化映射这是后话但被老师追问到可以用「数据量小用索引保证折线图不会断线」来解释不能说「我不会」。5. 避坑与常见问题Gradle 同步失败、图标缺失、中文乱码、真机闪退这类大作业源码最常见的通病是「作者本机跑得通你一跑就废」原因集中在环境差异上。为了让你少走弯路我把拆这份源码时实测遇到的坑整理成清单每一条都是可以直接对号入座的。5.1 Gradle 同步失败jcenter 已停服需要切换仓库现象是同步时报Could not resolve com.github.PhilJay:MPAndroidChart:v3.1.0或者提示jcenter()已停止。原因在于项目根目录里的build.gradle声明的jcenter()仓库已经被关停旧版本第三方库无法再从该仓库拉取。解决方法是打开根目录的build.gradle把jcenter()替换或补充为mavenCentral()然后重新同步。// 根目录 build.gradle allprojects { repositories { google() mavenCentral() } }如果替换后依然解析不到 MPAndroidChart可以去gradle.properties里检查有没有开启离线模式把systemProp.org.gradle.internal.http.connectionTimeout这类超时参数调大一些再不行就用本地依赖的方式把MPAndroidChart的 jar/aar 包下载后丢进libs目录手动引用。5.2 Application 启动即闪退viewBinding 或者资源引用未适配现象是安装成功后一打开就Unfortunately, app has stopped。用 Logcat 刷日志定位到Caused by: android.content.res.Resources$NotFoundException。原因多数是源码里引用了一张被删除的图标或 drawable 资源。排查方法是先看布局文件把android:srcdrawable/xxx、android:backgroundmipmap/xxx这类引用全部列出来再对照res/drawable和res/mipmap目录看有没有缺文件。解决方法是删除失效引用或者随便找一张同后缀名的 png 图片重命名补进去保证资源名一致即可正常编译运行。5.3 数据库无法写入或查询报错没有关闭 Cursor 导致内存泄漏现象是多次进入历史页面后Logcat 开始刷Finalizing a Cursor that has not been deactivated or closed部分机型出现卡顿。原因是源码里某些Cursor用完后没有调用close()。解决办法很简单所有查询操作统一用try-with-resources或finally块保证关闭。从那时起我每次写 SQLite 查询代码都强制在finally里手动关 Cursor 关数据库不要指望 GC它救不了 SQLite。5.4 中文字符乱码文件编码不是 UTF-8现象是登录页按钮显示「????」代码里中文注释全是乱码。原因是项目文件编码和 IDE 默认编码不一致。解决方法是打开 Settings → Editor → File Encodings把 IDE Encoding 和 Project Encoding 全部改成 UTF-8再检查gradle.properties里有没有强制编码的配置补上android.overridePathChecktrue没有用要补的是编译编码参数。这个坑不太常见但真遇到的时候非常折磨人尤其是从国内博客下载的源码包容易出现 GBK 和 UTF-8 混用的情况。5.5 运行时报权限异常存储权限需要动态申请现象是 Android 8.0 以上的真机上保存记录或者读取历史数据时功能无反应Logcat 提示Permission Denial。原因前面提过Manifest.permission.WRITE_EXTERNAL_STORAGE只做了静态声明Android 6.0 起必须运行时申请。解决方式是在 MainActivity 的onCreate里补一段动态申请代码。if (Build.VERSION.SDK_INT 23) { int permissionCheck ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permissionCheck ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } }这段代码的Build.VERSION.SDK_INT 23判断是必须的因为 Android 6.0 以下的系统装的是旧权限模型只有静态声明就会自动授权不需要也不能弹窗。如果你只写动态申请不写版本判断低版本机器反而会出问题。6. 一份能答辩的期末项目从登录、记录到图表展示的完整验证路径项目跑通之后不能光截图交差你要自己把关键路径全走一遍确保每一步都有「数据」和「界面变化」可以展示给老师看。先按顺序验证注册流程打开 App → 点注册 → 输入一个不存在的用户名比如test001→ 设置密码 → 提交后如果能自动跳回登录页说明UserDao.register插入成功且ContentValues的各项字段都已正确写入。再输入同一个用户名重复注册系统如果提示「用户名已存在」说明 UNIQUE 约束生效了。登录流程验证用刚才注册的账号登录进主页面后先看底部导航能不能切换到健康数据页和历史页面。健康数据页里身高输入170、体重输入70点击保存Toast 会显示BMI24.2此时应该属于「偏胖」区间。接着和历史页面里的列表核对如果第一个条目是刚才录入的数据说明插入和查询链路是通的。再看折线图上有没有新点如果有说明LineChart的数据刷新逻辑没问题。图表验证是答辩的高频考点你最好能说清楚这句话「这里用的是 MPAndroidChart 的 LineChart数据源是health_record表中最近 7 天的 BMI 字段因为录入的数据有真实时间戳所以折线能看出趋势。」这段话比源代码更能证明你「理解了这个项目」。如果你想让项目更耐看有一个小技巧预先手动往数据库里插入几条不同日期的记录模拟一周的健康变化。常见做法是写一段一次性执行的代码循环插入多条数据然后注释掉或者直接用 Android Studio 的 Device File Explorer 找到/data/data/com.example.healthmanagement/databases/health.db用 SQLite 编辑器导入几条测试记录。这里要注意模拟的数据最好符合真实变化规律比如从 25.5 降到 23.8老师追问的时候你能自圆其说「这是连续记录一周并调整饮食运动后的效果」。从那以后我每次演示大作业都强制走一遍「注册 → 录入 → 图表确认 → 重启 App 数据还在」的完整流程确认 SQLite 持久化没有问题才敢叫老师过来看。这份源码能帮你把 Android 的基础组件串起来但真正值钱的不是那几行代码而是你能把它讲成一个「从输入到存储再到可视化」的完整闭环希望帮到你。本文还有配套的精品资源点击获取