尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android旅游APP真机部署实战:定位、权限、适配与签名全通关

Android旅游APP真机部署实战:定位、权限、适配与签名全通关 简介这是一套面向计算机专业本科生的Android毕业设计实战源码聚焦旅游服务类移动应用开发帮助学生快速完成具备完整业务闭环的课程设计或毕设项目。资源包含5828个文件主体为211个Java核心逻辑代码、763个XML界面布局与327个JSON数据配置文件辅以1073张PNG图标资源及2975个Flat资源文件整体压缩包达65.45MB结构清晰覆盖APP全生命周期模块。已有461人学习下载配套提供MySQL数据库SQL脚本、6套实操视频含环境配置、代码讲解、运行流程、项目结构说明文档及全部依赖软件显著降低部署门槛。读者可直接复用登录注册、景点介绍与智能推送、个性化路线规划、实时天气集成、周边美食推荐等六大功能模块并基于合理数据设计与美观UI实现快速二次开发与功能扩展。1. 这不是“做个界面就交差”的毕业设计一个真正能跑在真机上的 Android 旅游 APP要过得了定位、网络、权限、适配四道关很多同学把“基于 Android 的旅游景点 APP”当成课程设计级别的练手项目——拉几个 Activity贴几张景点图片加个 TextView 显示介绍导出 APK 就算完成。但现实是真机上一装就闪退、高德地图 SDK 初始化失败、Android 12 动态权限拒绝后无法重试、RecyclerView 滑动卡顿、华为/小米手机因后台限制收不到推送……这些不是“锦上添花的优化”而是毕业答辩前必须现场演示通过的硬门槛。本篇聚焦真实开发闭环从 Android Studio 环境搭建开始到 Gradle 配置、权限申请逻辑、景点数据本地缓存与网络加载双通道、基于 RecyclerView ViewPager2 的景点详情页、以及适配 Android 12API 31及以上版本的隐私合规要求。适合已学完《Android 基础》但没完整跑通过真机部署流程的本科生也覆盖了企业级 APP 开发中高频出现的兼容性陷阱。2. 用 Android Studio 2023.2.1 创建最小可运行工程避开 Gradle 8.x 与 AGP 8.2 的常见冲突2.1 为什么必须锁定 Android Studio 版本与构建工具链毕业设计提交常被卡在“环境不一致”你在本地用 Android Studio Flamingo 跑通导师用 Giraffe 打开却报Could not resolve com.android.tools.build:gradle:8.2.0。根本原因在于 Gradle 插件AGP与 Gradle 版本存在严格对应关系。Android Studio 2023.2.1Giraffe默认捆绑 AGP 8.2.0而它仅支持 Gradle 8.2 —— 若你手动升级 Gradle 到 8.3 或降级到 8.0都会触发Plugin [id: com.android.application] was not found或Unsupported Java version错误。因此第一步不是写代码而是固化构建环境。2.1.1 下载与验证 Android Studio 版本前往 developer.android.com/studio 下载Android Studio 2023.2.1 (Giraffe)安装包非 Canary 或 Preview 版。安装完成后在欢迎页点击Help → About确认显示Android Studio Giraffe | 2023.2.1 Build #AI-232.9921.75, built on July 26, 2023 Runtime version: 17.0.70--11029794 amd64提示若已安装其他版本请卸载干净再装 Giraffe。残留的.gradle缓存目录位于用户主目录下会导致新项目继承旧配置引发Could not determine the dependencies of task :app:compileDebugJavaWithJavac类错误。2.1.2 新建项目时的关键配置项启动 Android Studio →New Project→ 选择Empty Activity模板避免 Navigation Component 等复杂依赖干扰初期验证→ 设置包名com.example.travelapp不可含下划线或大写字母→ Minimum SDK 选API 21 (Android 5.0)覆盖 99.2% 设备且满足毕设硬件要求→ 点击 Finish。此时自动生成的gradle/wrapper/gradle-wrapper.properties内容应为distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists若 URL 中是gradle-8.3-bin.zip请手动改为8.2-bin.zip并保存。这是确保 AGP 8.2.0 与 Gradle 8.2 匹配的强制前提。2.1.3 修改 app/build.gradle 中的 targetSdkVersion 与 compileSdkVersion打开app/build.gradle将compileSdk和targetSdk统一设为33Android 13这是当前主流市场兼容性与功能支持的平衡点android { compileSdk 33 defaultConfig { applicationId com.example.travelapp minSdk 21 targetSdk 33 // ← 必须与 compileSdk 一致 versionCode 1 versionName 1.0 } }注意targetSdk 33意味着你必须处理 Android 12 的近似位置权限ACCESS_COARSE_LOCATION、POST_NOTIFICATIONS权限显式声明、以及android:exported属性对所有activity的强制要求。这些不是可选项而是系统级拦截条件。3. 实现景点列表页RecyclerView Retrofit Gson 的三层数据流落地3.1 为什么不用 WebView 加载 HTML 页面—— 毕设评审的隐性扣分点部分同学为省事用 WebView 加载一个本地 HTML 文件展示景点列表。这在答辩时极易被质疑“APP 的核心交互逻辑在哪里如何实现离线缓存如何与系统定位服务联动” 正确做法是构建原生 UI 层 网络层 数据层的分层结构。本节以“获取热门景点列表”为例展示从 API 请求到 UI 渲染的完整链路。3.1.1 添加网络与序列化依赖在app/build.gradle的dependencies块中添加// 网络请求 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 // UI 列表 implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.cardview:cardview:1.0.0 // 权限与生命周期感知 implementation androidx.lifecycle:lifecycle-viewmodel:2.6.2 implementation androidx.lifecycle:lifecycle-livedata:2.6.2同步点击右上角Sync Now。若提示Failed to resolve检查是否启用了mavenCentral()仓库settings.gradle中需包含mavenCentral()。3.1.2 定义景点数据模型与 API 接口创建data/model/ScenicSpot.ktdata class ScenicSpot( val id: Int, val name: String, val city: String, val description: String, val rating: Float, val imageUrl: String )创建data/api/TravelApiService.ktimport retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET interface TravelApiService { GET(spots) suspend fun getScenicSpots(): ListScenicSpot companion object { private const val BASE_URL https://api.example-travel.com/v1/ // ← 替换为你的后端地址 fun create(): TravelApiService { return Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() .create(TravelApiService::class.java) } } }逻辑说明suspend函数配合CoroutineScope使用避免在主线程执行网络请求GsonConverterFactory自动将 JSON 字段映射到 Kotlin 数据类属性BASE_URL必须以/结尾否则GET(spots)会拼接成.../v1spots导致 404。3.1.3 构建 RecyclerView Adapter 与 Item 布局创建ui/spot/SpotAdapter.ktclass SpotAdapter : RecyclerView.AdapterSpotAdapter.ViewHolder() { private val spots mutableListOfScenicSpot() inner class ViewHolder(val binding: ItemSpotBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(spot: ScenicSpot) { binding.tvName.text spot.name binding.tvCity.text spot.city binding.tvRating.text ⭐ ${spot.rating} Glide.with(binding.root.context) .load(spot.imageUrl) .placeholder(R.drawable.ic_placeholder) .into(binding.ivCover) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding ItemSpotBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(spots[position]) } override fun getItemCount() spots.size fun submitList(newList: ListScenicSpot) { spots.clear() spots.addAll(newList) notifyDataSetChanged() } }对应res/layout/item_spot.xml使用 ConstraintLayout 保证低版本兼容androidx.cardview.widget.CardView xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_margin8dp app:cardCornerRadius8dp app:cardElevation4dp androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:padding12dp ImageView android:idid/ivCover android:layout_width80dp android:layout_height80dp android:scaleTypecenterCrop app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent / TextView android:idid/tvName android:layout_width0dp android:layout_heightwrap_content android:layout_marginStart12dp android:textSize16sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toEndOfid/ivCover app:layout_constraintTop_toTopOfid/ivCover / TextView android:idid/tvCity android:layout_width0dp android:layout_heightwrap_content android:layout_marginStart12dp android:textSize14sp android:textColor#666 app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toEndOfid/ivCover app:layout_constraintTop_toBottomOfid/tvName / TextView android:idid/tvRating android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginTop4dp android:textSize12sp app:layout_constraintStart_toStartOfid/tvCity app:layout_constraintTop_toBottomOfid/tvCity / /androidx.constraintlayout.widget.ConstraintLayout /androidx.cardview.widget.CardView参数说明CardView提供阴影与圆角避免在低端机上使用elevation导致渲染异常ConstraintLayout替代嵌套LinearLayout减少 Measure 时间Glide加载图片时必须传入binding.root.context而非activity.context防止内存泄漏。4. 处理 Android 12 权限与后台限制让定位与通知在真机上稳定工作4.1 为什么“申请了 ACCESS_FINE_LOCATION 却拿不到坐标”—— 近似位置权限的强制切换Android 12API 31起系统引入近似位置权限ACCESS_COARSE_LOCATION。当用户授予此权限后APP 只能获取城市级精度如“北京市朝阳区”而非 GPS 级别如“东经116.48°北纬39.99°”。毕业设计若需在地图上精准标注景点必须引导用户升级为精确权限。4.1.1 在 AndroidManifest.xml 中声明全部必要权限uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /注意READ_EXTERNAL_STORAGE与WRITE_EXTERNAL_STORAGE在 Android 10 已被Scoped Storage机制弱化但为兼容旧设备及文件分享功能仍需声明。4.1.2 实现动态权限申请与降级回退逻辑在MainActivity.kt中添加private fun requestLocationPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { // 用户曾拒绝需解释用途 AlertDialog.Builder(this) .setTitle(需要位置权限) .setMessage(开启精确位置权限才能为您推荐附近景点并显示地图坐标) .setPositiveButton(去设置) { _, _ - Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data Uri.parse(package:$packageName) startActivity(this) } } .setNegativeButton(取消, null) .show() } else { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_CODE_LOCATION ) } } else { startLocationService() // 权限已获直接启动定位 } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_CODE_LOCATION) { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { startLocationService() } else { // 用户永久拒绝尝试申请近似权限作为降级方案 if (!shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_CODE_COARSE_LOCATION ) } } } }提示shouldShowRequestPermissionRationale返回false表示用户勾选了“不再询问”此时必须跳转至应用设置页无法再次弹窗。这是 Android 的强制设计绕过即违反 Play Store 政策。4.1.3 解决 Android 12 后台定位被系统杀死的问题华为、小米等厂商系统默认限制 APP 后台活动。若你的 APP 在后台持续获取位置如导航中需在AndroidManifest.xml中为定位 Service 声明前台服务service android:name.service.LocationTrackingService android:enabledtrue android:exportedfalse android:foregroundServiceTypelocation /并在 Service 启动时调用if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { startForegroundService(intent) } else { startService(intent) }同时在onStartCommand中立即调用startForeground(NOTIFICATION_ID, notification)否则系统会在 5 秒内杀死服务。5. 真机调试与 APK 发布绕过签名、V2 签名、安装包校验三重关卡5.1 为什么“生成的 APK 在小米手机上安装失败”—— V2/V3 签名与平台白名单机制Android 7.0 强制启用 APK Signature Scheme v2而华为/小米等厂商还额外校验 v3 签名。若你使用 Android Studio 默认的 debug keystore 生成 APK其签名算法SHA-256可能被某些定制 ROM 拒绝。毕业答辩需确保 APK 在导师的任意品牌手机上可安装。5.1.1 创建符合全平台兼容的签名密钥在 Terminal 中执行keytool -genkeypair -v -keystore my-release-key.jks -alias travelapp-key -keyalg RSA -keysize 2048 -validity 10000 -storepass mypassword -keypass mypassword参数说明-keystore: 输出密钥库文件路径建议放在项目根目录外避免误提交-alias: 别名必须与后续 Gradle 配置一致-keyalg RSA: 使用 RSA 算法EC 算法在部分旧设备不兼容-keysize 2048: 密钥长度2048 是当前安全与兼容性平衡值-validity 10000: 有效期 10000 天约 27 年覆盖毕设周期5.1.2 在 Gradle 中配置签名并启用 V2/V3 签名在app/build.gradle的android块内添加signingConfigs { release { storeFile file(../my-release-key.jks) // ← 路径需相对于 project root storePassword mypassword keyAlias travelapp-key keyPassword mypassword } } buildTypes { release { signingConfig signingConfigs.release // 启用 V2/V3 签名 packagingOptions { signers { signerConfig { v1SigningEnabled true v2SigningEnabled true v3SigningEnabled true } } } // 移除调试信息减小体积 minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } }注意storeFile路径必须是相对路径且my-release-key.jks不得放入app/目录下否则会被 Git 跟踪。建议将其放在项目同级目录并在.gitignore中添加my-release-key.jks。5.1.3 生成可安装 APK 并验证签名完整性点击Build → Generate Signed Bundle / APK→ 选择APK→ Next → 选择刚创建的my-release-key.jks→ 输入密码 → 选择releasebuild type → Finish。生成的 APK 位于app/release/app-release.apk。在命令行验证签名apksigner verify --verbose app-release.apk成功输出应包含Verified using v1 scheme (JAR signing): true Verified using v2 scheme (APK Signature Scheme v2): true Verified using v3 scheme (APK Signature Scheme v3): true若任一为false说明签名未生效需检查build.gradle中packagingOptions是否正确配置。5.1.4 解决“安装包损坏”提示的终极排查清单现象原因解决方案小米手机提示“解析包时出现问题”APK 未启用 V2 签名确认v2SigningEnabled true且apksigner verify输出为true华为手机提示“应用未通过安全检测”应用未在华为 AppGallery 上架毕设场景下进入“设置 → 安全 → 更多安全设置 → 安装外部来源应用”开启对应 APP 的权限安装后图标不显示AndroidManifest.xml中application缺少android:icon属性添加android:iconmipmap/ic_launcher启动黑屏 3 秒后崩溃minSdkVersion低于手机系统版本检查build.gradle中minSdk 21确保目标手机 Android 版本 ≥ 5.0最后一步将app-release.apk通过微信/QQ 发送给导师或用 USB 线直连手机执行adb install app-release.apk。只要adb logcat中无FATAL EXCEPTION且首页 Activity 正常渲染即达到毕设交付标准。本文还有配套的精品资源点击获取
返回列表