☰
Android Studio健身App课程设计:从SQLite到RecyclerView完整实现
2026/10/26 16:59:04 网站建设 项目流程

简介:这是一套面向安卓初学者与课程设计需求者的完整健身系统源码,基于Android Studio开发,适合作为移动应用开发大作业或练手项目。项目覆盖登录注册、个人信息管理、运动项目列表与锻炼计时等模块,并融入MD5加密、Sqlite数据库、VideoView视频播放、OkHttp网络通信、RecycleView列表、ViewPager轮播图、Glide图片加载及Handler异步消息等十余个核心知识点,能帮助读者系统串联安卓开发技能。压缩包共626个文件,约85.28MB,包含java源码、xml布局、png与jpg图片资源、json数据、jar依赖及apk安装包等,结构完整可直接导入运行。目前已有1728人学习下载,适合需要完整赛题方案、排错思路与模块化代码参考的读者,便于快速理解项目架构并二次修改。

1. 从课程设计到能跑通的健身 App:Android Studio 里到底要写多少东西

每年期末,总有一批人打开 Android Studio 准备做健身系统课程设计,然后卡在第一个 Activity 上。这个标题看着像作业,实际拆开是一套完整的安卓开发最小闭环:多页面导航、SQLite 本地存储、列表适配器、计时器逻辑、图表展示。健身系统的好处是业务逻辑不复杂,但该有的模块一个不少,拿来练手刚好。适合两类人:一是第一次做安卓课程设计、不知道从哪下手的新手;二是做过记账本或计算器,想加个计时和数据统计功能的人。下面按我实际做过的路径,从建工程到打包,把每一步的参数和坑都写清楚。

2. 工程搭建与数据库设计:先把骨架立住

2.1 新建工程时选哪个模板

打开 Android Studio,选 Empty Views Activity,不要选 Compose 那个。课程设计场景下,XML 布局加 Java 或 Kotlin 的写法资料最多,出问题好搜。语言选 Java 还是 Kotlin 看课程要求,如果老师没指定,我一般用 Java,因为网上课程设计案例源码大部分是 Java 的,抄改方便。

最低 SDK 选 API 24(Android 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身高 cm
userweightREAL体重 kg
recordidINTEGER PRIMARY KEY AUTOINCREMENT主键
recorduser_idINTEGER外键关联 user.id
recordsport_typeTEXT运动类型,如跑步、骑行
recorddurationINTEGER时长,单位秒
recordcaloriesREAL消耗卡路里
recordrecord_dateTEXT日期,格式 yyyy-MM-dd
planidINTEGER 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:inputType="textPassword",否则密码明文显示。这个细节很多新手会漏。

3.2 运动记录列表用 RecyclerView 还是 ListView

2024 年了,用 RecyclerView。ListView 虽然简单,但复用机制不如 RecyclerView 灵活,而且面试问起来 RecyclerView 是必考。课程设计用 RecyclerView 还能多写点代码,显得工作量足。

// RecordAdapter.java public class RecordAdapter extends RecyclerView.Adapter<RecordAdapter.ViewHolder> { private List<Record> dataList; public RecordAdapter(List<Record> 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负责创建 ViewHolder,onBindViewHolder负责绑定数据。注意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 里按 Ctrl+Shift+F 全局搜索报错的资源名,找到重复定义的地方删掉。图片资源建议加前缀,比如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); List<BarEntry> 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) { List<Record> 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 复用。这些理由在答辩时说出来,比多写两个页面管用。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询