ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android健康管理系统大作业开发实战:从数据库设计到答辩全程详解

Android健康管理系统大作业开发实战:从数据库设计到答辩全程详解 简介本资源是面向高校计算机类专业本科生的Android移动应用开发期末大作业完整实现——一款基于Android Studio开发的健康管理系统App源码工程。项目覆盖用户注册登录、健康数据录入如体重、血压、运动时长、可视化图表展示及日常提醒等核心功能适合作为移动开发课程设计或实训项目参考。压缩包共90个文件包含35个XML布局与配置文件、17个Java业务逻辑代码、14张JPEG图标资源、11个WebP图片素材以及Gradle构建脚本、ProGuard混淆规则、Git版本控制配置等关键工程文件整体体积仅903KB结构规范、依赖精简开箱即用。已有3365人下载学习源码经实测可在Android Studio中直接导入编译运行配套目录组织清晰含app/src/main/标准结构、gradle wrapper、build.gradle等便于理解Android MVVM基础架构与模块化开发实践。 做Android大作业尤其是健康管理系统这种每年都被选烂了的经典题目最尴尬的状态是功能全堆上去了答辩的时候被老师一问为什么用SQLite不用SharedPreferences你的表结构怎么设计的当场卡壳。这篇文章把我从拿到题目到最终交付整套健康管理系统App源码的完整过程写下来包含需求拆解、数据层设计、核心模块实现、UI架构、调试踩坑和答辩准备希望能给正在被这个题目折磨的同学一条可以直接落地的路线。这套App选的是Java SQLite MPAndroidChart的组合没有引入任何重量级框架主打一个代码自己看得懂、答辩讲得清楚、功能对得起大作业题目。如果你正准备用Android Studio做类似的管理系统类项目这篇内容的工程思路和排错经验都可以直接复用。1. 大作业需求拆解健康管理系统不是能跑就行1.1 功能清单的取舍思路健康管理系统听起来范围很广但作为Android Studio大作业核心就是围绕健康数据做增删改查加可视化。我见过不少同学一上来就想做运动轨迹、饮食拍照识别、心率监测联动最后由于时间和技术储备不够项目烂尾只能交一个半成品。我的做法是先把题目拆成四个必须满足的硬功能用户注册与登录能区分不同用户的健康数据健康数据录入包括步数、体重、睡眠时长、血压等历史记录列表支持查看、修改、删除图表统计直观展示一段时间的数据变化趋势在此基础上再添两个加分项每日健康贴士和个人信息完善。这四个硬功能加两个加分项已经足够撑起一个大作业的体量而且每一项都能在答辩时清晰说明我做了什么、怎么做、为什么这么做。1.2 技术选型Java还是Kotlin要不要用第三方库关于语言选择我强烈建议按你自己的实际水平来。如果平时上课和实验用Java就别为了显得高级硬上Kotlin。大作业的本质是展示你对已有知识的掌握而不是冒险尝试新东西。我最终选了Java原因很简单网上资料多报错能搜到答案老师讲的是Java答辩不会被追问到怀疑人生。第三方库我只用了一个——MPAndroidChart来做图表。其余的比如网络请求、图片加载、数据库框架全部用Android原生API手写。这有两点好处一是减少依赖冲突二是答辩时老师问你这个功能怎么实现的你可以理直气壮说用的SQLiteOpenHelper自己封装的DAO而不是含糊地说用了Room然后……。再就是项目结构。建议按包名划分好层次哪怕功能不多也要有这五个包activity存放所有Activityfragment存放主界面的各个Fragmentadapter存放各种列表适配器db数据库帮助类和数据访问对象model实体类这样分层看起来工程量更大但实际写代码时逻辑清晰很多后期改bug也快。后面真正折磨你的不是写功能而是功能多了之后到处找代码。2. 数据层设计把用户、运动、饮食、体征数据理清楚2.1 数据库表结构与字段设计数据层的设计决定了后面所有功能的开发效率。健康管理系统最核心的数据是用户的体征记录我的数据库一共设计了四张表用户表、健康记录表、健康贴士表、个人信息表。用户表存放账号密码健康记录表是核心业务表存的是每一次录入的身体数据健康贴士表是预置的每日建议内容个人信息表用来扩展用户的年龄、身高、性别等基础信息。四张表的关联关系是用户表和健康记录表通过user_id字段关联用户表和个人信息表通过user_id字段关联。这里最需要注意的一个设计决策是健康记录表不要用自增id做业务标识而是用user_id date的组合来区分某个人在某个日期的数据。原因很简单一个用户一天只需要一条体重记录、一条步数记录用日期做唯一性约束可以避免重复录入。下面是我实际建表的SQL语句这段在答辩时可以直接拿出来讲CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE health_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, record_date TEXT NOT NULL, weight REAL, steps INTEGER DEFAULT 0, sleep_hours REAL, systolic INTEGER, diastolic INTEGER, memo TEXT, FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE TABLE profile ( user_id INTEGER PRIMARY KEY, nickname TEXT, gender TEXT, age INTEGER, height REAL, target_weight REAL );字段类型上我把体重设置为REAL步数设置为INTEGER睡眠时长REAL血压分收缩压和舒张压两个整型字段。特别提醒一点record_date一定要用TEXT类型存格式统一为yyyy-MM-dd不要用INTEGER存时间戳。因为后续要按日期查询和排序文本格式的日期在SQLite里可以直接用字符串比较写SQL语句时非常省心。2.2 SQLiteOpenHelper与DAO层封装数据访问层我采用的是最朴素的写法一个DBHelper继承SQLiteOpenHelper负责建库建表一个HealthRecordDao封装增删改查方法。没有引入GreenDAO、Room这些框架因为大作业场景下手写SQL反而更透明出问题也更好排查。DBHelper的核心代码需要关注两个方法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 user (...)); db.execSQL(CREATE TABLE health_record (...)); db.execSQL(CREATE TABLE profile (...)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 简单粗暴正式项目不能这么干但大作业够用 db.execSQL(DROP TABLE IF EXISTS health_record); onCreate(db); } }注意onUpgrade里我写的是删表重建这在正式项目里是绝对禁止的会导致用户数据丢失。但大作业场景下数据库版本迭代很少发生这样写不影响功能演示。如果你愿意多花点时间可以改成ALTER TABLE添加新列的方式答辩时还能多一个加分点。DAO层我建议每个实体类对应一个DAO。HealthRecordDAO里至少要有这几个方法insert、update、deleteById、getRecordByUserIdAndDate、getAllRecordsByUserId。这里有一个很关键的手写封装细节——如果你反复查询某个用户的记录列表SQL语句会拼接很多次所以我用ContentValues来做参数绑定既避免SQL注入问题代码也更整洁public long insert(HealthRecord record) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(user_id, record.getUserId()); values.put(record_date, record.getRecordDate()); values.put(weight, record.getWeight()); values.put(steps, record.getSteps()); values.put(sleep_hours, record.getSleepHours()); values.put(systolic, record.getSystolic()); values.put(diastolic, record.getDiastolic()); values.put(memo, record.getMemo()); long id db.insert(health_record, null, values); db.close(); return id; }2.3 数据版本升级与种子数据初始化大作业项目一般不太用到数据库版本升级但健康贴士这种功能需要预置数据。我是在DBHelper的onCreate里顺带把贴士数据插入进去的Override public void onCreate(SQLiteDatabase db) { // 建表... insertDefaultTips(db); } private void insertDefaultTips(SQLiteDatabase db) { String[] tips { 每天喝够8杯水促进新陈代谢。, 连续久坐不超过90分钟适当起身活动。, 成年人每天建议睡眠7-8小时。, 每餐保证有蔬菜控制油盐摄入。, 每周至少进行150分钟中等强度运动。 }; for (String tip : tips) { ContentValues values new ContentValues(); values.put(content, tip); db.insert(health_tips, null, values); } }这里要提醒一个我踩过的坑SQLite插入中文数据时如果没设置好编码很容易出现乱码。解决办法是确保Android Studio工程的编码统一为UTF-8同时SQLite本身是支持UTF-8的只要代码文件是UTF-8编码写的中文插入后查询出来就没问题。如果你发现模拟器里显示乱码先检查File - Settings - Editor - File Encodings里的全局编码设置。3. 核心模块实现登录注册、数据录入、图标统计3.1 注册登录模块MD5加盐与登录态保持登录注册是几乎所有管理系统类App的第一个模块也是最容易被老师追问安全性的地方。很多同学的代码是密码明文存数据库查询时直接select * from user where usernamexxx and passwordxxx。这种做法演示没问题但答辩时老师问一句密码安全吗你就只能尴尬。我的处理方式是MD5加盐存储。虽然MD5不算最强加密方案但对于Android Studio大作业来说完全够用而且实现简单、讲得清楚。public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes()); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } // 注册时加盐MD5(username password) String encryptedPwd MD5Utils.md5(username password); // 登录校验 String encryptedInput MD5Utils.md5(username inputPassword); // 和数据库中该用户名对应的密码字段比较加盐的方式我选了用户名拼接密码这样即使用户名相同、密码不同的场景下也能保证密文有区分度。注册时先查一遍用户名是否已存在登录时先查用户再比对密码不要直接拿账号密码做联合查询这样能区分用户不存在和密码错误两种提示。登录态保持方面我用的是Application全局变量加SharedPreferences配合的方案。登录成功后把当前用户ID存到SharedPreferencesApp重启后自动读取如果没登录就跳回登录页登录了就直接进主界面。这种方案不涉及Token、Session这些Web概念在纯本地App里简单实用。3.2 健康数据录入表单校验与日期选择健康记录录入页面是整个项目的核心交互页面字段包括日期、体重、步数、睡眠时长、收缩压、舒张压、备注。这个页面的技术难点不在布局而在三个细节处理日期不手动输入用DatePickerDialog让用户选择。这样既能保证格式统一为yyyy-MM-dd也从源头杜绝了用户乱输日期导致的查询问题。体重、睡眠用EditText填数字步数和血压用数字键盘。关键是要给EditText设置inputTypeEditText android:inputTypenumberDecimal ... / EditText android:inputTypenumber ... /numberDecimal允许小数点适合体重和睡眠时长number只允许整数适合步数和血压。这一步必须在设计阶段就考虑好否则用户输入字母程序就会崩溃。表单校验我写了一个简单方法每次保存前逐项检查private boolean validateInput() { if (weightInput.getText().toString().isEmpty()) { Toast.makeText(this, 请输入体重, Toast.LENGTH_SHORT).show(); return false; } double weight Double.parseDouble(weightInput.getText().toString()); if (weight 20 || weight 300) { Toast.makeText(this, 体重数据不合理, Toast.LENGTH_SHORT).show(); return false; } // 其他字段同理... return true; }这里建议对数据范围做一个合理性判断比如体重20到300公斤步数0到10万步睡眠0到24小时。这个逻辑不仅防止异常数据入库答辩时还能说明你在健壮性设计上的考虑。录入之后当天如果已经有记录应该走更新而不是新增。我是在页面加载时根据当前用户 今天日期查一次数据库有记录就回填表单保存时执行update没记录就执行insert。这比让用户自己选择新增还是修改体验好得多。3.3 图表统计MPAndroidChart展示体重和步数趋势图表模块是健康管理系统的亮点功能也是视觉上最能打动老师的部分。我用的是MPAndroidChart库的LineChart和BarChart两种图折线图展示体重变化趋势柱状图展示最近一周的步数。集成方式是在build.gradle里加一行依赖implementation com.github.PhilJay:MPAndroidChart:v3.1.0使用MPAndroidChart有三个容易踩的坑需要特别注意。第一个坑是X轴的日期格式。MPAndroidChart的X轴默认是按索引显示的直接给它setValues传字符串列表它会均匀分布但如果数据有缺失日期X轴和数据的对应关系就会错位。我的解决方法是先构建一个完整的日期列表从今天往前推7天查询数据时按日期放进Map缺失的日期补0这样图表X轴就不会乱。第二个坑是数据集的数据类型。LineChart的Entry构造函数是(float x, float y)x不是时间戳而是数据点的索引很多人误以为能直接传时间。必须用一个自增的循环变量做x值再对应日期列表生成图例说明。第三个坑是MarkerView也就是点击数据点弹出的气泡提示。这个功能默认实现比较麻烦需要自定义一个View继承MarkerView并重写getOffset和refreshContent方法。如果时间紧张可以不做这个交互只保留图例就能满足大作业要求。下面是我生成体重折线图的核心逻辑这段代码答辩时讲出来就是加分项ListEntry entries new ArrayList(); String[] last7Days getLast7Days(); // 返回从旧到新的日期数组 MapString, HealthRecord recordMap getRecordsByDateList(last7Days); for (int i 0; i last7Days.length; i) { HealthRecord record recordMap.get(last7Days[i]); float weight (record ! null record.getWeight() ! null) ? record.getWeight().floatValue() : 0f; entries.add(new Entry(i, weight)); } LineDataSet dataSet new LineDataSet(entries, 体重); dataSet.setColor(Color.parseColor(#4CAF50)); dataSet.setCircleColor(Color.parseColor(#4CAF50)); LineData lineData new LineData(dataSet); lineChart.setData(lineData); lineChart.getXAxis().setValueFormatter(new IndexAxisValueFormatter(last7Days)); lineChart.getDescription().setEnabled(false); lineChart.invalidate();4. UI与导航架构底部导航栏、Fragment、自定义控件4.1 底部导航加Fragment的主框架健康管理系统App的主界面我采用的是底部导航栏 四个Fragment的经典结构四个Tab分别是首页、数据、贴士、我的。如果你用的Android Studio版本较新模板本身就带Bottom NavigationView但大作业场景下我建议手动搭一遍因为模板有时候生成的代码太繁琐反而不容易讲清楚。核心的实现方式是通过FragmentManager在FrameLayout容器中切换Fragment配合BottomNavigationView的OnNavigationItemSelectedListener做联动。这里最关键的一个设计决策是使用FragmentTransaction的show/hide方式而不是replace方式。原因有两个一是show/hide可以保持Fragment的状态用户切换Tab后之前输入的内容不会丢失二是replace会在切换时重新创建Fragment图表数据每次都要重新加载明显卡顿。private void switchFragment(Fragment targetFragment) { FragmentTransaction transaction getSupportFragmentManager().beginTransaction(); if (!targetFragment.isAdded()) { transaction.add(R.id.fragment_container, targetFragment); } else { transaction.show(targetFragment); } // 隐藏其他三个Fragment transaction.hide(homeFragment); transaction.hide(dataFragment); transaction.hide(tipsFragment); transaction.hide(mineFragment); transaction.show(targetFragment); transaction.commit(); }4.2 shape drawable与主题风格统一健康管理系统的主题风格我会选择绿色系因为绿色天然和健康、生命绑定视觉上也比默认的白色加紫色主题更贴合题目。统一风格的做法是先在colors.xml里定义一组主色调然后在drawable目录下用shape drawable来定义按钮背景、输入框背景、卡片背景。一个典型的圆角按钮背景?xml version1.0 encodingutf-8? shape xmlns:androidhttp://schemas.android.com/apk/res/android android:shaperectangle solid android:colorcolor/primary_green / corners android:radius20dp / /shape所有Button都引用这个背景就能保证全局风格统一。同理卡片式布局用CardView设置cardCornerRadius和elevation页面整体质感立刻就上来了。这一点是很多大作业容易忽略的地方——功能全但UI粗糙老师第一印象就打了折扣。4.3 布局适配与权重的使用经验大作业通常只要保证模拟器和自己的手机显示正常就行但为了防止答辩现场连一台分辨率差距大的设备导致布局错乱我还是做了一些基础适配工作。最常用的是LinearLayout的layout_weight权重来做等分和自适应。比如首页的四个统计卡片用水平LinearLayout里放四个等宽的TextView每个设置layout_width0dp和layout_weight1这样无论屏幕宽度多少都能等分。另一个重点是ScrollView套LinearLayout时内层LinearLayout要设置android:fillViewporttrue否则内容不满屏时背景色会有空白。另外建议在AndroidManifest.xml里给Activity设置screenOrientationportrait锁定竖屏。一方面避免横竖屏切换时Activity重建导致数据丢失另一方面也减少适配工作量。这是一个非常实用的小技巧很多初学者不知道不锁竖屏的话切一下屏幕Activity就重建了正在填写的表单内容全没了。5. 数据传递与联动Intent、Bundle、接口回调的配合5.1 页面跳转传参的两种方式健康管理系统里页面跳转不少比如从首页点击一条记录跳到详情页、从个人中心跳到修改密码页。传参方式我用的是Intent的putExtra加Bundle。普通参数直接Intent.putExtra就可以了Intent intent new Intent(HomeActivity.this, RecordDetailActivity.class); intent.putExtra(record_id, record.getId()); intent.putExtra(record_date, record.getRecordDate()); startActivity(intent);在目标页面通过getIntent().getStringExtra(record_date)取出来。这种方式对大作业来说完全够用。Serializable序列化传整个对象也可以但我不建议在数据量小的时候用它——一来要改实体类实现Serializable接口二来如果以后字段改了容易出现序列化版本不一致的坑。传主键ID目标页面再查数据库反而是最不容易出错的做法。5.2 Fragment与Activity通信的推荐方案Fragment和Activity之间的通信是很多初学者的大坎。首页Fragment里点击添加记录按钮需要让Activity响应并跳转数据Fragment加载时需要从数据库读取数据。我的做法是接口回调为主findViewById强制转换只用在特殊情况。比如在HomeFragment中定义接口public class HomeFragment extends Fragment { public interface OnRecordClickListener { void onAddRecordClick(); void onRecordItemClick(int recordId); } private OnRecordClickListener listener; Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnRecordClickListener) { listener (OnRecordClickListener) context; } } // 按钮点击时调用 listener.onAddRecordClick() }MainActivity实现这个接口在回调方法里做页面跳转。这样做的好处是Fragment不直接依赖具体的Activity实现以后把Fragment复用到其他Activity里也不会有问题。答辩时如果老师问Fragment怎么和Activity通信你能说出接口回调这四个字就已经超过一半的同学了。5.3 全局数据与登录态的保存多页面共享用户ID最省事的方案是写一个全局的UserManager类用static字段保存当前登录用户信息。但这里要特别提醒App进程被系统杀掉之后static字段会被清空如果直接访问就会空指针崩溃。所以我的处理方案是双保险内存中保留一个User对象供页面快速读取同时在SharedPreferences里持久化一个userId。每次进入MainActivity时先从SharedPreferences读取如果发现内存中没有用户对象再从数据库重新加载。这样既保证了速度又保证了进程被杀后重启App不会崩溃。这里还有一个我踩过的坑在Application的onCreate里初始化数据库和全局配置没毛病但绝对不要在里面做耗时操作比如网络请求。虽然大作业没有网络请求但这个习惯要养成——Application的onCreate如果卡顿App启动就会出现白屏模拟器上格外明显。6. 调试与答辩准备这些坑踩过才算真正做完6.1 模拟器卡顿、闪退与ANR排查Android Studio自带的模拟器在老电脑上经常卡到怀疑人生这里把我的经验整理一下。首先是启动模拟器时在AVD设置里把内存调到2GB以上图形渲染选Hardware启用Host GPU。如果还是卡就改用Genymotion或者直接连真机开发。真机调试其实比模拟器更靠谱尤其是涉及传感器、定位这类功能模拟器完全是另一个体验。Flashback和ANR是最常见的两类问题。ANR一般是主线程做了耗时操作导致的比如在onCreate里直接查询大量数据。解决办法其实很简单如果数据量预估超过几百条就把查询放到子线程执行然后在主线程更新UI。但大作业的数据量通常很小在SQLite里查几百条记录根本不到1毫秒放主线程完全没问题。所以遇到ANR先检查是不是有什么网络请求或死循环放在了主线程而不是盲目引入线程池。6.2 中文乱码和编码问题的根因与解法中文乱码在Android Studio里主要有两种表现一种是在布局文件和Java代码里的中文都正常但数据库里存进去再读出来是乱码另一种是代码里显示正常但运行到模拟器上按钮文字变成了乱码。这两种情况的排查思路完全不同。第一种先去检查File Encoding设置Project Encoding和Default Encoding都要统一为UTF-8。第二种检查一下是不是使用了ASCII编码的String资源文件或者strings.xml里忘记加编码声明。还有一个容易忽略的点Gradle脚本里如果写了compileOptions指定了编码也要和工程编码保持一致。我在做这个项目时曾经因为全局编码被改成了GBK导致一批中文注释和字符串全部乱码最后整个工程重新同步Gradle才恢复。这个问题很隐蔽因为代码编辑器里看是正常的编译出来才出问题。6.3 RecyclerView刷新闪烁与数据更新时机健康记录列表我用的是RecyclerView遇到的最典型问题是删除一条记录后调用notifyDataSetChanged()列表虽然更新了但滚动位置上跳了视觉上有闪烁感。这个问题的根因是notifyDataSetChanged()会重建所有可见的ItemHolder没有做精准的差异更新。解决思路是尽量用notifyItemRemoved(position)而不是notifyDataSetChanged()并且先删除数据源里的对应项再通知适配器。如果一定要用notifyDataSetChanged()那就在删除操作后先保存当前列表的滚动位置刷新完成后恢复scrollPosition。这个细节看起来小但演示的时候如果列表位置乱跳观感很差。删除逻辑的代码大致是这样的recordList.remove(position); recordAdapter.notifyItemRemoved(position); recordAdapter.notifyItemRangeChanged(position, recordList.size() - position);6.4 答辩演示的准备工作与代码讲解技巧代码写完只是完成了70%剩下30%是答辩准备。我建议花一个晚上做三件事。第一把每个Activity和Fragment的职责用一句话说清楚。比如MainActivity负责底部导航与Fragment切换HomeFragment负责展示首页健康摘要RecordEditActivity负责录入和修改健康记录。自己先在纸上写一遍讲的时候才不卡壳。第二选两段代码重点准备一段是数据库的增删改查封装一段是图表数据加载。这两段最能体现你的编程能力。准备的时候要想好三个问题的答案这段代码解决了什么问题为什么用这种写法如果数据量变大或需求变化怎么扩展第三提前录一个演示视频或者准备一个备用演示环境。答辩现场最怕设备突然出问题HDMI转接头不兼容、模拟器起不来、手机连不上电脑这些我都遇到过。提前录一份视频存在U盘里绝对是最稳妥的方案。我实际做这个项目时最深的体会是健康管理系统这个题目难的不是某个技术点而是如何把一堆分散的功能串成一个有机的整体。从数据库表结构设计到Fragment通信从数据校验到图表联动每一步走扎实最后的结果不会差。如果你正准备交这个题目的作业不要急着写代码先把表和页面之间的调用关系画清楚再动手。代码可以边写边想架构不行。本文还有配套的精品资源点击获取
返回列表