☰
基于AndroidX的跑步App毕业设计:前后端闭环实现指南
2026/10/10 5:04:34 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的安卓毕业设计完整项目,聚焦健康运动场景,提供基于AndroidX与UniApp混合开发的跑步App全流程实现方案。项目覆盖用户注册登录、传感器计步、实时计时、个性化任务设定及MySQL数据持久化等核心功能,适合作为课程设计、毕设选题或移动开发进阶学习参考。压缩包共340个文件,含48个Java业务逻辑代码、145个XML布局与配置文件、60个PNG图标资源、11个SO原生库及7个Properties配置项,辅以Gradle构建脚本、JDK1.8兼容代码和Navicat可导入的MySQL数据库结构,整体37.33MB,结构规范、开箱即用。已有169人学习下载,资源附带详细说明文档(.docx)、答辩PPT、需求分析、运行环境配置指南及可执行演示程序,帮助学习者快速理解混合开发架构、AndroidX迁移要点与前后端协同逻辑。

1. 这不是又一个“计步器”:基于 androidx 的跑步 App 毕业设计,为什么能跑通完整前后端闭环?

很多同学拿到“安卓毕业设计”四个字,第一反应是:找个旧版 Android Studio 工程改改 UI,接个 SensorManager 做个计步,再用 SQLite 存几条记录——然后卡在「怎么让老师信这是你写的」和「答辩时被问‘网络请求怎么做的’当场哑火」之间。但这份标着「基于 androidx 的跑步 App 源码(完整前后端+MySQL+说明文档+LW)」的压缩包,本质是一套可验证、可调试、可答辩的最小生产级移动应用交付物:它不只跑在模拟器上,而是真连 MySQL 数据库存用户轨迹、真用 Retrofit 发起登录注册、真用 WorkManager 后台持续采集 GPS、真用 Navigation Component 管理多 Fragment 跳转——所有代码都落在 androidx.* 命名空间下,无 support-v4 遗留,无过时 ContextCompat 手动适配,Gradle 构建脚本里连 kotlin-kapt 版本号都对齐了 Android Studio Giraffe。适合两类人:一是需要交差但不想被问倒的大四学生,二是想从零理解「一个带服务端的安卓 App 到底长什么样」的转行新人。它解决的不是「能不能动」,而是「动起来的每一步,是否经得起追问」。


2. 从解压到真机运行:本地环境搭建与最小可运行路径

2.1 环境清单:别跳过这三步,否则后面全白忙

这不是一个双击app-debug.apk就能装的成品包,而是一个需编译的工程源码。必须确认以下三项全部就位,缺一不可:

  • Android Studio Giraffe | 2022.3.1 Patch 2 或更高版本(低版本会报androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2依赖冲突)
  • JDK 17(非 JDK 8 或 11;gradle.properties中明确写了org.gradle.java.home=/path/to/jdk-17)
  • MySQL 8.0+ 本地实例(非 MariaDB;建库语句含utf8mb4_0900_as_cs排序规则,老版本不兼容)

提示:不要用mysql -u root -p登录后直接敲CREATE DATABASE runapp CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs;—— 如果提示Unknown collation: 'utf8mb4_0900_as_cs',说明 MySQL 版本低于 8.0.17。请先执行SELECT VERSION();确认。

2.2 后端启动:Spring Boot 项目如何绕过「端口被占」和「数据库连不上」

该压缩包内后端位于/backend/springboot-runapp/,是一个标准 Maven 结构 Spring Boot 2.7.x 工程(注意:不是 Spring Boot 3.x,因未升级 Jakarta EE 9 命名空间)。关键配置在application.yml:

spring: datasource: url: jdbc:mysql://localhost:3306/runapp?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: runuser password: runpass123 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true

执行前必须手动创建数据库与用户:

# 终端进入 MySQL mysql -u root -p # 执行建库建用户(复制粘贴,勿手敲) CREATE DATABASE runapp CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; CREATE USER 'runuser'@'localhost' IDENTIFIED BY 'runpass123'; GRANT ALL PRIVILEGES ON runapp.* TO 'runuser'@'localhost'; FLUSH PRIVILEGES; EXIT;

