从决定开始做一个基于 Android 的运动健身 APP 到最终跑通测试机,整个周期大概花了三个月。这段时间我把健身类应用从业务到底层折腾了一遍,踩了不少坑,也积累了一些值得分享的经验。如果你正准备做类似的 APP,或者只是想把一个带数据库、图表、权限管理的 Android 项目完整落地,这篇内容应该能给你省下不少时间。
先说清楚,我做的这个 APP 不是那种接入穿戴设备的重型健身应用,而是一款以"训练记录 + 运动统计 + 目标管理"为核心的轻量级健身助手。目标用户是刚接触健身、想要记录每次训练数据并且看到自己进步曲线的人群。所以整个产品逻辑围绕三个词展开:记录、反馈、坚持。
1. 为什么做运动健身方向,以及第一版产品功能白皮书
1.1 健身场景的真实痛点与功能边界
在动手敲代码之前,我花了一周时间做需求盘整。健身类 APP 看起来赛道很挤,但大多数产品要么太专业,上来就是动作库、视频跟练、社区社交,对新手来说学习成本太高;要么就是个简单的计步器,连训练类型都分不清。普通健身者真正的痛点其实很朴素:我今天练了什么、练了多少组、重量有没有进步、这段时间体脂率或者体重走势怎么样。
所以我在第一版里做了一个非常克制的产品定义。没有社交、没有教练视频、没有在线课程,只做四个核心模块:训练记录、统计图表、目标设置、个人中心。训练记录支持按日期和运动类型分类,统计图表展示每周训练次数和体重变化趋势,目标设置让用户自定义阶段目标,个人中心管理基本资料和单位偏好。功能少了,开发路径就清晰,验收标准也容易定。
1.2 MVP 优先,先做四个主模块
第一版我给自己规定了两周内必须跑通完整闭环,也就是从用户打开 APP 到完成一次训练记录并且能在统计页看到数据变化。基于这个闭环,我的页面结构非常固定:首页展示今日训练入口和最近记录,训练页选择运动类型并填写数据,统计页用图表展示趋势,我的页面承载目标与设置。
这个 MVP 策略在后续开发中帮了大忙。因为模块边界清晰,我可以在每个页面里独立做测试,而不会出现"改一个功能带崩半个 APP"的情况。很多初学者上来就想做十几个功能模块,最后往往卡在互相纠缠的逻辑里。健身类 APP 的核心竞争力永远是数据闭环,先把一行行训练数据沉淀下来,再去谈运营和体验。
1.3 锁定目标 Android 版本与设备策略
项目启动前我查了当前 Android 版本分布,发现 Android 8.0 以上占据绝大多数设备,而 Android 12、13 的份额正在快速上升。所以我的 minSdkVersion 定在 26(Android 8.0),targetSdkVersion 定在 34。这样既能覆盖主流设备,又能使用较新的 API 特性。
这里有一个非常关键的取舍:不要为了兼容老设备而把 targetSdkVersion 压得过低。Google 应用商店对 targetSdkVersion 有逐年提高的要求,而且新版本 Android 的运行时权限、前台服务限制、后台电量策略都在收紧,如果你一开始就把 target 定得很低,后期适配成本会非常高。
2. 技术选型复盘:Kotlin、MVVM 与 Jetpack 组件如何组合
2.1 用 Kotlin 而不是 Java 的真实理由
我见过太多人纠结"用 Java 还是 Kotlin",其实真正常用的语法量并不大。我选 Kotlin 的理由非常实际:协程帮我解决了大部分异步数据加载问题,空安全机制让数据库字段映射到 UI 时少了一堆空指针判断,代码量整体比 Java 少了差不多三分之一。
比如从数据库加载训练记录列表,在 Java 里需要写 AsyncTask 或者 Handler,而 Kotlin 配合协程只需要一个 suspend 函数加一行 viewModelScope.launch。后续在统计页做图表数据聚合时,Kotlin 的集合操作符 map、filter、groupBy 也让代码清晰很多。
2.2 为什么坚持用 MVVM 架构
健身 APP 的第一个版本我用过传统 MVC,页面写在 Activity 里,数据加载也写在 Activity 里,结果一旦数据刷新逻辑增多,Activity 动不动就五六百行。后来我重构成了 MVVM:Activity/Fragment 只负责 UI 交互,ViewModel 持有数据状态,Repository 层统一管理本地数据库和网络请求。
MVVM 还有一个容易被忽视的好处:旋转屏幕不会丢数据。配置变更时 Activity 会重建,但 ViewModel 会保留下来,加载到一半的数据不用重新请求。对健身记录这种"填写到一半接了个电话"的场景来说非常友好。
2.3 依赖清单与版本选择建议
我的 build.gradle 核心依赖大致如下:
- Jetpack 全家桶:lifecycle-viewmodel-ktx、lifecycle-livedata-ktx、room-ktx、navigation-compose 或 fragment-ktx
- 图表库:我用的是 MPAndroidChart,用它绘制折线图和柱状图,社区成熟,坑比较可控
- 数据库:Room 2.6 系列,配合 Kotlin 协程
- 网络层:第一版其实用不到网络,所以我没有引入 Retrofit,避免不必要的复杂度
这里想提醒一句:图表库的教学价值很高,但不要迷信最新版本。有些新版本对 minSdk 的要求提高了,或者 API 改了名字,反而会让老项目适配起来很痛苦。我最后锁定的 MPAndroidChart 3.1.0 版本,虽然几年没更新,但稳定性和教程数量都是最好的。
3. 数据库设计:带着业务逻辑去建表
3.1 核心表结构拆解
健身 APP 的数据模型不复杂,但如果一开始设计得糙,后面统计功能会非常难受。我建了三张主表:用户表、训练记录表、目标表。
用户表保存基础信息,比如身高、体重、出生年份、性别,这些数据用于计算 BMI 和基础代谢率。训练记录表是整个 APP 的核心,字段包括记录 ID、运动类型、训练日期、训练时长、组数、次数、重量、消耗卡路里、备注。目标表则存用户的阶段目标和开始结束日期。
我贴一下训练记录表的简化建表语句:
CREATE TABLE workout_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, sport_type TEXT NOT NULL, record_date TEXT NOT NULL, duration_minute INTEGER NOT NULL, sets_count INTEGER DEFAULT 0, reps_count INTEGER DEFAULT 0, weight_kg REAL DEFAULT 0, calories_burned REAL DEFAULT 0, note TEXT DEFAULT '' );3.2 用 Room 管理 CRUD 与异步查询
我用 Room 而不是原生 SQLite,核心原因有三个:Room 在编译期做 SQL 语法检查,写错了直接编译报错;Room 可以自动生成从数据库表到 Kotlin 数据类的映射代码;Room 与 LiveData/Flow 配合,数据一变化 UI 自动刷新。
比如查询最近 30 天的训练记录,我只需要写这样的 DAO 方法:
@Query("SELECT * FROM workout_record WHERE record_date >= :startDate ORDER BY record_date DESC") fun loadRecordsSince(startDate: String): Flow<List<WorkoutRecord>>在 ViewModel 里拿到这个 Flow 之后,UI 层用 LiveData 收集变化,数据库里新增一条记录,统计图表立刻就能更新。这个机制对健身 APP 来说特别舒服,因为用户记录的频率不高,但每一条都希望马上看到反馈。
3.3 数据迁移与版本升级
Room 的数据库版本管理也值得一说。我第一版把数据库版本定为 1,后面如果要加字段,比如给训练记录增加"感知疲劳度"列,就必须写 Migration。不写 Migration 而直接用 fallbackToDestructiveMigration,会在升级时把用户表数据全部清空,这是健身记录类 APP 绝对不能接受的。
所以项目一开始就要养成习惯:每个版本改动都写 Migration 类,哪怕只是加一个默认值字段。数据是用户坚持健身的动力,丢数据就是丢信任。
4. 核心功能模块的实现细节
4.1 训练记录模块:从手动输入到计时模式
训练记录我做了两种模式。第一种是手动录入,用户选运动类型、填时长和组数,适合力量训练这种需要慢慢填的场景。第二种是计时模式,点击开始后页面显示持续的计时器,结束后自动生成记录,适合跑步、跳绳这类有时间连续性的运动。
计时模式里给新手一个建议:不要直接在 Activity 里用 Handler 每秒刷新 TextView,这样电量消耗很大,频繁旋转屏幕还会导致计时错乱。更稳妥的做法是用系统时钟或者 Monotonic 时钟计算开始和结束之间的差值,再用 LiveData 定期更新显示。比如启动时记录 startTimeMillis,然后每秒从 ViewModel 里读 elapsed,UI 只负责渲染。
4.2 统计模块:卡路里计算与可视化图表
卡路里计算是健身 APP 绕不开的东西。为了不过度复杂化,我参考了常见的 MET 代谢当量表,用公式"卡路里 = 体重(kg) × MET × 时长(小时)"估算。不同运动类型的 MET 值差别很大,例如跑步 8km/h 大约 8.3,骑自行车约 6.8,力量训练大约 5.0。
这个计算精度谈不上精确,但对个人用户来说足以反映趋势。我在统计页里用了 MPAndroidChart 的折线图展示每天消耗卡路里,柱状图展示每周训练频率,饼图展示不同运动类型占比。三种图的组合能让用户很直观地看到"我其实没有自己想象中练得那么勤"。
4.3 目标提醒模块:通知与前台服务
用户设置目标后,APP 需要在每天固定时间提醒用户完成训练。Android 8.0 以后发通知必须使用 NotificationChannel,否则通知不显示。Android 13 新增了通知运行时权限 POST_NOTIFICATIONS,需要主动申请才能弹通知。
如果项目需要后台持续记录运动状态,比如实时计步或正在进行的训练计时,只靠普通 Service 很容易被系统回收。这时候要使用前台服务,配合通知栏常驻一条"正在训练中"的通知,告诉系统和应用商店这个服务在用户可见地工作。不过要遵守系统对前台服务类型的限制,如果不是运动健康背景,就要把前台服务类型声明清楚,避免审核出问题。
5. UI/UX 设计:健身 APP 的界面不止是好看
5.1 底部导航与页面骨架
健身类 APP 的导航结构一般稳定在三到四个主标签页。我采用的是底部导航栏加四个 Fragment 的结构,也就是前面说的首页、训练、统计、我的。底部导航的好处是用户永远知道当前在哪,且切换成本极低。
具体实现上我用了 BottomNavigationView 配合 Navigation Component,切换时用 show/hide 代替反复 replace。这样做的意义在于保留页面状态,用户在统计页滚动到一半时切出去再切回来,不会跳到顶部。这个体验细节很多 APP 都不注意,但对健身数据这种长列表页面来说非常重要。
5.2 协调布局与动态进度
首页我刻意用了 CoordinatorLayout 配合 AppBarLayout,让顶部留出区域展示今日训练进度环。进度环没有用自定义 View,而是直接用的 MaterialProgressBar 样式调整 + 圆角背景,同时用动画让进度从 0 跑到目标值。这样一个视觉动效能够让用户马上感知"今天还差多少训练量"。
这里有个实操心得:动态 progress 动画不要让 View 自己开线程循环,而是用 ValueAnimator 为主,监听动画值去设置进度。ValueAnimator 会自动处理 UI 刷新频率,代码也简洁,不会因为 Fragment 销毁导致内存泄漏。
5.3 深色模式与图标适配
健身 APP 在夜间的使用率其实非常高,很多人都是下班以后去健身房,所以深色模式不是可选功能,而是刚需。我直接基于 Material3 的 ColorScheme 做动态取色,在 values-night 目录下提供一套深色主题色,文本和背景的对比度专门做了对比测试。
另外图标适配也值得花时间。Android 自带的自适应图标机制允许同一套图标的各个层被裁切处理,我准备了前景层和背景层资源,并确保前景层在安全区域内没有关键视觉元素。动态图标主题在 Android 13 以后可以直接支持,用户换主题壁纸时 APP 图标也会跟着变色,这虽然是小加分项,但能在细节上拉高完成度的整体感觉。
6. Android 平台特有的磨合:权限、电量与后台
6.1 一步步处理运行时权限
运动健身类 APP 最常涉及的是这两个权限:通知权限和可能的传感器使用权限。如果只是手动记录训练数据,权限敏感面比较小,这其实是件好事。
我处理权限的顺序是先把需要在 AndroidManifest 中声明的权限列清楚,再在真正需要时调用请求。不要进入 APP 就弹一堆权限申请,那样用户会很反感。比如通知权限,我等到用户第一次打开"设置目标"页时,才统一申请,并配上一句说明"开启通知后,每天会按时提醒你完成训练计划"。
6.2 后台保活与前台服务
很多 Android 新手有个误解,觉得应用退到后台就一定要"保活",否则就是不合格。实际上全面屏时代系统限制后台驻留反而是一种负责任的行为,强行保活既影响用户体验也容易被应用商店审核拒绝。
我用的策略是区分场景:如果用户手动结束训练并切到后台,我直接停止一切持续任务;如果用户开始计时训练后按了 Home 键,我需要保证计时继续,用前台服务持续运行并更新通知栏进度。action 是在服务里处理 startCommand 的 START_STICKY,这样即使进程被系统回收,也会尝试重新创建。
6.3 多厂商适配的时候发现的差异
真机测试阶段我发现,各厂商 ROM 的后台策略差别远超预期。同样一个前台服务,在 A 手机上能稳稳运行,在 B 手机上却可能被"智能省电"杀掉。这不是你的代码问题,是各厂商自研系统的进程管理差异。
解决思路分两步:一是保证你的前台服务是"用户可见、有通知、持续更新"的,尽量提高系统杀掉它的成本;二是在首次使用时做一个轻量的引导提示,告知用户如果发现后台计时异常,可以把 APP 加入系统的"自启动管理"或"后台运行"白名单。不要引导用户做极端操作,点到为止即可。
7. 测试、打包与上架之前必须做的检查
7.1 测试并非只是模拟器
我把测试分了三层:单元测试、数据库测试、真机兼容测试。单元测试主要针对卡路里计算和目标进度判断这些纯逻辑方法;数据库测试用 Room 的 in-memory 数据库验证 DAO 查询;真机测试则覆盖了三台不同品牌机型,分别对应 Android 10、13 和 14 的系统版本。
真机测试中发现比较典型的问题是数据库查询在主线程执行导致的卡顿。开发时我用 Flow 处理数据没什么感觉,但低端机上偶发卡顿,后来查日志发现是 Room 查询结果映射耗时。我改用协程加 Dispatchers.IO 统一调度查询,并把列表数据的 RecyclerView 适配器改成了 DiffUtil 差量更新,流畅度明显提升。
7.2 包体、混淆与签名
打包 release 之前,我的 APK 体积是 28MB,主要体积来自图表库和 Material 组件库。我通过开启资源压缩和启用 R8 代码压缩,把包体压到了 11MB 左右。R8 开启后要注意部分库的反射代码需要 keep 规则,比如图表库的某些自定义对象,我在 proguard-rules.pro 里补了一条 keep。
签名我直接用 Android Studio 的 Generate Signed Bundle 生成签名文件和 keystore。这里一定要提醒大家:签名文件要备份到多个地方,丢失之后应用将永远无法升级覆盖安装。我身边就有开发者因为重装电脑丢了 keystore,只能被迫换包名重新上架的惨痛案例。
7.3 数据验证与回归清单
上架前我给自己列了这样的一个回归清单:首次安装时的权限引导是否顺畅;统计页的数据是否和数据库实测一致;时间跨越"今天"是否导致图表日期错乱;深色模式下折线图的颜色对比是否合适;旋转屏幕后正在编辑的训练记录有没有丢失;应用被系统杀掉后重新打开是否回到正确页面;计时模式退到后台再回来,卡路里和时间是否继续正确。
这些项目逐一跑完之后,我会在真机上把全流程再走两遍。因为记数据和看数据是健身 APP 的两条生命线,界面上一点小瑕疵可以以后改,数据一旦错位,用户的信任就回不来了。
8. 最后一点心得:工程化思维才是这个项目的核心
项目做完后回过头看,Android 技术本身并不难,难的是把一个模糊的"运动 APP"概念拆成可实施、可验收、可迭代的具体工程。我在第一周浪费了不少时间研究视频播放、社区评论这些花哨功能,后来才意识到,对一个没有强大运营支撑的个人项目来说,数据闭环和稳定性才是核心竞争力。
如果你也准备做类似的项目,我建议先画一张极简的数据流图:用户填写记录、数据存入本地数据库、统计页读取和展示趋势。把这条链路跑通,再考虑图表交互、动画、多语言、上架这些事情。每一步都要问自己一句"这是不是用户真正需要的",这个筛选过程会帮你省下大量时间。
最后分享一个小技巧:开发期间我每天都会用自己做的 APP 记录训练数据,用真实的数据去检验图表是否顺眼、输入是否方便、提醒是否烦人。自己不用的健身 APP,很难做得好用。希望这篇记录能给你的 Android 项目带来一点参考。