简介这是一套面向安卓初学者与课程设计需求者的完整健身系统源码基于Android Studio开发适合作为移动应用开发大作业或练手项目。项目覆盖登录注册、个人信息管理、运动项目列表与锻炼计时等模块并融入MD5加密、Sqlite数据库、VideoView视频播放、OkHttp网络通信、RecycleView列表、ViewPager轮播图、Glide图片加载及Handler异步消息等十余个核心知识点能帮助读者系统串联安卓开发技能。压缩包共626个文件约85.28MB包含java源码、xml布局、png与jpg图片资源、json数据、jar依赖及apk安装包等结构完整可直接导入运行。目前已有1728人学习下载适合需要完整赛题方案、排错思路与模块化代码参考的读者便于快速理解项目架构并二次修改。1. 从课程设计到能跑通的健身 AppAndroid Studio 里到底要写多少东西每年期末总有一批人打开 Android Studio 准备做健身系统课程设计然后卡在第一个 Activity 上。这个标题看着像作业实际拆开是一套完整的安卓开发最小闭环多页面导航、SQLite 本地存储、列表适配器、计时器逻辑、图表展示。健身系统的好处是业务逻辑不复杂但该有的模块一个不少拿来练手刚好。适合两类人一是第一次做安卓课程设计、不知道从哪下手的新手二是做过记账本或计算器想加个计时和数据统计功能的人。下面按我实际做过的路径从建工程到打包把每一步的参数和坑都写清楚。2. 工程搭建与数据库设计先把骨架立住2.1 新建工程时选哪个模板打开 Android Studio选 Empty Views Activity不要选 Compose 那个。课程设计场景下XML 布局加 Java 或 Kotlin 的写法资料最多出问题好搜。语言选 Java 还是 Kotlin 看课程要求如果老师没指定我一般用 Java因为网上课程设计案例源码大部分是 Java 的抄改方便。最低 SDK 选 API 24Android 7.0覆盖率高而且 SQLite 的 API 在这个版本上很稳定。Gradle 配置用默认的 Groovy DSL 就行别折腾 Kotlin DSL新手容易在 build.gradle.kts 的语法上翻车。# 新建工程后先跑一次确认环境没问题 # 在 Android Studio 底部 Terminal 里执行 ./gradlew assembleDebug这一步的目的是确认 Gradle 能正常下载依赖并编译。如果卡在 Download 阶段检查 Settings 里的 HTTP Proxy 是否配了镜像。国内环境建议在项目根目录的 settings.gradle 里加上阿里云镜像// settings.gradle dependencyResolutionManagement { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } google() mavenCentral() } }参数说明google()和mavenCentral()是官方源阿里云镜像放在前面可以加速。注意顺序镜像在前、官方在后这样找不到的包还能回落到官方源。2.2 数据库表结构怎么定健身系统核心就三张表用户表、运动记录表、训练计划表。课程设计不用搞太复杂但字段类型要选对。表名字段类型说明useridINTEGER PRIMARY KEY AUTOINCREMENT主键自增userusernameTEXT NOT NULL用户名userpasswordTEXT NOT NULL密码存明文即可课程设计不要求加密userheightREAL身高 cmuserweightREAL体重 kgrecordidINTEGER PRIMARY KEY AUTOINCREMENT主键recorduser_idINTEGER外键关联 user.idrecordsport_typeTEXT运动类型如跑步、骑行recorddurationINTEGER时长单位秒recordcaloriesREAL消耗卡路里recordrecord_dateTEXT日期格式 yyyy-MM-ddplanidINTEGER PRIMARY KEY AUTOINCREMENT主键planuser_idINTEGER外键planplan_nameTEXT计划名称plantarget_durationINTEGER目标时长秒planis_completedINTEGER0 未完成1 已完成建表 SQL 写在 SQLiteOpenHelper 的 onCreate 里// DBHelper.java public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME fitness.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 ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, password TEXT NOT NULL, height REAL, weight REAL)); // 运动记录表 db.execSQL(CREATE TABLE record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, sport_type TEXT, duration INTEGER, calories REAL, record_date TEXT)); // 训练计划表 db.execSQL(CREATE TABLE plan ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, plan_name TEXT, target_duration INTEGER, is_completed INTEGER DEFAULT 0)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 课程设计阶段直接删表重建生产环境不能这么干 db.execSQL(DROP TABLE IF EXISTS user); db.execSQL(DROP TABLE IF EXISTS record); db.execSQL(DROP TABLE IF EXISTS plan); onCreate(db); } }逻辑说明onCreate 只在数据库第一次创建时调用onUpgrade 在版本号增加时调用。参数 DB_VERSION 从 1 开始改表结构时加 1否则新表不会生效。注意 onUpgrade 里直接 DROP 会丢数据课程设计可以接受但你要知道这是偷懒做法。2.3 封装一个数据库操作类不要在每个 Activity 里直接写 SQL抽一个 DAO 类出来。以运动记录为例// RecordDao.java public class RecordDao { private DBHelper helper; public RecordDao(Context context) { helper new DBHelper(context); } // 插入一条记录 public long insert(int userId, String sportType, int duration, double calories, String date) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(user_id, userId); values.put(sport_type, sportType); values.put(duration, duration); values.put(calories, calories); values.put(record_date, date); long id db.insert(record, null, values); db.close(); return id; } // 查询某用户某天的总时长 public int getTotalDuration(int userId, String date) { SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT SUM(duration) FROM record WHERE user_id? AND record_date?, new String[]{String.valueOf(userId), date}); int total 0; if (cursor.moveToFirst()) { total cursor.getInt(0); } cursor.close(); db.close(); return total; } }参数说明db.insert返回新插入行的 id失败返回 -1。rawQuery的第二个参数是占位符的值数组顺序要和 SQL 里的?对应。注意 Cursor 用完必须 close否则反复查询会报 CursorWindowAllocationException。3. 界面与交互把功能串起来3.1 登录注册页怎么写才不崩登录页是第一个坑最多的地方。常见翻车点空输入没判断、数据库查询在主线程、密码框类型没设对。// LoginActivity.java 核心逻辑 public void onLoginClick(View view) { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(this, 用户名和密码不能为空, Toast.LENGTH_SHORT).show(); return; } UserDao userDao new UserDao(this); boolean success userDao.checkLogin(username, password); if (success) { // 保存当前登录用户 id后续页面要用 SharedPreferences sp getSharedPreferences(user, MODE_PRIVATE); sp.edit().putInt(user_id, userDao.getUserId(username)).apply(); startActivity(new Intent(this, MainActivity.class)); finish(); } else { Toast.makeText(this, 用户名或密码错误, Toast.LENGTH_SHORT).show(); } }逻辑说明先做非空校验再查数据库。SharedPreferences 用来存登录态key 是 user_id。注意apply()是异步写commit()是同步写登录场景用 apply 就行。布局文件里密码框要设android:inputTypetextPassword否则密码明文显示。这个细节很多新手会漏。3.2 运动记录列表用 RecyclerView 还是 ListView2024 年了用 RecyclerView。ListView 虽然简单但复用机制不如 RecyclerView 灵活而且面试问起来 RecyclerView 是必考。课程设计用 RecyclerView 还能多写点代码显得工作量足。// RecordAdapter.java public class RecordAdapter extends RecyclerView.AdapterRecordAdapter.ViewHolder { private ListRecord dataList; public RecordAdapter(ListRecord dataList) { this.dataList dataList; } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvType, tvDuration, tvCalories, tvDate; ViewHolder(View itemView) { super(itemView); tvType itemView.findViewById(R.id.tv_type); tvDuration itemView.findViewById(R.id.tv_duration); tvCalories itemView.findViewById(R.id.tv_calories); tvDate itemView.findViewById(R.id.tv_date); } } 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) { Record record dataList.get(position); holder.tvType.setText(record.getSportType()); holder.tvDuration.setText(record.getDuration() 秒); holder.tvCalories.setText(String.format(%.1f 千卡, record.getCalories())); holder.tvDate.setText(record.getRecordDate()); } Override public int getItemCount() { return dataList null ? 0 : dataList.size(); } }参数说明onCreateViewHolder负责创建 ViewHolderonBindViewHolder负责绑定数据。注意getItemCount要判空否则数据没加载完就 setAdapter 会崩。String.format(%.1f, ...)保留一位小数卡路里显示更自然。3.3 计时器功能Handler 还是 CountDownTimer运动计时用 CountDownTimer 最省事但如果你要做正计时比如跑步计时用 Handler 发消息更灵活。// 正计时实现 private Handler handler new Handler(Looper.getMainLooper()); private int seconds 0; private boolean isRunning false; private Runnable timerRunnable new Runnable() { Override public void run() { if (isRunning) { seconds; tvTimer.setText(formatTime(seconds)); handler.postDelayed(this, 1000); } } }; // 开始计时 public void startTimer() { if (!isRunning) { isRunning true; handler.post(timerRunnable); } } // 停止计时 public void stopTimer() { isRunning false; handler.removeCallbacks(timerRunnable); } // 格式化时间 private String formatTime(int totalSeconds) { int min totalSeconds / 60; int sec totalSeconds % 60; return String.format(%02d:%02d, min, sec); }逻辑说明handler.postDelayed每隔 1000 毫秒执行一次isRunning标志位控制是否继续。注意在 Activity 的onDestroy里调用handler.removeCallbacks否则页面销毁后计时器还在跑内存泄漏。提示CountDownTimer 的onTick回调间隔不精确做倒计时够用做精确计时不行。课程设计用 Handler 方案更稳。4. 避坑与排查课程设计里最容易翻车的 5 个地方4.1 数据库查询报 no such table现象App 一运行就崩Logcat 显示android.database.sqlite.SQLiteException: no such table: user。原因DBHelper 的 onCreate 没执行或者数据库版本号没改。常见情况是你改了建表 SQL但 DB_VERSION 还是 1系统认为数据库已经存在不会重新调用 onCreate。解决把 DB_VERSION 加 1或者在手机设置里清除应用数据。开发阶段我一般直接卸载重装比改版本号快。4.2 RecyclerView 数据更新后列表不刷新现象插入一条新记录后返回列表页列表还是旧的。原因Adapter 的 dataList 是同一个引用但你没有调用notifyDataSetChanged()。解决在 Activity 的onResume里重新查询数据库并更新 dataList然后调adapter.notifyDataSetChanged()。注意不要用notifyItemInserted除非你能确定插入位置。4.3 模拟器上时间不对导致日期查询为空现象查询当天记录返回空但数据库里明明有数据。原因模拟器时区或系统时间和宿主机不一致SimpleDateFormat格式化出来的日期和插入时不一致。解决统一用Calendar.getInstance()获取日期或者直接用 SQLite 的date(now)函数。我一般用new SimpleDateFormat(yyyy-MM-dd, Locale.getDefault())Locale 一定要指定否则在某些系统上会出问题。4.4 打包 release 版安装后闪退现象debug 版正常release 版一打开就闪退。原因release 版默认开启混淆SQLiteOpenHelper 或实体类被混淆后反射失败。解决在 proguard-rules.pro 里加保留规则-keep class com.example.fitness.model.** { *; } -keep class com.example.fitness.db.** { *; }或者课程设计阶段直接在 build.gradle 里把 minifyEnabled 设为 false。4.5 Gradle 同步报资源重复错误现象Duplicate resources或Resource entry already exists。原因图片资源重名或者 values 目录下 strings.xml 里定义了重复的 key。解决在 Android Studio 里按 CtrlShiftF 全局搜索报错的资源名找到重复定义的地方删掉。图片资源建议加前缀比如ic_sport_run.png、ic_sport_cycle.png。5. 进阶技巧让课程设计多拿 10 分的三个细节5.1 用 MPAndroidChart 画一周运动趋势课程设计如果只做增删改查分数不会太高。加一个柱状图展示最近七天的运动时长工作量上去了答辩也好讲。在 build.gradle 的 dependencies 里加implementation com.github.PhilJay:MPAndroidChart:v3.1.0然后在 settings.gradle 里加 jitpack 仓库maven { url https://jitpack.io }画图代码BarChart barChart findViewById(R.id.bar_chart); ListBarEntry entries new ArrayList(); // 假设 queryLast7Days 返回长度为 7 的数组单位分钟 int[] minutes recordDao.getLast7DaysMinutes(userId); for (int i 0; i 7; i) { entries.add(new BarEntry(i, minutes[i])); } BarDataSet dataSet new BarDataSet(entries, 每日运动时长分钟); BarData barData new BarData(dataSet); barChart.setData(barData); barChart.invalidate(); // 刷新参数说明BarEntry的第一个参数是 X 轴索引第二个是 Y 值。invalidate()触发重绘。X 轴的日期标签可以用XAxis的setValueFormatter自定义。5.2 用 SharedPreferences 记住登录状态每次打开 App 都要重新登录体验很差。在启动页判断 SharedPreferences 里有没有 user_id有就直接跳主页面。// SplashActivity.java SharedPreferences sp getSharedPreferences(user, MODE_PRIVATE); int userId sp.getInt(user_id, -1); if (userId ! -1) { startActivity(new Intent(this, MainActivity.class)); } else { startActivity(new Intent(this, LoginActivity.class)); } finish();注意退出登录时要sp.edit().remove(user_id).apply()否则下次启动还是自动登录。5.3 导出数据到 CSV 作为加分项答辩时老师问“数据能不能导出”你说能当场演示效果很好。实现思路查询所有记录拼成 CSV 字符串写到getExternalFilesDir目录。public void exportCsv(int userId) { ListRecord list recordDao.getAllByUser(userId); StringBuilder sb new StringBuilder(类型,时长(秒),卡路里,日期\n); for (Record r : list) { sb.append(r.getSportType()).append(,) .append(r.getDuration()).append(,) .append(r.getCalories()).append(,) .append(r.getRecordDate()).append(\n); } File file new File(getExternalFilesDir(null), fitness_export.csv); try (FileWriter writer new FileWriter(file)) { writer.write(sb.toString()); Toast.makeText(this, 已导出到 file.getPath(), Toast.LENGTH_LONG).show(); } catch (IOException e) { e.printStackTrace(); } }这个功能代码量不大但答辩时能体现你考虑了数据可移植性。注意 Android 10 以上要动态申请存储权限或者直接用getExternalFilesDir免权限。我做了三个学期的课程设计指导发现最后拿高分的不是功能最多的而是每个功能都能讲清楚为什么这么做的。数据库为什么用 SQLite 不用 Room因为课程设计环境不固定SQLite 零依赖列表为什么用 RecyclerView因为要练 ViewHolder 复用。这些理由在答辩时说出来比多写两个页面管用。希望帮到你。本文还有配套的精品资源点击获取