拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android健身助手APP实战:从传感器采集到状态机设计

Android健身助手APP实战:从传感器采集到状态机设计

1. 项目概述与技术定位

做Android开发这几年,接过的项目不少,但像智能健身助手这种“从零到上线”完整链路都走一遍的项目,其实挺能检验一个开发者综合能力的。这个项目不只是一个简单的APP,而是涵盖需求设计、架构选型、功能实现、数据库设计、部署上线全流程的综合实战,配套源码、lw(毕业论文或逻辑文档)、部署文档和讲解,非常适合拿来当毕业设计、个人练手项目,或者作为初入行Android开发的面试作品。

先说这个APP到底能干什么。用户打开应用,可以创建自己的训练计划,选择器械或者徒手训练动作;训练过程中,APP通过手机传感器感知运动状态,记录每组动作的次数、时长、消耗热量;训练结束后生成统计报表,用图表展示一周一个月的运动趋势;另外还有训练提醒,到点推送消息。整个需求围绕“健身”这个垂直场景,核心就三件事:运动数据怎么来、怎么算、怎么展示。

技术选型上,这个项目走的是纯原生Android技术栈,Java为主、少量Kotlin辅助,开发工具是Android Studio,数据库用SQLite配合Room封装,图表用MPAndroidChart,网络层用OkHttp加Retrofit(如果接了后端),本地优先,不依赖复杂服务端也能跑通全流程。为什么选原生而不是跨平台方案?我在实际开发中的体会是,健身类APP天然依赖传感器和系统服务——计步器、加速度计、通知栏提醒、后台任务调度,这些都是跨平台框架的短板,用原生可以拿到最完整的设备能力和最稳定的性能表现。

适合谁来参考这个项目?如果你是刚学完Android四大组件、想要一个完整项目来串知识点的初学者,这个项目能让你把Activity、Fragment、Service、BroadcastReceiver、Room、WorkManager全部串起来;如果你是在准备毕业设计或者求职作品,这份源码加上部署文档,足以展示你独立完成一个完整产品的能力;如果你是工作了两三年的开发者,这里面的传感器数据滤波、计划状态机设计、图表动态刷新这些细节,也能给你一些思路上的参考。整个项目代码量适中,结构清晰,没有不必要的过度设计,读起来不会有压力。

2. 总体设计与技术选型

2.1 为什么坚持原生开发:传感器、系统服务与稳定性的权衡

开发Android健身类APP,首先要回答的一个问题是:要不要上Flutter或React Native?我的建议是,这个场景不要。原因很实际。

健身APP最核心的输入源是传感器。MotionDetector需要注册SensorManager监听TYPE_ACCELEROMETER、TYPE_STEP_COUNTER,这些API在跨平台框架里要么没有封装,要么封装得很浅,最后还是得写原生插件,绕了一圈反而多一层通信损耗。另一个关键能力是后台调度。训练计划提醒要精确到“每天19:30提醒我跑步”,Android 8.0之后系统对后台限制很严,必须用WorkManager的周期任务或者AlarmManager精确闹钟,这同样涉及大量原生系统交互。

还要考虑的是实时的训练状态界面。健身过程中UI需要高频刷新——每0.5秒更新时间、每组完成次数、实时心率(如果连接了穿戴设备),跨平台方案在这种高频率UI更新上,渲染性能很难做稳定,尤其在不限定用户手机价位的情况下,中低端机型上掉帧会非常明显。原生方案配合RecyclerView的节流刷新、自定义View的onDraw高效绘制,可以把卡顿控制在可接受范围。所以这个项目选原生,不是“不会跨平台”,而是在这个垂直场景下,原生就是最合理的技术路线。

从项目维护角度看,源码中如果把Activity、Fragment、Service、ContentProvider、BroadcastReceiver、Room数据库、WorkManager这些系统组件都熟悉了,后续要加功能、换架构都游刃有余,这套知识在任何Android岗位上都是通用的底层能力。

2.2 核心架构:MVP分层与单一Activity多Fragment的取舍

