ARTICLE DETAIL

资讯详情

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

Android天气预报App课设指南:Kotlin+Retrofit实现满分项目

Android天气预报App课设指南:Kotlin+Retrofit实现满分项目 简介这是一份基于Android Studio实现的天气预报App完整课设源码面向安卓初学者和需要完成课程设计的同学覆盖了从界面搭建到数据存储的典型移动开发流程。项目通过调用天气预报接口获取实时天气数据以ListView和GridView构建首页、城市管理、搜索更多城市、删除城市、更多设置五大界面并利用SQLiteDatabase完成查询记录与城市信息的持久化存储。代码注释详细结构清晰便于理解与二次开发。资源包共1207个文件约19.97MB其中包含java源码、xml布局、json配置、png图标等核心文件同时附带可直接安装的apk文件和gradle构建工程方便快速运行与调试。目前已有17025人学习下载无论是用于课设答辩还是系统学习Android网络请求、数据存储与列表适配器都具备较高的参考价值。1. 拿这个标题当课设先搞清楚要做什么天气类应用是 Android 课设里出现频率最高的题目之一但不是每个同学都清楚它为什么适合入门。原因在于它要求你在一个完整页面上同时处理用户输入、网络请求、数据解析和界面刷新恰好覆盖 Android 应用开发最核心的闭环。标题里的满分课设并不是为了吸引眼球只要把数据真正从天气服务端拉下来再解决掉权限、线程和版本匹配这几个隐藏问题这个项目就能达到课设演示的标准。这个教程适合刚学完 Kotlin 基础、正在为课程设计发愁的学生也适合想独立完成一个 Android 项目的转行开发者。下面的路线完全基于 Android Studio 默认工程不依赖额外模板。2. 天气预报App的架构选型API、数据格式和请求库怎么定2.1 课设场景下不建议自己写后端直接接天气 API很多同学喜欢在本地随机生成温度或湿度数据来模拟天气看起来页面是跑起来了但答辩时只要被问一句数据来源在哪整个架构就站不住。实际工程中天气数据是通过 HTTP 请求访问远端服务器获取的。课设阶段直接调用公开天气 API 是最省时间也最稳妥的方案后端服务、存储、运维都不需要自己负责只需要在客户端发起请求并解析返回值。目前国内网络环境下容易访问的天气服务有两类。国外服务如 OpenWeatherMap免费额度大约每分钟 60 次注册后就能用字段覆盖湿度、风速、温度等常见指标国内服务如和风天气访问延迟低需要先注册开发者账号再创建 Web API 密钥。选型的标准很简单能返回 JSON、支持城市名查询、免费额度足够现场演示。字段多少不影响后续开发可以按需裁剪。实际开发中我一般会先用 OpenWeatherMap 把流程跑通再视网络延迟情况切换到国内服务。一次典型的请求返回 JSON 结构如下这里以 OpenWeatherMap 的 Current Weather 接口为例{ weather: [ { main: Clear, description: clear sky, icon: 01d } ], main: { temp: 286.67, feels_like: 285.37, humidity: 72 }, name: Beijing }这个结构中有三个关键要素weather是数组里面有天气现象和对应图标main.temp默认以开尔文为单位必须使用unitsmetric请求参数才能得到摄氏度name返回的是服务端匹配到的城市名可能是英文也可能是拼音。很多同学解析后温度显示 290°C就是因为漏掉了units参数。接口返回的字段不会恰好等于 UI 需要的字段所以你需要在数据类里精确映射再做一层简单的转换。2.2 Retrofit 加 Gson 还是 OkHttp 加协程网络库的选择会直接影响代码量。Android 上最原始的写法是HttpURLConnection但处理流、编码和线程切换需要大量样板代码。我不推荐在课设里用这种写法可读性和可维护性都不够。比较成熟的方案有两类。第一类是 Retrofit Gson把 URL 拼接、请求参数、JSON 反序列化全部交给框架开发时只需要定义一个接口声明GET和Query。第二类是 OkHttp kotlinx.serialization 或手动解析 JSON灵活度更高但要多写不少代码。我一般会选 Retrofit Gson因为在课程设计这个时间节点你不需要关心连接池、缓存策略或者拦截器Retrofit 已经把这层封装好了。二者的取舍可以看这个对照表方案依赖复杂度代码量异常处理课设推荐度Retrofit Gson2~3 个依赖配置简单接口 数据类 调用处有 HttpException信息较明确高OkHttp 协程1 个依赖但需手写解析每次请求都要拿到 Response 再解析需要自己判断 response.code中HttpClient 协程0 个依赖但兼容性差模板代码多Java 标准库的异常不够直观低如果心态是只要能跑就行OkHttp 没有问题但如果要控制代码行数、让答辩时能清晰讲出来Retrofit 会省很多事。Retrofit 底层本来就是 OkHttp只是在外面帮你完成了接口代理和 JSON 转换。2.3 先把 Gradle 换成国内镜像不然依赖下载会卡住新建项目后第一步会触发 Gradle Sync绝大多数新手卡在这是默认仓库指向国外地址依赖下载慢或者直接超时。解决办法很简单把仓库镜像配好。在项目根目录的settings.gradle.kts里将pluginManagement.repositories和dependencyResolutionManagement.repositories调整为阿里云镜像或腾讯云镜像。常见的写法是pluginManagement { repositories { maven(https://maven.aliyun.com/repository/gradle-plugin) maven(https://maven.aliyun.com/repository/google) maven(https://maven.aliyun.com/repository/public) google() mavenCentral() gradlePluginPortal() } }注意仓库顺序镜像地址要写在前面这样 Gradle 会优先访问镜像如果google()在前面它仍然可能先连海外地址。gradle-plugin镜像存放 AGP 插件google镜像负责 AndroidX 和部分 Google 依赖public镜像包含 Maven Central 和 JCenter 的常用库。这样一个新项目通常在几分钟内完成同步。写到这里顺便提一个高频场景每次新建项目都要重复配一次镜像很烦。可以把这段配置存到一个自己的模板工程里之后File New Project Import Project直接复用。提示如果你把 API Key 放在local.properties记得检查.gitignore否则很容易随着代码仓库一起提交到远程。2.4 API Key 怎么放才不会被扣分最直接的写法是在 MainActivity 里写一个API_KEY字符串常量。这样能跑但在代码审查时很容易被提问Key 直接暴露了怎么办。更稳妥的做法是把 Key 放到gradle.properties中再在app/build.gradle.kts里读取并生成BuildConfig字段buildFeatures { buildConfig true } buildTypes { debug { buildConfigField(String, API_KEY, \${project.property(WEATHER_API_KEY)}\) } release { buildConfigField(String, API_KEY, \${project.property(WEATHER_API_KEY)}\) } }对应地gradle.properties里加一行WEATHER_API_KEY你的Key代码里通过BuildConfig.API_KEY引用即可。这个做法对课设来说属于加分项能让老师看到你有版本管理和配置分离的意识。3. 在 Android Studio 里从零搭出天气界面和请求链路3.1 创建项目并确定最低 SDK 版本打开 Android Studio 后选择New Project Empty Views Activity语言选 Kotlin。注意这里不是 Empty Activity因为 Empty Activity 模板在较新版本中默认使用 Compose而 Compose 对课设来说引入了额外概念。选择 Views Activity 会让你得到一个基于 XML 的布局文件后续复制代码进去最容易理解。最低 SDK 选多少分建议选 24。一方面覆盖了 Android 7.0 及以上版本绝大多数国内手机都满足另一方面API 24 开始很多运行时行为和通知接口比较统一不用在代码里写各种版本分支。目标 SDK 按 Android Studio 默认值即可不用刻意改低。项目创建完成后先做一件事确认 Gradle 文件里compileSdk、minSdk、targetSdk的值。很多问题都来自三个 SDK 版本和 AGP 不对齐具体现象会在第 4 章的 Gradle 构建错误里出现。3.2 声明网络权限并允许明文 HTTP 请求Android 网络请求需要在AndroidManifest.xml中加入权限声明。在application标签之前添加uses-permission android:nameandroid.permission.INTERNET /如果天气 API 的地址是http://而不是https://Android 9 及以上的系统会默认拒绝明文流量。你需要在application标签内加一个属性android:usesCleartextTraffictrue这里的INTERNET是安装时权限不需要像相机或定位那样在代码中动态申请。usesCleartextTraffic是应用的全局开关课设阶段这样设置没有任何问题。如果做正式产品建议在network_security_config.xml中只针对特定域名放开明文而不是全局允许。注意如果你调用的天气 API 始终是https就不要加这个属性保留系统的安全默认值。3.3 写一个能容纳所有天气信息的布局天气 App 的界面不需要花哨但信息层级要清楚顶部是城市输入框和查询按钮中部是城市名、温度和天气现象底部是湿度等附加信息。用ScrollView包住LinearLayout可以避免键盘弹出时内容被顶出屏幕。示例如下?xml version1.0 encodingutf-8? ScrollView xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding16dp EditText android:idid/et_city android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入城市名 android:imeOptionsactionSearch android:inputTypetext / Button android:idid/btn_search android:layout_widthmatch_parent android:layout_heightwrap_content android:text查询天气 / TextView android:idid/tv_city android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize24sp android:textStylebold / TextView android:idid/tv_temperature android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize48sp android:textStylebold / TextView android:idid/tv_weather android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize20sp / TextView android:idid/tv_humidity android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize16sp / /LinearLayout /ScrollView布局里四个TextView分别用不同的id后续 Kotlin 代码会直接按 id 找到控件。温度用48sp是为了视觉上突出信息量也可以用android:textColor把它换成深色或主题色。imeOptionsactionSearch可以让软键盘的确认键变成搜索用户点一下键盘上的搜索图标就能触发查询。这个细节值得加上体验会好不少。3.4 数据类、接口和请求代码串起来有了布局接着定义最终要显示的模型。在MainActivity.kt或其他文件中创建数据类data class WeatherResponse( val weather: ListWeatherInfo, val main: MainInfo, val name: String ) data class WeatherInfo( val main: String, val description: String, val icon: String ) data class MainInfo( val temp: Double, val humidity: Int )Gson会把响应的 JSON 字段反射到同名字段。如果数据类和 JSON 字段不完全一致例如 JSON 返回的是temp而数据类里拼写成了temperature那么反序列化后温度默认为0.0界面就会显示0.0°C。另外weather是数组即使只有一个元素也要用weather[0].description来取描述。然后定义 Retrofit 接口interface WeatherApi { GET(data/2.5/weather) suspend fun getWeather( Query(q) city: String, Query(appid) appId: String, Query(units) units: String metric ): WeatherResponse }suspend函数可以让它在协程中执行Query会自动拼接查询参数。把这几个参数设计成函数参数而不是硬编码是为了以后可以轻松支持城市切换和单位切换。在MainActivity中构建 Retrofit 实例并调用接口class MainActivity : AppCompatActivity() { private val api: WeatherApi by lazy { Retrofit.Builder() .baseUrl(https://api.openweathermap.org/) .addConverterFactory(GsonConverterFactory.create()) .build() .create(WeatherApi::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewByIdButton(R.id.btn_search).setOnClickListener { val city findViewByIdEditText(R.id.et_city).text.toString() if (city.isNotBlank()) { queryWeather(city) } else { Toast.makeText(this, 城市不能为空, Toast.LENGTH_SHORT).show() } } } private fun queryWeather(city: String) { lifecycleScope.launch { try { val result withContext(Dispatchers.IO) { api.getWeather(city, BuildConfig.API_KEY) } findViewByIdTextView(R.id.tv_city).text result.name findViewByIdTextView(R.id.tv_temperature).text ${result.main.temp} °C findViewByIdTextView(R.id.tv_weather).text result.weather[0].description findViewByIdTextView(R.id.tv_humidity).text 湿度${result.main.humidity}% } catch (e: Exception) { Toast.makeText(this, 查询失败${e.message}, Toast.LENGTH_SHORT).show() } } } }代码里用lifecycleScope.launch启动协程比GlobalScope安全。withContext(Dispatchers.IO)把网络请求切到后台线程避免阻塞 UI。请求完成后自动切回主线程更新控件。如果 API Key 写错或网络不通会进入catch分支弹出提示。上述代码中的BuildConfig.API_KEY需要提前在 gradle 里配置如果你跳过 2.4 节这里可以临时改成字符串字面量调试。3.5 打开 Logcat 看请求细节当页面显示查询失败时最快的定位方式不是加断点而是看日志。在onCreate中临时加一个 OkHttp 拦截器可以打印整个请求和响应val logging HttpLoggingInterceptor() logging.level HttpLoggingInterceptor.Level.BODY val client OkHttpClient.Builder().addInterceptor(logging).build() val retrofit Retrofit.Builder() .baseUrl(https://api.openweathermap.org/) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build()日志级别选择BODY会打印响应 JSON。在 Logcat 窗口右上角选择当前应用包名过滤关键字okhttp就能看到类似GET https://api.openweathermap.org/data/2.5/weather?q...的完整地址。如果返回 JSON 中cod字段是401就是 API Key 问题如果是404说明城市名没匹配上如果没有CLEARTEXT报错但一直没数据重点检查拦截器是否真的加载进去了。4. 课设最容易翻车的几个坑和排错方法4.1 SDK 无法勾选和模拟器下载慢在 Android Studio 的 SDK Manager 中偶尔会遇到某个 API Level 的install复选框是灰色不可勾选。这通常不是权限问题而是该平台的索引没有加载出来。常见的解决办法是重启 Android Studio 或点击Refresh如果仍然无效直接从命令行安装# 找到 SDK 目录下的 cmdline-tools sdkmanager platforms;android-34然后回到 Android Studio 的 SDK 路径设置确认Android SDK location指向同一个目录已安装的 Platform 会被自动识别。如果下载系统镜像很慢可以在 SDK Manager 的SDK Update Sites中把镜像站点加为更新源。启动模拟器时选择x86_64系统镜像比 arm64 在电脑上运行更流畅。还有一个小技巧如果模拟器启动后画面卡顿或渲染异常可以在AVD Manager里把Graphics改为Software虽然画面流畅度会下降但至少能稳定跑起来。4.2 Gradle 构建失败tag number over 30 is not supported这个报错出现在 Gradle 同步或执行 Task 时具体含义是解析 class 文件时遇到了不支持的常量池标记通常由 Java 版本和 AGP 版本错位引起。比如系统默认 JDK 17项目的 AGP 还是较老版本旧插件解析新编译的 Kotlin 字节码时就会报这种错。解决思路是让AGP - Gradle - JDK链条对齐。先检查当前使用的 JDKjava -version然后在File Settings Build Tools Gradle Gradle JDK中选择对应版本。AGP 8.x 通常需要 JDK 17 和 Gradle 8.xAGP 7.x 需要 JDK 11 或 17。如果项目是别人给的旧工程不要盲目升级 AGP否则会出现其他兼容性错误。也可以在gradle-wrapper.properties中调整 Gradle 发行版本让项目和本地 JDK 匹配。如果加过自定义插件比如lifecycle或hilt也需要同步检查其版本。动手改之前先看完整报错堆栈一般会提示是com.android.build.gradle还是某个依赖库的问题。4.3 连接真机调试小米手机连不上怎么办模拟器适合开发测试但课设答辩时用真机演示更稳。小米手机连接 Android Studio 有几个常见问题。首先是开发者选项没打开进入设置 我的设备 全部参数与信息连续点击MIUI 版本号7 次直到提示已进入开发者模式。然后进入设置 更多设置 开发者选项打开USB 调试和USB 安装开关。插入手机后通知栏通常会出现USB 用于的选项默认是仅充电需要改成传输文件。然后在电脑终端执行adb devices如果看到一串序列号后面跟着device说明连接成功如果是unauthorized检查手机屏幕上的授权弹窗点允许后重新拔插如果完全没列出设备可能数据线只支持充电换一根数据线试。很多连接不上的坑其实是数据线的问题。在 MIUI 上开发还会遇到USB 安装和USB 调试安全设置权限这两个开关尽量都打开。打开安全设置需要插入 SIM 卡并登录账号这是厂商限制课设阶段不一定需要若遇到按提示操作即可。4.4 请求返回成功但 UI 不更新最常见的现象是 Logcat 里已经打印出完整 JSON但界面没有任何变化。这时候先看更新 UI 的代码是否发生在主线程。协程中withContext(Dispatchers.IO)之外的代码依然在调用协程的上下文里如果在Dispatchers.IO里直接更新 TextView会在运行时抛出CalledFromWrongThreadException。另一个容易忽略的问题是调用接口后返回的数据类字段与 JSON 不匹配。比如服务端返回的是temp而数据类写的是temperatureGson 反序列化后temp为0.0UI 上自然显示0.0°C。这种情况不会崩溃也不会进 catch。排查时可以临时把响应字符串打印出来或用JSONObject校验字段名。还有一种情况是协程被页面销毁时取消。在 Activity 中直接用createCoroutineScope创建的协程如果不主动取消Activity 销毁后协程可能还在执行。而lifecycleScope会自动跟随生命周期取消所以在课设里不要图简单用GlobalScope改用lifecycleScope能省掉很多不可预知的闪退现场。5. 让课设拿高分的几个收尾技巧5.1 用 SwipeRefreshLayout 加下拉刷新把前面布局中的根节点ScrollView外层再包一层SwipeRefreshLayout并设置唯一的id。然后在代码里监听setOnRefreshListener重新请求刚才查过的城市。重要的是请求结束后无论成功还是失败都要调用refresh.isRefreshing false否则加载动画会一直转。可以把这个操作放在finally块中确保异常时也能恢复状态。5.2 用 SharedPreferences 保存最后一次数据在onResume里先读缓存val prefs getSharedPreferences(weather_cache, Context.MODE_PRIVATE) val cachedJson prefs.getString(last_json, null)如果存在就把缓存的 JSON 字符串直接还原成WeatherResponse显示出来再发起网络请求。没有网络时页面也不会是空白。保存时机选在请求成功之后保存原始 JSON 字符串而不是序列化对象兼容性最好。这样做代码量不大演示效果却不差属于典型的低成本加分项。5.3 提交前检查清单与 APK 打包课设提交前按下面列表逐项确认检查项操作API Key 是否正确先跑一次 debug 版本断网情况下是否崩溃打开飞行模式测试布局在 5 寸和 6.5 寸屏上是否正常创建两个模拟器缓存和刷新是否同时生效查询城市后下拉刷新打包用 Android Studio 的Build Generate Signed Bundle / APK如果只是交作业可以直接执行./gradlew assembleDebug生成的app-debug.apk在 app/build/outputs/apk/debug 目录下。这个包已经用 debug key 签名可以安装到手机上不需要自己生成 keystore。如果要用 release 包需要在Build Generate Signed Bundle / APK里创建签名文件课设阶段不做上架要求使用 debug 包就足够。本文还有配套的精品资源点击获取
返回列表