简介:一份面向Android开发学习与毕业设计场景的健身社交APP完整项目,涵盖登录注册、健身锻炼、朋友圈动态、每日打卡与个人中心等核心模块,技术栈采用Android客户端加服务器后端,适合计算机相关专业学生用于毕设、课设或项目起步,也适合有Java基础的学习者进阶参考。压缩包共251个文件,大小约96.59MB,以Java源码、XML布局与配置、PNG界面资源为主,同时包含后端Servlet类、JAR依赖、SQL脚本、Gradle构建文件以及演示视频等,结构清晰,便于运行调试和二次开发。项目内附设计文档与运行说明,代码经测试可稳定运行,已有79人学习下载。对需要快速搭建健身社交类应用、理解客户端与服务端交互的读者而言,这套资源提供了从界面到数据库的完整实现,可在此基础上扩展功能,或直接用于答辩展示与课程作业提交。
1. 健身社交 APP 毕设选题:先想清楚差异化,再动手写代码
健身社交 APP 是 Android 方向毕设里选得最多的题目之一,也是答辩翻车重灾区。很多同学照着 Keep 把界面抄完,演示时却只能翻页面:运动记录是写死的假数据,社交模块只有列表没有交互,地图轨迹是手工画的直线。能站住脚的版本,至少要跑通三件事:硬件计步传感器的真实读数、地图上的运动轨迹绘制、发布-点赞-评论的信息流闭环。
下面按这三条主线展开,先定架构和数据流向,再给关键代码和参数说明,最后是一份从 Android Studio 导入到模拟器运行可以直接照做的教程。适合两到三个月工期完成毕设的同学,也适合想看一个完整 Android 工程如何组织的初级开发者。标题里提到“结合 KEEP 等应用创新实践”,本质上就是把运动数据和社交内容联动起来,这也是答辩时最能讲出深度的部分。
2. 架构选型与数据流转:让健身记录和社交信息流各归其位
动手写第一个 Activity 之前,先定架构和依赖。对于五到十个页面的毕设工程,组件化和多模块的成本远大于收益,Gradle 同步、依赖传递、资源命名每一处都是新坑;MVVM 单模块是性价比最高的方案,也最容易在十分钟内讲清楚。另一个关键点是数据来源不同:运动模块的数据来自本地传感器加 Room 数据库,社交模块的数据来自远端接口,两边的节奏完全不一样,必须靠 Repository 层统一汇合。
2.1 为什么选 MVVM 单模块,而不是网上常见的 MVC 写法
网上流传的安卓毕设源码大多还是 MVC 老写法:Activity 里直接 new Retrofit、回调里更新 TextView。这种写法在屏幕旋转后会立刻暴露问题——Activity 重建,请求结果回到已销毁的页面,轻则丢数据,重则空指针闪退。MVVM 的核心是 ViewModel 的生命周期独立于 Activity,旋转屏幕后数据还在;UI 层只观察 LiveData 或 StateFlow,不直接发请求。答辩时能把“数据驱动 UI”这一句讲透,架构分就拿到了。
不分模块的原因更实际:毕业设计要在答辩教室的电脑上打开,多模块工程只要某一层依赖没对齐,Gradle 同步就够折腾一晚上。单模块配合包名分层,结构足够清晰,也方便导师检查代码。
2.2 SDK 版本与依赖清单:Room、Retrofit、高德地图的配置
打开 app/build.gradle,下面的配置可以直接抄。compileSdk 与 targetSdk 对齐到 34,对应 Android 14;minSdk 设 24,覆盖 Android 7.0 及以上,这个版本以下基本没有计步硬件,不需要为兼容浪费精力。
android { namespace 'com.example.fitsocial' compileSdk 34 defaultConfig { applicationId "com.example.fitsocial" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0.0-demo" } buildFeatures { viewBinding true } }versionName 带 demo 后缀,是为了答辩现场一眼确认装的是最新包。依赖部分需要注意:Room 2.6 及以上推荐用 KSP 而不是 kapt,编译速度快一倍以上;高德地图 SDK 需要额外配置它的 Maven 仓库地址。
dependencies { implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") // 本地存储 implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") ksp("androidx.room:room-compiler:2.6.1") // 网络与 JSON implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") // 高德地图 implementation("com.amap.api:3dmap:9.2.0") implementation("com.amap.api:location:6.4.0") // 图片加载 implementation("com.github.bumptech.glide:glide:4.16.0") }这里的版本号是一组能稳定联动的组合,实际创建工程时以仓库里的最新稳定版为准。高德两个 SDK 如果从 Maven 拉不下来,也可以去官网下载 aar 放进 app/libs,再写implementation(files("libs/amap.jar")),效果一样。
工程的包结构建议按下表分层,原则是 UI 层不直接碰数据库和网络。被问到“数据从哪来”,你只需要指向 Repository,不用在 Activity 里到处找逻辑。
| 包名 | 职责 | 典型类 |
|---|---|---|
| ui.main / ui.sport / ui.social | 页面与状态渲染 | MainActivity、SportFragment、FeedFragment |
| data.local | Room 数据库与 DataStore | ExerciseRecordDao、AppDatabase |
| data.remote | Retrofit 接口与拦截器 | SocialApi、ApiClient |
| data.repository | 数据汇合与缓存 | HomeRepository、SocialRepository |
| service | 计步前台服务 | StepCounterService |
| common | 工具与自定义控件 | TimeUtils、ImageCompressor |
2.3 Repository 层:把本地数据库和远端排行榜接进同一个 UI 状态
运动记录是最典型的本地数据。Room 实体和 DAO 可以这样定义:
// ExerciseRecord.kt - 运动记录表 @Entity(tableName = "exercise_record") data class ExerciseRecord( @PrimaryKey(autoGenerate = true) val id: Long = 0, val sportType: String, // 跑步 / 骑行 / 自由训练 val distanceMeters: Int, // 距离,单位米 val durationSeconds: Long, // 时长,单位秒 val calories: Float, // 消耗,单位千卡 val routeJson: String, // 轨迹点列表的 JSON 字符串 val startTime: Long // 开始时间戳,毫秒 ) @Dao interface ExerciseRecordDao { @Query("SELECT * FROM exercise_record ORDER BY startTime DESC LIMIT 20") fun observeRecentRecords(): Flow<List<ExerciseRecord>> }routeJson 存的是轨迹点的 JSON 数组字符串。对毕设来说,不拆轨迹表是合理选择:列表页只需要按运动类型和时间展示,只有进入详情页才解析 JSON 回放路线。如果创新点要做到“轨迹回放逐点动画”,才需要拆成 point 表。
首页通常同时需要“最近运动记录”和“好友排行榜”两个数据源,接口的 Flow 和 Room 的 Flow 通过 combine 合流:
// HomeRepository.kt - 双数据源汇合成一个 UI 状态 class HomeRepository( private val recordDao: ExerciseRecordDao, private val socialApi: SocialApi ) { fun loadHomeState(): Flow<HomeUiState> { val localRecords = recordDao.observeRecentRecords() val remoteRank = flow { emit(socialApi.getWeeklyRank().data) }.catch { emit(emptyList()) // 断网时给空榜,不能让页面崩溃 } return combine(localRecords, remoteRank) { records, rank -> HomeUiState(records = records, rankList = rank) } } }combine 的含义是两个 Flow 任意一个发新数据,组合结果都会重新发射。断网时排行榜被 catch 兜底成空列表,运动记录仍然来自本地库,首页就不会白屏。这就是“本地数据负责保底、远端数据负责增量”的常见做法,也是后面做演示预案的基础。
提示:ViewModel 里不要直接持有 Activity 或 Fragment 的引用,依赖注入用手写工厂类即可,答辩时被问到生命周期问题能接住。
3. 核心功能实现:计步传感器、运动轨迹、社交信息流的关键代码
这一章是项目的体力活。三个模块分别对应 Keep 类应用的三条主线:运动数据从哪来、轨迹怎么画、社交内容怎么流转。每条线都有容易踩的坑,代码不复杂,参数和生命周期才是关键。
3.1 计步传感器:TYPE_STEP_COUNTER 的基线计算与前台服务保活
Android 从 API 19 开始提供两种步数传感器,选型直接影响准确率和耗电。
| 传感器类型 | 回调方式 | 含义 | 坑 |
|---|---|---|---|
| TYPE_STEP_COUNTER | 持续上报累计值 | 开机以来总步数 | 重启清零,必须自己保存基线 |
| TYPE_STEP_DETECTOR | 每步一次事件 | 单步触发 | 高频回调耗电,且易漏步 |
计步应该用前台服务实现,否则 App 退到后台一段时间后传感器会被系统回收。Android 10 及以上,后台访问传感器需要把服务声明为 foregroundServiceType,并在通知栏常驻一条通知。
// StepCounterService.kt - 计步前台服务 class StepCounterService : Service(), SensorEventListener { private lateinit var sensorManager: SensorManager private var baseStep = -1L override fun onCreate() { super.onCreate() sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager // 优先用 TYPE_STEP_COUNTER;低端机拿不到再退回报步数 val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) ?: sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR) if (sensor == null) { stopSelf() return } sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL) } override fun onSensorChanged(event: SensorEvent) { val total = event.values[0].toLong() if (baseStep == -1L) baseStep = total // 第一次回调记基线 val todayStep = total - baseStep StepDataStore.update(this, todayStep) // 写入 DataStore / Room } }TYPE_STEP_COUNTER 上报的是开机以来的累计值,第一次回调时记录 baseStep,之后每步取差值。这里有一个细节:手机重启后累计值归零,如果本地保存的是旧开机周期的基线,差值会变成负数或巨大值,所以收到开机广播 BOOT_COMPLETED 时要重置基线。SENSOR_DELAY_NORMAL 对计步足够,不要用 SENSOR_DELAY_FASTEST,只会徒增耗电。
3.2 运动轨迹:高德地图定位参数与 polyline 的降频绘制
轨迹功能是答辩演示最容易出彩的部分,也是最容易出问题的地方。定位初始化时,LocationClientOption 的参数直接影响轨迹质量:
// SportActivity.kt - 高德地图定位与轨迹绘制 private fun initAMapLocation() { val option = LocationClientOption() option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy option.interval = 3000 // 3 秒定位一次 option.setNeedAddress(false) // 不解析街道地址,省电省流量 option.setLocationCacheEnable(false) // 关缓存,防止轨迹拉直线 locationClient = LocationClient(this) locationClient.setLocationOption(option) locationClient.registerLocationListener(this) } override fun onLocationChanged(location: AMapLocation) { if (location.errorCode != 0) return val point = LatLng(location.latitude, location.longitude) points.add(point) // 每攒 30 个点重绘一次,避免高频回调反复刷新地图 if (points.size - lastDrawIndex >= 30) { drawPolyline(points) lastDrawIndex = points.size } } private fun drawPolyline(path: List<LatLng>) { if (path.size < 2) return val options = PolylineOptions() .addAll(path) .width(18f) .color(Color.parseColor("#FF5B4B")) .geodesic(false) aMap.addPolyline(options) }interval 设 3000 毫秒是省电和平滑度的折中;跑步可以切到 2000,骑行可以放宽到 5000。setLocationCacheEnable(false) 是最容易被忽略的一行,开着缓存时两个定位点之间可能出现一条穿过楼房的直线。30 点重绘阈值让地图操作保持流畅,结束后把 points 序列化成 JSON 存进 Room 的 routeJson 字段,历史记录页就能回放。
定位需要运行时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 两个权限,targetSdk 34 下还要处理“仅在使用期间允许”的分区权限写法。另一个容易忽略的点是离线包:答辩教室经常没网,高德地图在没有网络时瓦片全是灰格子,提前在应用内下载演示城市的离线地图包可以避免这个尴尬,注意离线包不随 APK 自动分发,需要单独下载或者手动导入。
3.3 社交信息流:RecyclerView 差分更新与运动打卡发布
社交模块借鉴 Keep 的“动态”设计,常见创新点是让一次运动记录自动生成一条动态,用户补一段文案后发布。这样信息流里的每条内容都绑定真实运动数据,答辩时能讲出“内容和数据联动”,而不是普通的论坛帖子。网络接口可以这样定义:
// SocialApi.kt - 社交模块接口 interface SocialApi { // 分页拉信息流,page 从 1 开始 @GET("api/feed") suspend fun getFeed( @Query("page") page: Int, @Query("pageSize") size: Int = 10 ): BaseResponse<List<Post>> // 点赞 / 取消点赞 @POST("api/post/{id}/like") suspend fun toggleLike(@Path("id") id: Long): BaseResponse<LikeState> }pageSize 设 10 是为了首次加载快;上拉加载下一页时 page 加 1 重新请求。点赞的接口设计成“切换”语义,比分开的 like 和 unlike 两个接口更适合动态运营。列表部分用 RecyclerView 加 DiffUtil 做增量更新:
// FeedAdapter.kt - 差分更新 class PostDiff : DiffUtil.ItemCallback<Post>() { override fun areItemsTheSame(old: Post, new: Post) = old.id == new.id override fun areContentsTheSame(old: Post, new: Post) = old == new }点完赞后调用 adapter.submitList(newList),DiffUtil 只重绘变化的那一行,整个列表不会闪烁或跳位。这些都是信息流流畅度的关键。发布动态时图片要先压缩再走 Multipart 上传:
// ImageCompressor.kt - 压缩到宽度 1080 再上传 fun compressForUpload(uri: Uri): File? { val input = contentResolver.openInputStream(uri) ?: return null val bitmap = BitmapFactory.decodeStream(input) val scale = 1080f / bitmap.width val scaled = Bitmap.createScaledBitmap( bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true ) val out = File(cacheDir, "upload_${System.currentTimeMillis()}.jpg") out.outputStream().use { scaled.compress(Bitmap.CompressFormat.JPEG, 85, it) } return out }宽度压到 1080、质量 85,一张 3MB 的原图通常能压到 200KB 以内,上传速度快一个量级。这条链路完整跑通后,“运动记录生成动态、配图上传、列表刷新”的闭环就成立了。
注意:高德地图的 key 绑定包名和签名 SHA1,debug 包和 release 包是两套签名,需要在控制台分别配置,否则 release 包地图黑屏。
4. 运行教程:Android Studio 导入、Gradle 同步与模拟器部署
拿到一个 zip 工程后,最常见的失误是直接解压打开就点运行,然后在报错里耗掉半天。正确的顺序是:先确认环境版本与工程要求匹配,再让 Gradle 同步通过,最后选运行载体。这一步一步来,绝大多数问题都能在十分钟内定位。
4.1 环境准备:Android Studio 安装、SDK 与 JDK 版本匹配
先从官网下载 Android Studio 稳定版,不要用预览版做毕业设计。首次启动会自动下载 Android SDK;如果列表一直转圈加载不出来,重启 Android Studio 再试,或者确认当前网络能正常访问 Google 的 SDK 下载服务。需要安装的组件在 Settings → Appearance & Behavior → System Settings → Android SDK 里能看到:
| 组件 | 版本 | 用途 |
|---|---|---|
| Android SDK Platform 34 | API 34 | 编译和运行 targetSdk 34 的工程 |
| Android SDK Build-Tools | 34.x | 编译工具链,AGP 会自动选择 |
| Platform Tools | 最新 | 提供 adb 命令 |
| AVD 系统镜像 | 按需 | 创建模拟器用,后面会细说 |
SDK 组件无法勾选的场景,常见原因是 SDK 列表加载不完整,处理办法是先点右上角 Reset 重新拉取;如果还不行,手动下载对应的 platform zip 解压到 SDK 的 platforms 目录下,重启 Android Studio 即可识别。
JDK 不需要单独折腾。Android Studio 自带 JetBrains Runtime,在 File → Project Structure → SDK Location 里确认 Gradle JDK 选的是内置版本。想用中文界面的,在 Settings → Plugins 里搜索 Chinese Language Pack 安装后重启,这一步对英文不熟练的同学能省不少时间。最后确认项目根目录有 local.properties 文件,里面写了 sdk.dir 指向本机 SDK 路径,没有就手动新建一行:
sdk.dir=C\:\\Users\\你的用户名\\AppData\\Local\\Android\\Sdk4.2 Gradle 同步阶段的高频报错与处理
双击打开工程后,Gradle 同步是第一道关卡。下面是这个类型的项目最常遇到的四类报错和处理方式:
| 报错信息 | 原因 | 处理 |
|---|---|---|
| Could not find com.android.tools.build:gradle:8.x | 仓库源缺 google() 或网络下载失败 | settings.gradle 的 pluginManagement 里确认 google() 和 mavenCentral() 存在,再点 Sync Now |
| build 出现 tag number over 30 is not supported | buildToolsVersion 手动写低了,和 AGP 版本不匹配 | 把 app/build.gradle 里 buildToolsVersion 那行删掉,让 AGP 用默认版本 |
| SDK location not found | local.properties 缺失或路径不对 | 检查 sdk.dir 路径和反斜杠转义 |
| java.lang.OutOfMemoryError: Java heap space | Gradle JVM 堆内存太小 | gradle.properties 里把 org.gradle.jvmargs 调到 -Xmx2048m |
“tag number over 30 is not supported” 这个报错很典型,工程里如果写了 buildToolsVersion "30.0.3" 这类老版本,AGP 8.x 会直接拒绝。删除这一行让它跟随 AGP 默认值是标准做法,比手动填新版本号更稳。依赖下载慢的问题,可以在 settings.gradle 的 dependencyResolutionManagement 里把仓库源顺序调成国内 Maven 镜像优先,注意镜像仓库要保留原仓库的缓存路径。
4.3 模拟器与真机:传感器差异和 adb 部署命令
选择运行载体时,健身类应用和普通商城应用不一样,传感器差异是硬伤。下面这个对比决定了演示策略:
| 运行方式 | 优点 | 注意点 |
|---|---|---|
| Android Studio AVD 模拟器 | 环境官方、和 IDE 日志集成好 | 首次要下载系统镜像;没有真实计步传感器 |
| 第三方模拟器(MuMu、雷电等) | 启动快、支持多开 | 传感器模拟不完整,部分版本拿不到 TYPE_STEP_COUNTER |
| 真机 | 计步、GPS 全部真实 | 需要开启开发者选项和 USB 调试 |
AVD 创建时建议选 Pixel 5 设备定义加 API 33/34 的 x86_64 镜像,性能和兼容性都稳定。启动模拟器后,Extended Controls → Virtual sensors → Additional sensors 里可以手动给 TYPE_STEP_COUNTER 填一个值,模拟计步;GPS 坐标则在 Location 面板设置。但这对答辩演示来说太僵硬,所以强烈建议真机为主、模拟器为辅。
真机连接后,用命令行可以做完整的部署和验证:
# 查看设备是否被识别,状态应为 device adb devices # 覆盖安装 debug 包 adb install -r app/build/outputs/apk/debug/app-debug.apk # 强制启动主界面 adb shell am start -n com.example.fitsocial/.ui.MainActivity # 过滤计步服务的生命周期日志 adb logcat -s StepCounterServiceadb 全称 Android Debug Bridge,是 Android 调试的底层工具。第一行输出里如果设备状态是 unauthorized,到手机屏幕上点掉 USB 调试授权弹窗再重试。最后一行 logcat 用来确认计步前台服务有没有被系统回收,如果频繁看到 onDestroy 又 onCreate,说明服务保活不满足要求,要检查是否漏配了前台服务类型或通知渠道。
5. 答辩收尾技术:混淆规则、签名打包与现场演示预案
功能做完只是第一步,release 包能不能跑、答辩现场稳不稳定,是另外一回事。健身社交项目里 Release 构建最容易翻车的地方是混淆,其次是高德地图的 key 配置,最后是演示当天的数据状态。
5.1 混淆规则里的三类必留项
不写混淆规则,debug 包一切正常,release 包启动秒退,这是所有地图加网络项目都见过的场景。proguard-rules.pro 里至少要保留三类内容:
# 高德地图 SDK 有 native 方法和反射调用,官方固定写法 -keep class com.amap.api.** { *; } -keep class com.loc.** { *; } -dontwarn com.amap.api.** # Room 的数据库类在编译期生成实现,保留完整类名 -keep class * extends androidx.room.RoomDatabase # Gson 反序列化依赖字段名,实体类不能混淆 -keep class com.example.fitsocial.model.** { *; }地图 SDK 的规则直接复制即可,删掉任何一行都可能出现类找不到。Room 在较新版本里自带 keep 规则,但 TypeConverter 类不在此列,有自定义转换器时把 model 包整体保留是最省事的写法。第三段最容易忽略——本地调试时 JSON 字段就算被混淆也不一定立刻暴露,release 包上反序列化字段全变 null,只在 logcat 里留下几行难定位的 warning。
5.2 演示预案:双签名校验、三份备份与冷启动优化
答辩前一天的检查清单可以这样安排。先用命令行确认签名证书的 SHA1 和高德控制台里配置的一致:
# 查看签名文件的指纹信息 keytool -list -v -keystore ./release.jks -alias fit_social把输出里的 SHA1 复制到高德开发者控制台,和 release 包名重新核对一次。之后 clean 工程,完整打一个 debug 包和一个 release 包,两个 APK 都装到演示真机上跑一遍地图、计步、发动态三个核心流程;APK 文件同时放进 U 盘和网盘各一份,防止现场换电脑的情况。
演示当天,提前用下面两条命令做冷启动:
adb shell am force-stop com.example.fitsocial adb shell am start -n com.example.fitsocial/.ui.MainActivity不要带着一整天调试留下的后台状态上台。计步模块提前在校园里走几百步,让当日数据有个像样的基线;信息流如果依赖自建后端,提前启动服务并把接口返回值确认到 200。最后留意一个指标:冷启动到首页数据出现的时间。超过 3 秒,优先检查首屏是否在主线程做了 Room 查询或 JSON 解析,把这两件事移进 Repository 层的 Flow 或 IO 线程池,通常能压回两秒以内——这是健身社交这类“本地库加远端接口”双数据源架构最值得做的一次性能优化。
本文还有配套的精品资源,点击获取