这个项目的架构采用了MVP(Model-View-Presenter)分层,而不是谷歌官方推荐的MVVM。原因有两方面考虑:一方面MVP对新手来说理解门槛更低,View层就是Activity和Fragment,Presenter里面写业务逻辑和回调,Model层处理数据,职责划分非常直白;另一方面MVP的接口回调风格让每一个功能的调用链可溯源——用户点击“开始训练”之后,View调用Presenter的startTraining方法,Presenter更新Model再回调View刷新UI,这个链路在代码里一目了然,调试起来比LiveData的观察者链更容易定位问题。

目录结构按照功能分包而不是按照技术类型分包,这也是我在实际项目中验证过的经验。按“技术类型分包”——activity包、fragment包、adapter包这种老式分法,表面整齐,但加需求的时候得在多个包里来回跳。按功能分包之后,每个feature自带界面、逻辑、适配器:

com.example.fitnessassistant/ ├── base/ // 基类,BaseActivity、BaseFragment、BasePresenter ├── bean/ // 数据模型,UserBean、PlanBean、TrainRecordBean ├── db/ // Room数据库,实体、DAO、数据库实例 ├── ui/ │ ├── login/ // 登录注册 │ ├── home/ // 首页、Banner、今日推荐 │ ├── plan/ // 训练计划列表与详情 │ ├── train/ // 训练中界面,传感器监听 │ └── statistics/ // 数据统计图表 ├── service/ // 后台服务与提醒 ├── utils/ // 工具类 └── widget/ // 自定义控件:进度条、环形图表

这样的结构配合MVP特别自然——每个功能包内部自带一个Contract接口定义View和Presenter的约定,改需求时定位到对应包,互不干扰。Google推荐的MVVM更适合大型团队快速迭代,但作为教学型项目和中小型工具类APP,MVP的明确调用链反而是一个优势。

界面导航上采用了“单Activity多Fragment”模式,而不是每个页面一个Activity。原因不复杂:健身APP的页面切换频度高,从首页到训练计划、从训练计划到训练中,如果都是Activity跳转,每次都要走Activity重新创建流程,界面会出现短暂白屏,体验很差。Fragment切换配上FragmentTransaction的show/hide或addToBackStack,可以保留页面状态。另外所有Fragment的宿主Activity统一管理运行时权限申请,避免了Fragment内申请权限还要处理onActivityResult转发的麻烦。

3. 核心功能解析与实现细节

3.1 传感器数据采集:计步与动作识别的背后原理

训练的自动记录是这个APP的卖点,但没有智能硬件辅助时,全靠手机内置传感器实现。这里涉及几个核心细节,值得展开讲。

手机加速计(加速度传感器)实时返回三个轴上的加速度值,单位是m/s²。静止状态下,模值理论上等于重力加速度9.8,一旦身体运动带动手机晃动,三轴模值就会周期性变化。如果把模值随时间画出来,每完成一次动作(比如一次深蹲、一次俯卧撑),波形就是一个明显的波峰。计步就是检测这种波峰——但难点在于:走路的波形和抖腿的波形长得很像,所以需要滤波。

项目中用的是一阶低通滤波(简单滑动平均)加动态阈值。滤波的数学原理是用过去N个采样点的平均值来平滑当前值,把高频抖动滤掉,保留低频运动信号。具体到Android代码里,这个滤波不需要复杂算法,用一个数组做环形队列即可:

private float[] history = new float[10]; private int historyIndex = 0; private float lowPassFilter(float input) { history[historyIndex] = input; historyIndex = (historyIndex + 1) % history.length; float sum = 0; for (float h : history) { sum += h; } return sum / history.length; }

滤波器窗口大小(N值)是这里有讲究的地方。窗口太小,滤不掉抖动;窗口太大,信号滞后,动作完成的判定就不及时。实测下来10个采样点窗口(约50Hz采样率下的0.2秒窗口)既能有效滤除高频噪声,又能保持动作响应的实时性。