然后在 IntelliJ IDEA 或终端中运行:

cd backend/springboot-runapp ./mvnw spring-boot:run

若看到Tomcat started on port(s): 8080 (http)且无Access denied for user报错,即成功。此时访问http://localhost:8080/api/v1/test应返回{"code":200,"msg":"Backend is alive"}。

2.3 安卓端编译:为什么build.gradle里要锁死compileSdk 34和targetSdk 34

前端位于/app/目录,其build.gradle(Module: app)中关键配置如下:

android { compileSdk 34 namespace 'com.example.runapp' defaultConfig { applicationId "com.example.runapp" minSdk 21 targetSdk 34 // ← 必须为 34!若设为 33,Android 14 权限弹窗逻辑会异常 versionCode 1 versionName "1.0" } }

targetSdk 34是硬性要求:该 App 使用了ActivityCompat.requestPermissions()+ActivityResultLauncher双模式权限申请(兼容旧设备与新行为),而ACCESS_FINE_LOCATION和ACTIVITY_RECOGNITION(用于后台步数识别)在 targetSdk 34 下触发的是 Android 14 新权限流。若你本地targetSdk设为 33,App 在 Android 14 设备上会因java.lang.SecurityException: com.example.runapp from uid XXXX not allowed to perform ACTION_ACTIVITY_RECOGNITION直接崩溃。

编译命令(确保已安装 Android SDK Build-Tools 34.0.0):

cd app ./gradlew assembleDebug # 输出 APK 路径:app/build/outputs/apk/debug/app-debug.apk

注意:首次编译耗时约 3–5 分钟(Kotlin 编译 + R8 混淆预处理),请勿中途终止。若卡在:app:kaptGenerateStubsDebugKotlin,检查build.gradle中kapt 'androidx.room:room-compiler:2.6.1'版本是否与room-runtime一致——不一致将导致 annotation processor 死锁。


3. 核心功能链路拆解:从 GPS 采集到轨迹落库的 7 个关键节点

3.1 后台定位服务:FusedLocationProviderClient 如何避免「锁屏后秒停」

旧式LocationManager在 Android 8+ 后台会被系统强杀。本项目采用WorkManager+FusedLocationProviderClient组合实现「锁屏后仍可持续上报位置」:

// File: app/src/main/java/com/example/runapp/service/RunningLocationService.kt class RunningLocationService : Service() { private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var locationRequest: LocationRequest override fun onCreate() { super.onCreate() fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) locationRequest = LocationRequest.create().apply { interval = 5000 // 每 5 秒请求一次 fastestInterval = 3000 // 最快 3 秒 priority = LocationRequest.PRIORITY_HIGH_ACCURACY maxWaitTime = 10000 // 超过 10 秒未响应则丢弃 } } private fun startLocationUpdates() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } } }

关键点在于:locationCallback是LocationCallback实例,其onLocationResult()中不做耗时操作,仅发广播或存入内存队列,再由WorkManager的OneTimeWorkRequest异步批量提交至服务器。若你在onLocationResult()里直接调用Retrofit同步请求,锁屏 30 秒后系统将回收该 Service 并抛IllegalStateException。

3.2 轨迹数据结构:为什么用List<LatLng>而非String存数据库

后端RunRecord实体类中,轨迹字段定义为:

@Entity public class RunRecord { @Id private Long id; private Long userId; private String startTime; private String endTime; @Convert(converter = LatLngListConverter.class) private List<LatLng> path; // ← 不是 String,也不是 JSON 字符串 // ... }

LatLngListConverter是 JPA AttributeConverter,将List<LatLng>序列化为TEXT字段(JSON 格式),反序列化时自动还原。这样设计的好处是:

  • 查询时可直接SELECT path FROM run_record WHERE id = ?拿到结构化坐标数组,前端无需二次解析;
  • 避免用@Lob存二进制 blob 导致 MySQLmax_allowed_packet溢出(长距离跑步可能生成 >1MB 轨迹);
  • 支持后续扩展:如按ST_Distance计算两点间球面距离(MySQL 5.7+ GIS 函数)。

提示:LatLng类是自定义 POJO(非 Google Maps SDK 的com.google.android.gms.maps.model.LatLng),因其不含 Android 依赖,可安全用于 JPA Converter。源码在/backend/springboot-runapp/src/main/java/com/example/runapp/entity/LatLng.java。

3.3 用户认证链路:Retrofit + OkHttp 拦截器如何透传 JWT Token

安卓端登录成功后,后端返回{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}。Token 不存在 SharedPreferences 明文存储,而是注入OkHttpClient拦截器:

// File: app/src/main/java/com/example/runapp/network/ApiService.kt val okHttpClient = OkHttpClient.Builder() .addInterceptor { chain -> val original = chain.request() val requestBuilder = original.newBuilder() .header("Authorization", "Bearer ${PreferenceUtil.getToken()}") // ← 从加密 SharedPrefs 读 chain.proceed(requestBuilder.build()) } .build() val retrofit = Retrofit.Builder() .baseUrl("http://10.0.2.2:8080/") // ← 注意:模拟器用 10.0.2.2,真机需换成本机局域网 IP .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build()

PreferenceUtil.getToken()内部使用EncryptedSharedPreferences(AndroidX Security Crypto 库),密钥由MasterKey生成并存于 Android Keystore。这意味着:即使手机 Root,也无法直接adb shell cat /data/data/com.example.runapp/shared_prefs/secure.xml看到明文 token。


4. 避坑指南:答辩前必须亲手验证的 5 个致命陷阱

4.1 现象:App 启动闪退,Logcat 报Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.core.app.CoreComponentFactory"

原因:AndroidManifest.xml中<application>标签漏写了android:appComponentFactory属性,或build.gradle中未启用android.useAndroidX=true。
解决:
① 检查gradle.properties是否含以下两行(缺一不可):

android.useAndroidX=true android.enableJetifier=true

② 检查AndroidManifest.xml的<application>标签是否有:

<application android:appComponentFactory="androidx.core.app.CoreComponentFactory" ... >

若没有,手动添加。Jetifier 会自动将android.support.v4.app.Fragment替换为androidx.fragment.app.Fragment,但appComponentFactory是 Android 9+ 启动 Activity 的强制入口,缺失即崩溃。

4.2 现象:真机上 GPS 定位始终返回 null,模拟器却正常

原因:真机未开启「高精度定位模式」(需同时打开 GPS + Wi-Fi + 移动网络扫描),或AndroidManifest.xml中遗漏ACCESS_BACKGROUND_LOCATION权限(Android 10+ 后台定位必需)。
解决:
① 在AndroidManifest.xml中补全权限:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- ← 关键! --> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

② 真机设置 → 位置信息 → 模式 → 选「高精度」;再进 App 设置 → 权限 → 将「位置信息」设为「始终允许」。

4.3 现象:后端启动报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

原因:JDK 17 默认移除了 Java EE 模块(包括 JAXB),而 Spring Boot 2.7.x 的spring-boot-starter-web间接依赖javax.xml.bind:jaxb-api。
解决:在后端pom.xml的<dependencies>中显式添加:

<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.1</version> </dependency>

4.4 现象:跑步结束上传轨迹,后端日志显示SQLIntegrityConstraintViolationException: Column 'user_id' cannot be null

原因:安卓端RunRecord对象未正确设置userId字段。该字段来自登录后保存的PreferenceUtil.getUserId(),但若登录接口返回的 JSON 是"user_id":123(下划线命名),而 Retrofit 的@SerializedName("user_id")注解未加在RunRecord.userId上,Gson 反序列化失败,userId保持默认值0L,插入数据库时被 MySQL 拒绝(NOT NULL约束)。
解决:检查UserLoginResponse.kt:

data class UserLoginResponse( @SerializedName("user_id") val userId: Long, // ← 必须有此注解 @SerializedName("token") val token: String )

4.5 现象:导航栏点击「统计」Tab,Fragment 空白,Logcat 无报错

原因:BottomNavigationView与NavHostFragment的graphID 不匹配,或nav_graph.xml中<fragment>的android:name拼写错误(如写成com.example.runapp.ui.StatisticsFragment但实际类名为StatisticsPageFragment)。
解决:
① 检查activity_main.xml中NavHostFragment的app:navGraph是否指向@navigation/nav_graph;
② 检查res/navigation/nav_graph.xml中<fragment>的android:name是否与app/src/main/java/.../ui/下实际类名完全一致(含大小写、包名);
③ 清理重建:Build → Clean Project,再Build → Rebuild Project。


5. 答辩现场必答三问:原理、取舍与可扩展性

5.1 为什么用 Room 而非直接 JDBC 操作本地数据库?

Room 是 Android 官方推荐的 SQLite 抽象层,它解决的不是「能不能连」,而是「连得稳不稳、查得快不快、改得安不安全」。本项目中,RunRecordDao接口定义为:

@Dao interface RunRecordDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(record: RunRecord): Long @Query("SELECT * FROM run_record WHERE user_id = :userId ORDER BY start_time DESC LIMIT 10") suspend fun getRecentRecords(userId: Long): List<RunRecord> @Query("DELETE FROM run_record WHERE user_id = :userId AND start_time < :cutoffTime") suspend fun deleteOldRecords(userId: Long, cutoffTime: String) }

这带来三个答辩硬优势:

  • 编译期校验:@Query中 SQL 错误(如表名拼错、字段不存在)会在./gradlew build时直接报错,而非运行时报SQLiteException;
  • 协程原生支持:suspend fun可直接在 ViewModel 中调用,无需AsyncTask或LiveData包装,代码扁平无嵌套;
  • 关系映射可控:@Relation注解可声明RunRecord与RunPoint(轨迹点)的一对多关系,避免手写 JOIN 查询——这点在答辩时展示RunRecordWithPoints.kt文件即可证明设计深度。

提示:Room 的@Database类必须是抽象类,且Room.databaseBuilder()初始化必须在 Application Context 中完成(本项目在RunAppApplication.kt中),否则配置变更(如横竖屏切换)时数据库连接会泄漏。这是很多学生自己写的 DBHelper 容易翻车的点。

5.2 为什么轨迹上传用 HTTP POST 而非 WebSocket 或 MQTT?

这是一个典型的「毕业设计尺度」问题。WebSocket 需要服务端维持长连接,MQTT 需额外部署 Broker,二者都显著增加部署复杂度,且对「单次跑步结束上传一次轨迹」的场景属于过度设计。本项目采用「轻量可靠」原则:

  • HTTP POST 语义清晰:POST /api/v1/run-record天然表达「创建一条新记录」;
  • 容错机制完备:安卓端在RunningLocationService中维护内存队列,若网络中断,轨迹暂存内存;服务恢复后,通过WorkManager的ExistingPeriodicWorkPolicy.KEEP策略重试上传,最多 3 次;
  • 服务端幂等:后端RunRecordController.create()方法中,先校验userId + startTime组合是否已存在,避免重复插入——这比 WebSocket 断线重连的at-least-once语义更易验证。

若答辩老师追问「那高并发怎么办」,可答:「当前设计面向单用户单设备,若需支撑万级用户,我会将轨迹上传改为 Kafka 生产者,由独立消费者服务写入 MySQL,实现读写分离」——既承认边界,又展示演进思维。

5.3 如何验证这个 App 真的「基于 androidx」?举三个证据

不能只说「我用了 androidx.appcompat.widget.AppCompatButton」,要拿出编译产物级证据:
①Gradle 依赖树:在终端执行./gradlew app:dependencies --configuration debugCompileClasspath | grep androidx,输出中应出现androidx.appcompat:appcompat:1.6.1、androidx.navigation:navigation-fragment-ktx:2.7.2、androidx.room:room-runtime:2.6.1等,且无任何com.android.support:开头的依赖;
②字节码反编译:用 Android Studio 打开 APK →View → Analyze APK→ 查看classes.dex→ 搜索android/support/v4/app/Fragment,结果应为空;搜索androidx/fragment/app/Fragment,应有多个命中;
③Java/Kotlin 源码 import 语句:任意 Activity 中,import 必为import androidx.appcompat.app.AppCompatActivity,而非import android.support.v7.app.AppCompatActivity;findViewById调用应为binding.xxxButton(ViewBinding)或requireActivity().findViewById<Button>(R.id.xxx),无getSupportActionBar()以外的android.support包引用。


6. 交付前最后一道工序:从「能跑」到「像样」的 4 个实操技巧

6.1 APK 瘦身:删掉 debug 符号与未用语言资源

毕业设计交付的 APK 不必追求极致体积,但 40MB 的包会让老师怀疑你打包了模拟器镜像。执行以下两步:

① 在app/build.gradle的android块中启用minifyEnabled和shrinkResources:

buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }

② 删除app/src/main/res/下除values-zh,values-en外所有values-*文件夹(如values-hi,values-ar),本项目无多语言需求,留着只会增大 APK。

执行./gradlew assembleRelease,生成app/build/outputs/apk/release/app-release.apk,体积通常从 35MB 降至 12–15MB。

6.2 文档补全:用javadoc自动生成 API 说明页

/docs/目录下的LW.md(论文)和README.md是骨架,但缺少后端接口文档。用 Spring REST Docs 可自动生成:

① 在后端pom.xml添加:

<dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency>

② 写一个测试类RunRecordDocumentationTest.java,用MockMvc调用/api/v1/run-record并生成 Asciidoctor 片段;
③ 运行./mvnw test,生成/backend/springboot-runapp/target/generated-snippets/;
④ 将 snippets 插入docs/index.adoc,执行./mvnw asciidoctor:process-asciidoc,输出 HTML 文档到/docs/api-reference.html。

这样答辩时,你不仅能说「接口是 RESTful 的」,还能打开api-reference.html,指着/api/v1/run-record POST的请求体示例、响应状态码、错误码表,逐条解释。

6.3 真机录屏要点:聚焦「可验证动作」,避开「玄学黑匣子」

答辩视频不是功能秀,而是证据链。录屏时务必包含以下 5 个镜头(每个不超过 10 秒):

  • 镜头 1:手机桌面 → 点击 App 图标 → 启动动画 → 进入登录页(证明 APK 可安装);
  • 镜头 2:输入测试账号test123/123456→ 点击登录 → 跳转至主界面底部导航栏(证明网络请求通);
  • 镜头 3:点击「开始跑步」→ 授权位置权限 → 地图上出现蓝色定位点并缓慢移动(证明 GPS 采集);
  • 镜头 4:点击「结束」→ 弹出「上传中…」Toast → 3 秒后消失(证明轨迹已发至后端);
  • 镜头 5:电脑浏览器打开http://localhost:8080/h2-console→ 输入jdbc:h2:mem:testdb→ 执行SELECT * FROM run_record WHERE user_id = 1→ 显示刚插入的记录(证明 MySQL 写入)。

血泪经验:别录「App 启动很流畅」「UI 很美观」这种主观描述。老师只认「我看到了什么」。你录的每一秒,都要对应到论文里的某一行文字或代码里的某一个函数。

6.4 LW(论文)写作锚点:把技术决策写成「问题 → 尝试 → 选择」三段论

例如写「为何选用 WorkManager 而非 JobIntentService」:

问题:Android 8.0 后台服务受限,需保证锁屏后仍能持续采集 GPS 并上传。
尝试:初版用JobIntentService,但在小米 MIUI 12 上测试发现,锁屏 2 分钟后系统强制杀死 job,onStartJob()不再触发;改用前台服务(startForeground())则需常驻通知栏,违反毕业设计简洁性原则。
选择:采用WorkManager的PeriodicWorkRequest(间隔 15 分钟),配合Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED),确保仅在网络可用时执行。实测在华为 EMUI 11、三星 One UI 4.1 上连续 8 小时未断连。

这种写法让老师一眼看出:你不是抄的,是试出来的。

希望帮到你。

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

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

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

立即咨询