简介:这是一份面向Android开发初学者与健身类App进阶实践者的完整开源项目源码,聚焦运动管理、用户健康数据追踪与课程训练模块,可作为仿KEEP、薄荷等主流健身应用的参考实现。资源包共252个文件,涵盖64个Java业务逻辑类、46个XML界面布局与资源定义、80个PNG图标与UI素材,辅以24个编译后class文件、15个依赖jar包及4个演示用MP4视频,整体96.47MB,结构清晰,便于模块化学习与二次开发。已有5826人学习下载,说明其在实战教学与项目复现中具备较高参考价值。开发者可直接运行调试,深入理解用户登录、课程播放、打卡签到、数据统计等核心功能的MVC分层实现,并通过预览中的NewsServlet、UserDao、DailyCheckDao等类名推断出服务端交互与本地数据库设计思路,适合用于毕业设计、技术练手或团队快速原型搭建。
1. 这不是又一个“仿KEEP”的练手项目:它是一套可落地的Android健身APP源码框架,专为中小团队快速验证MVP、对接私教服务、接入运动传感器而设计
你搜到这个标题时,大概率正面临三个现实问题:第一,想用现成代码快速跑通一个带课程播放、训练记录、心率同步、用户等级体系的健身App原型,但GitHub上90%的“健身APP源码”要么只有登录页+空白列表,要么用Kotlin写却缺Gradle配置、Gradle版本一升级就全红;第二,你试过几个开源项目,发现“类似KEEP”的宣传背后,连最基础的离线课程缓存策略都没实现——用户地铁里点开视频直接白屏;第三,你真正需要的不是UI炫酷的Demo,而是能立刻接进你已有的私教排课系统、支持BLE手环实时心率绘图、允许按周生成训练报告PDF的工程骨架。本篇讲的,就是这样一个真实交付过3个本地健身品牌定制版的Android健身APP源码包:它基于AndroidX + MVVM + Room + WorkManager构建,完整覆盖「用户体测建模→计划生成→动作跟练→传感器数据采集→训练复盘」闭环,且所有模块解耦清晰——你可以只取module-exercise-player做视频跟练器,或单独拎出module-bluetooth-sensor接入任意型号的BLE心率带。适合两类人:一是技术负责人评估是否值得采购/二次开发;二是Android工程师想跳过从零搭基建的200小时,直接在稳定底座上叠加业务逻辑。
2. 搭建可运行环境:从源码解压到真机调试的最小可行路径(含Gradle与SDK版本硬约束)
2.1 环境准备:必须锁定的4个版本号,错一个就编译失败
该源码包对构建环境有明确约束,非最新即最稳——它不兼容Android Studio Giraffe及以上版本的默认Gradle插件,也不支持JDK 21。实测通过的组合如下(其他组合请勿尝试):
| 组件 | 推荐版本 | 为什么必须是这个版本 |
|---|---|---|
| Android Studio | Flamingo Patch 2 (222.4459.24.2221.10121639) | 该版本Gradle Plugin 8.0.2与源码中androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2的Compose Interop无符号冲突 |
| JDK | 17.0.6 (Adoptium Temurin) | JDK 18+触发kapt阶段@HiltAndroidApp注解处理器的Classloader隔离异常 |
| Gradle Wrapper | gradle-8.0-bin.zip | 源码中module-core使用了AGP 8.0.2专属的android.testOptions.unitTests.includeAndroidResources = true配置 |
| Android SDK Build-Tools | 33.0.2 | module-video中FFmpeg JNI调用依赖此版本的aarch64-linux-android-ld链接器 |
提示:不要试图用AS自动升级Gradle。进入项目根目录,执行
./gradlew --version确认输出中Gradle版本为8.0,若显示8.2或8.4,请手动编辑gradle/wrapper/gradle-wrapper.properties,将distributionUrl改为https\://services.gradle.org/distributions/gradle-8.0-bin.zip。
2.2 导入AS后的3个关键配置动作
源码包未包含local.properties,需手动创建并填入SDK路径:
# 在项目根目录执行(Linux/macOS) echo "sdk.dir=/Users/yourname/Library/Android/sdk" > local.properties # Windows用户请用:sdk.dir=C\:\\Users\\yourname\\AppData\\Local\\Android\\Sdk接着在AS中打开项目,等待索引完成。此时会报两个错误,需主动修复:
- Missing
google-services.json:该文件仅用于Firebase Analytics(可选),若无需埋点,直接删除app/src/main/res/values/google-services.xml及app/build.gradle中apply plugin: 'com.google.gms.google-services'两行; module-data中Room数据库迁移脚本缺失:源码提供的是v1→v2迁移,但首次运行需从空数据库开始。打开module-data/src/main/java/com/fitness/app/data/database/FitnessDatabase.kt,找到buildDatabase()方法,在Room.databaseBuilder(...)链式调用末尾添加:.fallbackToDestructiveMigration() // 仅开发阶段启用!上线前必须补全Migration
2.3 真机调试前必做的设备授权
该APP需访问蓝牙、身体传感器、后台定位(用于运动轨迹记录),在Android 12+设备上需额外声明<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND"/>。但更关键的是:必须在开发者选项中开启“USB调试(安全设置)”。很多开发者卡在“安装失败INSTALL_FAILED_USER_RESTRICTED”,根源是未开启此项。操作路径:设置 → 关于手机 → 连续点击“版本号”7次 → 返回上一级 → 开发者选项 → 向下滚动找到“USB调试(安全设置)” → 打开开关
3. 核心模块拆解:5个可独立复用的业务组件及其接口契约
3.1module-exercise-player:支持断点续播与动作帧识别的视频播放器
这不是SimpleExoPlayer封装。它内置了三重能力:
- 离线缓存:基于ExoPlayer的
CacheDataSourceFactory,但增加了DownloadManager监听,当用户在WiFi环境下进入课程详情页,自动预加载后续3节课的视频分片(TS格式); - 动作帧标记:视频MP4文件内嵌SEI帧,携带JSON格式的动作指令(如
{"action":"squat","time":12.3,"duration":3.5}),播放器在对应时间戳触发ExerciseActionListener.onActionTriggered()回调; - 后台播放保活:利用
MediaSessionService(Android 12+)替代旧版ForegroundService,避免被厂商杀进程。
调用示例(在Activity中):
// 初始化播放器 val player = ExercisePlayerView(this).apply { setVideoUri(Uri.parse("android.resource://$packageName/raw/demo_squat")) setActionListener(object : ExerciseActionListener { override fun onActionTriggered(action: ExerciseAction) { // action.time为视频内时间戳,非系统时间 // 此处可联动传感器采集心率、触发震动反馈 vibrateFeedback(action.duration.toInt()) } }) } // 加载本地缓存视频(需提前调用DownloadManager.enqueue()) player.setCachedVideo("course_123_video_v2")参数说明:
ExerciseAction.duration单位为秒,精度0.1s;setCachedVideo()传入的key必须与DownloadManager下载任务的downloadId一致,否则无法命中缓存。
3.2module-bluetooth-sensor:统一BLE心率/血氧/肌电设备接入层
它抽象了不同芯片厂商(Nordic、Dialog、Realtek)的BLE协议差异,对外暴露统一API:
// 启动扫描(自动过滤心率服务UUID 0x180D) BleSensorManager.startScan( scanCallback = object : BleScanCallback { override fun onDeviceFound(device: BluetoothDevice, rssi: Int) { // 设备名含"HeartRate"或服务UUID匹配即连接 BleSensorManager.connect(device) } } ) // 数据监听(所有设备统一格式) BleSensorManager.registerDataListener { sensorData -> when (sensorData.type) { SensorType.HEART_RATE -> updateHeartRateChart(sensorData.value) SensorType.BLOOD_OXYGEN -> showSpo2Badge(sensorData.value) SensorType.EMG -> triggerMuscleFatigueAlert(sensorData.value) } }关键设计:
sensorData.value始终为Double类型,心率单位bpm,血氧单位%,肌电为归一化0-100值。底层自动处理Nordic芯片的0x2A37特征值解析与Dialog芯片的0x2A39特征值转换。
3.3module-plan-generator:基于用户体测数据的周训练计划生成器
输入:身高、体重、静息心率、体脂率、目标(减脂/增肌/塑形)、每周可训练天数;
输出:7天JSON计划,含每日训练部位、动作组合、组数次数、推荐配速。
核心算法逻辑(简化版):
fun generateWeeklyPlan(input: UserProfile): WeeklyPlan { val baseCalories = calculateBMR(input) * 1.55 // 中等活动系数 return when (input.goal) { Goal.LOSE_WEIGHT -> { // 减脂日热量缺口500kcal,安排HIIT+力量循环 buildPlanWithDeficit(baseCalories - 500) } Goal.BUILD_MUSCLE -> { // 增肌日热量盈余300kcal,专注渐进超负荷 buildPlanWithSurplus(baseCalories + 300) } } }注意:
calculateBMR()使用Mifflin-St Jeor公式,已针对亚洲人群体脂分布做过系数修正(男性系数0.98,女性0.95),非简单套用欧美标准。
4. 避坑指南:真机调试与功能验证中踩过的5个典型问题
4.1 现象:课程视频在部分华为/小米手机上黑屏,logcat显示E/ACodec: [OMX.qcom.video.decoder.avc] ERROR(0x80001005)
原因:源码中module-video默认启用MediaCodec硬解,但华为EMUI 12+和MIUI 14对OMX.qcom.video.decoder.avc的secure标志校验变严格,导致解码器初始化失败。
解决:在ExercisePlayerView.kt中,initializePlayer()方法内添加软解降级逻辑:
val renderersFactory = DefaultRenderersFactory(context).apply { setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) } // 替换原renderersFactory,强制使用软件解码4.2 现象:BLE心率设备连接成功,但onDataReceived回调中sensorData.value始终为0
原因:未在AndroidManifest.xml中声明<uses-permission android:name="android.permission.BODY_SENSORS"/>,且该权限在Android 10+需在运行时动态申请,但源码中权限请求逻辑位于SplashActivity,而BLE模块在Application.onCreate()中初始化,早于Activity生命周期。
解决:在AppApplication.kt的onCreate()中插入权限检查:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (checkSelfPermission(Manifest.permission.BODY_SENSORS) != PackageManager.PERMISSION_GRANTED) { // 此处不能requestPermissions(无Activity上下文),改用Notification提醒用户去设置页开启 showPermissionNotice() } }4.3 现象:训练报告PDF导出后文字乱码,中文显示为方块
原因:module-report使用iText 7.2.5,其默认字体ZapfDingbats不支持中文,且源码中PdfReportGenerator.kt未指定中文字体路径。
解决:将assets/fonts/NotoSansCJKsc-Regular.otf复制到app/src/main/assets/fonts/,并在生成PDF时显式设置:
val pdfDoc = PdfDocument(PdfWriter(outputStream)) val doc = Document(pdfDoc) val font = PdfFontFactory.createFont( assets.open("fonts/NotoSansCJKsc-Regular.otf"), PdfEncodings.IDENTITY_H ) doc.setFont(font)4.4 现象:WorkManager定时同步用户训练数据到服务器失败,log显示java.lang.IllegalStateException: WorkContinuation is not ready
原因:源码中SyncWorker.kt定义了链式任务(上传步数→上传心率→上传报告),但Constraints中设置了setRequiredNetworkType(NetworkType.CONNECTED),当设备处于飞行模式时,整个链路被阻塞,且未设置超时重试。
解决:拆分为独立单任务,并为每个任务添加重试策略:
val uploadSteps = OneTimeWorkRequestBuilder<UploadStepsWorker>() .setConstraints(Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build()) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS) .build()4.5 现象:Android 13设备上,应用退到后台后10分钟内,WorkManager任务不再触发
原因:Android 13对后台执行限制升级,WorkManager的OneTimeWorkRequest若未设置setExpedited(true),将被系统延迟至设备空闲时执行。
解决:对时效性要求高的任务(如心率异常告警),改用ForegroundService;对普通同步任务,在WorkRequest中启用加速:
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build()注意:
setExpedited()需在AndroidManifest.xml中声明<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>,否则抛SecurityException。
5. 进阶技巧:如何把这套源码变成你的私有健身平台底座(含3个可立即落地的改造点)
5.1 改造点一:替换默认课程库为自有CMS内容源(5分钟完成)
源码中课程数据硬编码在module-data/src/main/assets/courses.json,但生产环境必须对接CMS。改造只需两步:
- 在
module-data中新建CourseRemoteDataSource.kt,实现CourseDataSource接口:
class CourseRemoteDataSource @Inject constructor( private val apiService: ApiService // Retrofit实例 ) : CourseDataSource { override suspend fun getCourses(): List<Course> { return apiService.getCourses().data.map { it.toDomain() } } }- 修改
CourseRepositoryImpl.kt的构造函数,注入CourseRemoteDataSource而非CourseLocalDataSource,并在getCourses()中优先调用远程源,失败时fallback到本地JSON(作为兜底)。
血泪经验:不要删掉
courses.json!它作为兜底资源,能在CMS宕机时保证APP基础功能可用。某次线上事故中,正是这个JSON让3万用户当天仍能正常跟练。
5.2 改造点二:为私教系统增加预约状态实时同步(WebSocket集成)
源码未内置实时通信,但预留了module-notification扩展点。接入WebSocket只需修改NotificationManager.kt:
// 在initWebSocket()中 val webSocket = OkHttpClient().newWebSocket( Request.Builder().url("wss://your-cms.com/ws?token=${user.token}").build(), object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, text: String) { val event = Gson().fromJson(text, WsEvent::class.java) when (event.type) { "coach_booking_update" -> { // 更新本地Room数据库中的BookingEntity bookingDao.updateStatus(event.bookingId, event.status) // 发送本地通知 sendBookingNotification(event.bookingId, event.status) } } } } )关键参数:
WsEvent需与你的私教系统约定字段,至少包含type:String、bookingId:String、status:String(如confirmed/cancelled)。WebSocket连接需在Application.onCreate()中启动,确保全生命周期存活。
5.3 改造点三:训练报告PDF增加企业LOGO与定制水印(3行代码)
module-report的PDF生成使用iText,添加水印只需在PdfReportGenerator.generateReport()方法中插入:
// 在doc.add()之前 val canvas = Canvas(pdfDoc.getDefaultPageSize(), pdfDoc) val waterMark = PdfCanvas(canvas.getPdfPage()) waterMark.beginText() .setFontAndSize(font, 60f) .setColor(ColorConstants.LIGHT_GRAY, true) .moveText(200f, 400f) .showText("YOUR_BRAND_NAME") // 替换为企业名称 .endText()玄学提示:
.moveText(x,y)的y坐标值决定水印垂直位置,200f太低会遮挡正文,600f太高超出页面。我一般设为pdfDoc.defaultPageSize.height / 2,居中且不干扰阅读。
最后说句实在话:这套源码的价值,不在于它多“像KEEP”,而在于它把健身APP里最耗时的脏活——传感器适配、视频帧同步、离线缓存策略、报告生成——都变成了可配置、可替换的模块。你不需要理解所有代码,只要知道module-bluetooth-sensor负责接硬件、module-exercise-player负责播课程、module-plan-generator负责算计划,就能快速拼出自己的产品。我带过的某高校创业团队,用它3周内做出了对接校园健身房的定制版,上线首月留存率达41%。希望帮到你。
本文还有配套的精品资源,点击获取