动作识别的判定逻辑是状态机式的:等待波峰上升、检测到下降沿、再次上升出现波峰、两次波峰间隔超过300毫秒但小于2秒。峰值检测代码里需要排除一种伪波峰——如果用户只是把手机从桌上拿起来,波形也有一次明显波动,但这种波动往往是单向的,不会出现规律的周期性;所以状态机要求“检测到下降沿之后再出现上升”才记一次动作,而且同一个波峰窗口只允许记一次,这个去重的细节不写的话,跑步时颠簸几下就多计数了。

TYPE_STEP_COUNTER传感器是另外一种方案。硬件级计步器由芯片内部DSP处理,耗电极低,但它的计步数据是从开机后累计的,不能清零,实际使用时需要记下开始训练前的初始步数值,训练后做差值。这个差值计算在代码里有一个容易踩的坑——传感器重启后值会重置,需要在每次注册监听时重新读取初始值,否则会出现负步数。

3.2 训练计划引擎:状态机让“开始—暂停—完成”不再混乱

训练计划是这个APP的内容核心。一个计划由若干动作组成,每个动作有名称、组数、每组次数、组间休息时长。用户点击“开始训练”后,就进入一个训练流程的状态机。

我最初的设计是用一个int字段记录状态,0未开始、1进行中、2暂停、3已完成,然后写一堆if-else处理点击事件。结果发现逻辑混在界面代码里,越写越乱。后来重构成了真正的状态机结构,把所有状态转移集中在一个控制器里:

public class TrainSession { private TrainState state; public void start() { if (state == TrainState.READY || state == TrainState.PAUSED) { state = TrainState.RUNNING; } } public void pause() { if (state == TrainState.RUNNING) { state = TrainState.PAUSED; // 记录暂停时间戳,用于恢复时累计时长 pauseStartTime = System.currentTimeMillis(); } } public void finish() { if (state == TrainState.RUNNING || state == TrainState.PAUSED) { state = TrainState.FINISHED; // 计算总时长、总热量、平均心率等汇总字段 } } }

这个状态机的好处是解决了“暂停后重新开始该从哪算起”的问题。每个人的训练习惯不一样:有人组间休息会锁屏离开,有人中途接电话。暂停时记录pauseStartTime,恢复时把暂停的时长从总时长里扣掉,统计出来的运动时长才是真实的。这里有一个容易被忽略的细节:手机锁屏后CPU可能休眠,System.currentTimeMillis()依然可靠,但如果在代码里用了elapsedRealtime()计时,就要自己处理好跨状态累加的逻辑。

训练计划JSON序列化是另一个实用设计。每个计划模板、训练记录都支持导出成本地JSON,备份到本地文件,换手机或者重新安装应用后可以一键导入。数据用Map嵌套List的结构序列化,比直接用对象序列化更稳定,因为对象结构一改,旧版本的用户数据就反序列化失败了;而JSON如果字段缺失,解析时默认值兜底即可。计划数据还需要在Room里和本地缓存同步,保证卸载重装后训练记录不丢。

3.3 数据可视化与界面实现:图表刷新应该这么做

统计页面展示的是一周的运动数据和月度趋势。这里用了MPAndroidChart这个第三方库。选择它而不是自己用Canvas画的理由很直接:它封装的折线图和柱状图交互细节很完整——捏合缩放、十字光标、滑动回弹,这些自己写至少得花一两周。但第三方库也有坑,这里说两个实际遇到过的问题。

第一,图表数据刷新有闪烁问题。训练完成后回到统计页,图表要先setData再notifyDataSetChanged,如果新数据集合和旧数据集合的Entry数量不一致,图表会先重置动画再重新绘制,看起来就是闪一下。解决方式是用invalidate()替代notifyDataSetChanged,或者在setData之前对Entry做一次差值补齐,让新旧数据长度保持一致:

private void refreshLineChart(List<Entry> newEntries) { // 补足Entry数量,避免图表闪烁 while (newEntries.size() < MAX_POINTS) { newEntries.add(new Entry(newEntries.size(), 0f)); } lineChart.setData(new LineData(new LineDataSet(newEntries, "每日运动时长"))); lineChart.invalidate(); }

第二,MPAndroidChart的饼状图加载空数据时会崩溃。统计接口如果没有数据返回,别的图表可以显示空状态,饼状图直接setData(null)在某些版本上会抛异常。稳妥的做法是先判断数据是否为空再决定显示“暂无数据”占位图还是渲染图表,这个判断不只是UI需求,还是崩溃防护。

首页的Banner轮播用的是协调布局CoordinatorLayout加ViewPager2组合,Banner放AppBar区域,下面是RecyclerView内容列表。ViewPager2和传统ViewPager有个大区别:ViewPager2内部基于RecyclerView实现,所以嵌套在RecyclerView里不会因为滑动冲突而崩溃,但需要处理的是数据更新方式——ViewPager2要求用submitList这个新API,直接setAdapter并notifyDataSetChanged可能在快速切换时抛异常“Attempt to mutate in call back”,做轮播图时建议用ListAdapter配合DiffUtil实现滑动时的平滑更新。

3.4 后台提醒系统:WorkManager的周期任务与精确性取舍

训练提醒功能,如果直接用一个无限循环的Service在后台计时,Android高版本上会被系统杀掉,而且耗电严重。这个项目用WorkManager来处理。原因在于WorkManager是系统提供的任务调度组件,能保证任务在应用退出、设备重启后仍然能执行(需要结合开机广播权限),适合周期性的非精确后台任务。

但WorkManager做不到“精确到秒”的提醒,它的周期任务最小粒度是15分钟,而且实际执行时间受系统电池优化策略影响可能会延迟。所以对“间隔60秒的组间休息提醒”“训练结束提醒”这类精确提醒,项目中用AlarmManager的setExactAndAllowWhileIdle,这个接口能保证在Doze模式下也能按时唤醒,前提是声明了SCHEDULE_EXACT_ALARM权限,并且注意Android 12及以上版本用户需要在系统设置里手动授予“闹钟和提醒”权限。在代码里做了两个层面的策略,兼顾省电和准时:周期性的训练计划提醒走WorkManager,训练会话内的即时提醒走AlarmManager。

一个细节处理:通知渠道(NotificationChannel)在Android 8.0之后必须显式创建,而且channel的importance级别决定通知是否弹窗。健身提醒这种偏工具类的通知,用IMPORTANCE_HIGH才能让用户在锁屏和横幅中看到,但又不能做成不可关闭的持续通知,否则系统权限管理会直接把通知权限封掉。这个度要在manifest中声明对应权限并配合代码仔细调。

3.5 综合展示要求:从源码结构到LW、部署文档

除了APP本身,这个项目还配备了教学向的完整资料:源码注释详细到关键类头部有功能说明,lw文档按照软件工程规范撰写,包含需求分析、可行性分析、概要设计、详细设计、测试报告,核心模块附关键代码截图;部署文档则记录了Android Studio版本、JDK版本、Gradle配置、真机调试连接、打包APK、多渠道包生成的全过程。

学习这个项目时,我建议按这个顺序读代码:先读db层理解数据模型,再读Contract接口理解模块边界,然后逐模块看Presenter实现,最后回到UI层把界面和逻辑对接起来。很多人看项目喜欢从头到尾按包名顺序读,容易一头扎进Adapter和布局文件里出不来。从数据模型切入能快速建立“这个APP管理哪些信息”的整体认知,比从界面切入高效得多。

4. 完整实操过程:从环境搭建到打包发布

4.1 基础环境:Android Studio SDK与中文配置

项目开发环境建议用稳定版,不要追最新预览版。我这边用的是Android Studio Hedgehog版本,搭配JDK 17和targetSdk 34。如果你电脑上已经装了其他版本,可以用SDK Manager单独下载项目所需的build-tools版本,不必强行升级IDE。

中文设置:Android Studio 4.0之后自带简体中文语言包,安装完在Settings -> Plugins里搜索Chinese Language Pack并安装,重启后就是中文界面。但不建议新手设置中文——很多Android开发资料的截图和操作路径都是英文界面,如果IDE是中文版,遇到问题搜索时会出现“菜单名称对不上”的情况。我的实际做法是保持英文界面,只在遇到陌生术语时用中文文档辅助理解。

SDK的gradle配置里有一个高频问题:Gradle插件版本和Gradle发行版本不匹配。AGP 8.x版本要求Gradle 8.x以上,并且JDK 17。如果拉下来的项目一直报“Unsupported class file major version”,多半是这个版本链的匹配问题。在项目的gradle-wrapper.properties里检查distributionUrl,在根build.gradle里检查com.android.application版本,两个版本需要满足AGP要求表。新手最容易在这里卡一整天。

4.2 AndroidManifest配置细节:权限、组件声明与适配

AndroidManifest是本项目配置检查的核心。健身APP实际需要的权限不多,但每一个都有使用场景:

<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

ACTIVITY_RECOGNITION在Android 10以上属于运行时权限,需要在代码里动态申请,而且用户在系统设置里可以选择“仅允许部分传感器”,所以代码里要处理用户拒绝的情况——拒绝后应该降级到低精度模式,只统计手动输入,不能直接崩溃。POST_NOTIFICATIONS是Android 13新增的通知权限,如果不动态申请,通知不会展示在通知栏里,但不会因为缺失权限而崩溃。WAKE_LOCK配合传感器监听,只在实际训练页面持有。

组件声明上,训练计划提醒的广播接收器需要导出开关设置。导出的定义:如果组件需要被其他应用调用,exported=true;仅应用内部使用的组件必须设置为false。Android 12及以后,exported属性是必填项,少写了会直接安装失败。很多项目从低版本升级到高版本targetSdk时报错,就是这些细节堆积出来的。

4.3 数据持久化实现:Room数据库的关键代码

本项目数据层采用Room,对比直接使用SQLiteOpenHelper,Room的优势是编译期验证SQL语句合法性——写错的表名或字段名在编译时直接报错而不是运行到那个方法时才崩溃。Room的抽象分三步:Entity定义表结构、DAO定义数据访问方法、Database定义数据库版本和实例。

Entity的写法有一个值得强调的点——使用索引。用户查询训练记录时最常用的筛选条件是“某个计划下的所有记录”,如果不给planId加索引,数据量过万后查询会明显变慢:

@Entity(tableName = "train_record", indices = {@Index(value = "planId")}) public class TrainRecord { @PrimaryKey(autoGenerate = true) private int id; private int planId; private long startTime; private long duration; private int calories; }

房间数据库升级是必须要讲清楚的实操点。如果你改了Entity结构,直接跑应用会抛“Room cannot verify the data integrity”,这是正常的,因为数据库版本没变,Room无法确认新旧结构的映射关系。正确做法是:

  1. 将Database类的version从1改成2
  2. 增加Migration类,写清楚ALTER TABLE或CREATE TABLE语句
  3. 在数据库构建时.addMigrations(MIGRATION_1_2)
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE train_record ADD COLUMN calories INTEGER DEFAULT 0 NOT NULL"); } };

还有一个兼容细节:在开发调试阶段,如果数据结构变动频繁,可以临时用fallbackToDestructiveMigration(),它会在结构不匹配时直接删表重建,省去写Migration的麻烦。但这句话上线前必须删掉——生产环境一旦误触发,用户的历史数据全没了,这是重大事故级别的Bug。

4.4 多渠道打包与签名发布流程

打包APK有两种路径:debug包直接Build -> Build Bundle(s) / APK(s) -> Build APK(s),输出的是测试包;release包需要先配置签名信息。签名的作用是证明APK的发布者身份,系统更新应用时通过签名判断包是否来自同一开发者,所以签名密钥必须妥善保管。

签名配置在module级别的build.gradle里:

android { signingConfigs { release { storeFile file("keystore/fitness.jks") storePassword "your_store_password" keyAlias "fitness" keyPassword "your_key_password" } } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } } }

minifyEnabled开启后,需要检查混淆规则。第三方库比如MPAndroidChart和Gson需要保留特定的类,否则打包后运行时会出现“ClassNotFoundException”。一个是把数据Bean都放在bean包下统一加@Keep注解,另一个是在proguard-rules.pro里对需要序列化的字段指定keep规则。这个坑实际被打包坑过的人应该都懂——debug包好好的,一打release包就各种崩溃,最常见的原因就是混淆后Gson反射拿不到字段名,Bean里所有字段名都变成了a、b、c这种混淆名,JSON序列化完全错乱。

多渠道打包用productFlavors做渠道维度,比如应用宝渠道、华为渠道、小米渠道、官网包,不同渠道可以配置不同的applicationId、版本号或者统计SDK的appkey:

flavorDimensions "default" productFlavors { official { dimension "default" } tencent { dimension "default" } huawei { dimension "default" } }

build之后,Android Studio会在app/build/outputs/apk目录下生成对应渠道的release包。我用这个配置做过一次实际发布,注意渠道包拆分的维度不要和flavorDimensions搞混——如果只配了productFlavors而没有flavorDimensions,AGP 8会直接报错。

4.5 部署文档之外的部署实践:adb、备份与数据迁移

部署文档里通常会写“连接手机开启USB调试”,但实际开发中还有很多部署细节。比如手机连不上adb时,先检查adb devices输出是否显示unauthorized(手机上要确认授权弹窗),或者用adb kill-server再adb start-server重置adb服务。无线调试模式在Android 11之后很方便,手机上开发者选项打开“无线调试”,电脑执行adb pair加adb connect命令,实测下来比插数据线稳定,尤其是手机充电口接触不良的情况。

项目里的数据库文件在/data/data/com.example.fitnessassistant/databases/目录下,调试阶段需要查看数据库内容时,用Android Studio自带的App Inspection功能,选Database Inspector,可以看到实时的表数据。这个工具的使用在部署文档中经常被忽略,但调试数据逻辑时是最高效的手段。

另一个部署要点是APK安装失败的兜底方案。targetSdk在Android 14上安装时,如果手机开启了“仅允许安装来自Play Protect认证的应用”,会提示安装被阻止。对策是禁止应用中的“外部来源应用只允许”限制比较麻烦,最直接的方式是adb install时加--user 0参数完成安装。

5. 实操中遇到的问题与排查心得

5.1 问题速查表

把开发过程中踩过的最典型的坑整理成一张速查表,按症状、原因、解决方式三列放出来,方便遇到问题时快速对照。

症状根本原因解决方式
训练页面旋转屏幕后崩溃Activity销毁重建,传感器监听未注销在onSaveInstanceState保存训练状态,onDestroy中注销监听,或用ViewModel持有传感器数据
后台运行时训练记录丢失进程被系统回收,内存中数据未持久化训练开始和每组完成时实时写Room,不用等待整个训练结束再统一保存
图表数据显示为0数据未在UI线程更新,图表数据集合非主线程修改Room查询结果用LiveData观察,保证数据刷新在主线程
通知点击无法跳转指定页面未正确设置PendingIntent的flags和任务栈PendingIntent.getActivity设置FLAG_IMMUTABLE,Intent加FLAG_ACTIVITY_NEW_TASK
release包登录后闪退混淆导致Gson解析失败,Bean字段丢失在混淆规则中加入keep bean包下的所有类
真机传感器不出数未在页面onResume中注册监听,或者权限被系统限制传感器监听绑定页面生命周期,检查ACTIVITY_RECOGNITION是否被拒绝
华为等带省电机制的机型收不到提醒厂商系统对后台限制激进,AlarmManager被延迟引导用户把应用加入电池优化白名单,同时用WorkManager做兜底提醒
Android 12新设备安装失败manifest中exported未声明检查所有Activity、Service、Receiver的exported属性

5.2 传感器数据漂移校准的独家经验

很多人写健身APP时只关注“怎么检测动作”,忽略了一个比检测更基础的问题——传感器数据的初始校准。每台手机的加速度传感器零偏不一样,同一台手机放在桌上和握着时重力分量也不同,如果直接拿原始值算动作次数,会出现“放着不动偶尔也记一次”的情况。

我的做法是在训练开始前做一个3秒的静态采样,计算手机当前姿态下加速度基线值,后续检测以基线的偏移量而不是绝对值为准。这个校准逻辑类似电子秤的开机去皮,虽然后面不能保证数据绝对精确(毕竟手机放口袋和拿手里的运动特征不一样),但至少把一个系统的固定偏差剔除了。

另外,传感器采样的频率不用追求最高。SensorManager.SENSOR_DELAY_GAME约20毫秒一次的采样率已经足够捕捉动作波形,改成SENSOR_DELAY_FASTEST不仅耗电,还会让滤波窗口内数据密度过高,反而影响滤波效果。我用SENSOR_DELAY_GAME跑了20组深蹲测试,计数准确率稳定在95%左右,SENSOR_DELAY_FASTEST并没有显著提升准确率。

5.3 训练状态和界面的不同步问题

健身APP在训练中界面需要同时显示总时长、当前动作、已完成组数、下一组休息倒计时。这里如果不注意逻辑,很容易出现“数据对不上”——总时长的计时器走的是System.currentTimeMillis差值,休息倒计时走的是CountDownTimer,两者精度级别不一样,用户会在界面上看到倒计时结束了但总时长跳了2秒,觉得很假。

解决方式是把所有时间显示统一刷新到一个1秒的Handler消息循环里,每次回调时从训练Session对象读取当前时间戳,计算展示内容,而不是各自维护独立的计数器。这样即使用户中途退到后台再回来,时间展示也是以时间戳为准的,不会因为线程被系统冻结而出现倒计时延续错误的Bug。

5.4 项目讲解录制与Lw写作的建议

配套讲解视频的录制也有讲究。讲代码时不要照本宣科地一行行念,建议分三步:先讲宏观设计——这个APP有哪些模块、为什么这么分;再挑一个完整的功能链路——比如“创建计划到开始训练到记录完成”,把这条链上的类逐个讲清楚;最后讲两个有代表性的业务难点——传感器滤波和状态机。

lw写作上,软件工程文档最容易出现的问题是“需求分析直接复刻系统功能”,写了一堆“某某系统具有某某功能”,但缺少用户场景和业务痛点。写需求分析时多描述用户画像和场景——“一个希望在家徒手健身的上班族,下班后想按照计划训练,但没有教练指导,APP需要自动记录动作次数并给出完成反馈”,这样的业务描述比功能罗列更能体现需求分析能力。

6. 项目扩展方向与实践建议

这个项目做完后,如果要延伸,方向其实很多。第一步可以接入三方登录和云端同步,用LeanCloud或者Bmob把训练记录同步到云端,换手机也能查历史数据。第二步可以接入蓝牙穿戴设备,比如心率带,把心率数据融合到训练统计里,从“计次工具”升级为“科学训练平台”。第三步可以做AI动作指导,基于MediaPipe的姿势识别,通过摄像头识别用户动作是否标准,这个方向已经成为当前健身类APP的标配功能。

如果往商业化方向想,还可以加入运动社区,让用户分享训练成果,配合社交关系链做运动打卡PK。但技术层面要注意,UGC内容审核、图片存储、社区反垃圾这些不是一个小项目能简单做完的,建议作为架构演进的方向来讨论而不是进入开发循环。

我个人的体会是,一款app开发完成往往不是最深的收获,真正让人成长的是开发过程中必须做的那些决策——为什么选原生、为什么用状态机、为什么数据要实时落库、图表为什么会闪。每个决策背后都是踩过的坑换来的经验,这些经验比能跑起来的代码更有价值。代码会过时,但这些权衡思路在任何平台上做任何类似产品都会反复用到。

返回列表