简介一份面向Android开发初学者的完整课设项目源码基于Android Studio实现天气查询App。功能上对接中国天气网API通过HTTP从网络获取实时天气信息并利用JSON解析得到气温、天气现象等数据后展示到界面同时使用数据库保存城市及其对应URL支持常用城市收藏与一键切换城市天气。项目覆盖网络请求、数据解析、SQLite本地存储、界面刷新等Android核心知识点适合作为课程设计或课堂实训的参考实现。压缩包共107个文件、约6.75MB主要文件包括15个Java源文件、24个XML布局与配置、53张PNG图片以及Gradle构建脚本和pinyin4j依赖包下载后导入开发环境即可运行。目前已有532人学习/下载。源码结构完整清晰实体、适配器、工具类分工明确能够帮助理解从城市选择到天气展示的完整流程拿到后可对照需求快速部署到模拟器或真机在演示与调试中加深对Android网络编程的理解也可进一步扩展多日预报、天气图表或个性化主题是Android入门阶段很实用的练手素材。1. 天气预报 App 的课设为什么不能直接下载源码交差课设题目发下来很多人的第一反应是找一个现成源码包解压导入 Android Studio。但天气预报这个题恰恰是最容易翻车的你拖下来的工程多半用的是过时写法打开先遇到 Gradle Sync 失败、JDK 版本不对、minSdk 和 buildTools 对不上跑通了又发现作者留的天气 API Key 早被限额界面永远显示加载失败。更致命的是答辩环节老师会问网络层为什么这么写、旋转屏幕为什么数据还在、Android 9 以上为什么不允许明文 HTTP——答不上来做得再像也拿不到高分。这篇按我辅导课设的固定路线来写先定数据源和网络层选型再在 Android Studio 里把请求、解析、渲染走通然后补动态权限、生命周期、状态保全这几个必考点最后落到签名打包和源码交付规范。结构按「能直接用」的标准展开代码可以抄参数按注释改坑按排查表对号入座。2. 在 Android Studio 里先定数据源与网络层选型做对后面少改代码课设不是商用项目老师看的是功能完整度和代码可解释性。所以天气数据源和网络库的选择原则很简单免费能用、文档全、字段够、答辩讲得清。选型这一步花半小时后面能省两天的返工。2.1 免费天气 API 怎么选额度、字段与稳定性对比市面上能用的天气数据源不少但很多免费套餐一天只有几百次调用演示时刷新两三次就触发频率限制当场翻车。我一般只推荐下面这几个都是开发者注册后直接能拿到 Key 的公开接口数据源免费额度请求方式课设友好度备注和风天气个人开发版每天约 1000 次HTTPS GET高v7 接口文档字段命名直观附示例 JSON高德开放平台每日限额按实名项目分配HTTPS GET中还能顺便用逆地理编码但主链路偏重OpenWeatherMap每分钟约 60 次HTTPS GET高文档全但中文城市名需自建映射表聚合数据按套餐计次HTTPS GET中部分接口要审核响应链路过长判断接口能不能直接用作课设重点看三点返回 JSON 里有没有now.temp和now.text这种「当前温度 天气现象」字段免费额度是每日总次数还是每分钟频率上限后者在连续下拉刷新时最容易触发官方文档有没有给出示例 JSON 和字段说明表这决定你写实体类时能不能把每个字段讲出处。和风天气有一个其他平台没有的细节天气查询接口的location参数要传 LocationID 或经纬度直接传中文城市名会返回 404。正确流程是用它的 GeoAPI 做「城市名 → LocationID」转换这一步建议单独封装一层后面切城市只用拿 ID。2.2 网络层选 Retrofit OkHttp 还是原生 HttpURLConnection早期课设模板最常见的写法是HttpURLConnection加子线程加runOnUiThread手动切主线程代码冗长还容易在主线程直接发请求踩NetworkOnMainThreadException。更务实的方案是 Retrofit 加 OkHttp 加 GsonConverterFactory理由有三条第一接口定义就是文档。答辩时指着GET注解和Query参数就能把「请求路径、参数名、返回类型」讲清楚不用翻几十行JSONObject解析代码。第二enqueue的回调在 Android 上默认抛回主线程不需要自己维护 Handler 消息循环。第三JSON 到实体类是自动映射手写解析省下来的是课设赶工最缺的时间。有同学担心用第三方库会被老师质疑不是自己写的这个顾虑多余。搭建网络框架只是工程实践你的业务代码、字段映射、UI 更新逻辑仍然在项目里。反过来如果你坚持手写网络层反而要额外解释「为什么不用业界主流封装」属于给自己添堵。真要用原生方案至少要封装线程池和回调接口那工作量比用 Retrofit 大多了。2.3 Gradle 依赖清单与版本匹配在模块的build.gradle里加上以下依赖版本是一组经过大量项目验证的稳定组合android { compileSdk 34 defaultConfig { minSdk 23 targetSdk 34 } } dependencies { implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.12.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 implementation com.google.code.gson:gson:2.10.1 implementation androidx.recyclerview:recyclerview:1.3.2 implementation androidx.swiperefreshlayout:swiperefreshlayout:1.1.0 }参数说明compileSdk 34对应 Android 14搭配 JDK 17 和 Android Gradle Plugin 8.1 以上minSdk 23表示只服务 Android 6.0 以上设备这样动态权限的逻辑只有一套路径targetSdk 34决定系统行为兼容模式影响后续要讲的明文流量策略。converter-gson会传递引入 Gson显式再加gson:2.10.1是为了锁版本避免和 AndroidX 内部传递依赖产生冲突。注意不要盲目追最新版依赖。Retrofit 2.9.0、OkHttp 4.12.0 这套组合兼容性最稳网上能搜到的报错解决方案也最多。版本越新可检索的排错案例越少课设周期没时间踩这个坑。3. 用 Android Studio 打通天气接口从 Gradle 依赖到数据上屏选型定完进入编码。这一章按照「常量封装 → 接口定义 → 实体映射 → 发请求渲染」的顺序推进每一步的代码都是完整可运行的。3.1 申请 API Key 并把常量集中封装在和风天气控制台注册后创建项目会拿到一串形如xxxxxxxxxxxxxxxx的 API Key。拿到后不要直接散落在 Activity 里统一放一个常量类public class ApiConstants { // 天气数据接口域名 public static final String WEATHER_BASE_URL https://devapi.qweather.com/; // 城市搜索接口域名 public static final String GEO_BASE_URL https://geoapi.qweather.com/; // 你的 API Key演示前替换 public static final String API_KEY 这里填你的Key; // 默认城市北京 public static final String DEFAULT_LOCATION_ID 101010100; }参数说明WEATHER_BASE_URL和GEO_BASE_URL是两个不同域名所以要给 Retrofit 建两个实例DEFAULT_LOCATION_ID是北京的和风天气城市 ID在没有定位权限的模拟器上能兜底显示数据。这里有个交付细节提交源码包之前把API_KEY换回占位符否则拿到你源码的人会持续消耗你账户的每日额度。3.2 定义 Retrofit 接口和 JSON 实体类网络层分两个接口一个查城市、一个查天气public interface WeatherApiService { // 实时天气 GET(v7/weather/now) CallWeatherResponse getNowWeather( Query(location) String locationId, Query(key) String apiKey ); // 城市搜索入参是城市名 GET(v2/city/lookup) CallCityResponse lookupCity( Query(location) String cityName, Query(key) String apiKey ); }逻辑说明两个方法共用同一个接口但 Retrofit 的 baseUrl 决定了实际访问的域名所以要区分开就是建两个不同 baseUrl 的 Retrofit 实例。Query注解的作用是把参数拼到 URL 后面Retrofit 会自动做 URL 编码中文城市名不会乱码。对应实体类字段名严格对照接口返回的 JSONpublic class WeatherResponse { private String code; // 200 表示业务成功 private String updateTime; // 天气数据的服务端更新时间 private Now now; public String getCode() { return code; } public Now getNow() { return now; } public static class Now { private String temp; // 当前温度单位℃ private String feelsLike; // 体感温度 private String text; // 天气现象描述如多云 private String windDir; // 风向 private String windScale; // 风力等级 private String humidity; // 相对湿度 private String vis; // 能见度 public String getTemp() { return temp; } public String getText() { return text; } // 其余 getter 省略 } }这里有个关键点Gson 是按字段名反射填充的实体类字段名必须和返回 JSON 的 key 完全一致。code字段要当成业务状态码判断不要只看 HTTP 状态码——即使 HTTP 200code不是 200 也要走错误分支。3.3 发起请求并把数据渲染到界面请求调用封装成方法在 MainActivity 里调用private void loadWeather(String locationId) { WeatherApiService service RetrofitClient.getInstance().create(WeatherApiService.class); service.getNowWeather(locationId, ApiConstants.API_KEY) .enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { WeatherResponse body response.body(); if (body null || !200.equals(body.getCode())) { Toast.makeText(MainActivity.this, 数据异常, Toast.LENGTH_SHORT).show(); return; } WeatherResponse.Now now body.getNow(); if (now null) { Toast.makeText(MainActivity.this, 字段缺失, Toast.LENGTH_SHORT).show(); return; } TextView tvTemp findViewById(R.id.tv_temp); TextView tvText findViewById(R.id.tv_text); tvTemp.setText(now.getTemp() ℃); tvText.setText(now.getText()); } Override public void onFailure(CallWeatherResponse call, Throwable t) { Toast.makeText(MainActivity.this, 网络失败 t.getMessage(), Toast.LENGTH_SHORT).show(); } }); }提示Retrofit 2 在 Android 上的onResponse和onFailure默认已经回到了主线程不要再套一层runOnUiThread或Handler.post重复切换是很多自己封装网络层的同学常犯的冗余代码问题。城市切换的逻辑是把lookupCity返回结果里的id字段拿出来再传给loadWeather。七日预报同样简单把GET(v7/weather/7d)加到WeatherApiService里返回类型改成Weather7dResponse内部用一个ListDaily接收daily数组然后绑定到 RecyclerView 即可适配器写法与普通列表没有区别。4. 动态权限、生命周期与源码移植课设源码里最容易扣分的三个点功能跑通只能算及格分。天气 App 课设里老师最喜欢问的正是这章讲的三个点定位权限怎么动态申请、旋转屏幕数据为什么还在、别人拿到源码能不能直接编译。这三块每块都能做成加分项。4.1 动态定位权限与检查回调. 不用配图这里直接用代码讲。先写权限检查和请求private static final int REQUEST_LOCATION_CODE 1001; private void checkLocationPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { // 弹系统对话框申请定位权限 ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_LOCATION_CODE); } else { startLocation(); } } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQUEST_LOCATION_CODE grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { startLocation(); } else { // 用户拒绝了也不能崩退回默认城市 loadWeather(ApiConstants.DEFAULT_LOCATION_ID); } }说明checkSelfPermission用于判断当前是否已授权requestPermissions触发系统弹窗onRequestPermissionsResult是唯一的回传结果入口。要点在于拒绝分支必须兜底直接回退到默认城市而不是弹 Toast 干等。定位用系统的LocationManager的getLastKnownLocation即可把返回的经纬度拼成经度,纬度传给天气接口和风天气支持这种坐标格式。模拟器上定位经常拿不到所以默认城市兜底在演示时是保命设计。4.2 旋转屏幕不丢数据的 ViewModel 方案没有做任何处理的时候旋转屏幕 Activity 会销毁重建重新发起一次网络请求不仅浪费流量还有可能在回调回来时 Activity 已经销毁直接空指针。常见错误解法是给 Activity 加android:configChangesorientation|screenSize这是掩耳盗铃API 33 以上部分行为已经被系统忽略答辩也讲不通。用 ViewModel 是最标准的做法public class WeatherViewModel extends ViewModel { private final MutableLiveDataWeatherResponse.Now weatherData new MutableLiveData(); public LiveDataWeatherResponse.Now getWeatherData() { return weatherData; } public void fetchWeather(String locationId) { // 在这里发起 Retrofit 请求onResponse 里执行 weatherData.setValue(now) } }在 Activity 里通过new ViewModelProvider(this).get(WeatherViewModel.class)获取实例。旋转后 Activity 重建但 ViewModel 不会销毁weatherData里的值还在UI 层observe到已有值会立即重新渲染不再重复请求。这是 Jetpack 组件的标准用法老师问生命周期问题时能讲出「ViewModel 存活范围跟随 ViewModelStoreOwner与 configuration change 解耦」这一句基本就是满分答法。4.3 源码工程移植与构建报错排查表拿到别人的 Android Studio 工程最常见的「双击打开报错、Sync 失败、Build 失败」可以按这张表排查报错现象常见原因处理方式打开工程后 SDK Location 找不到对方把local.properties一起打包了里面是对方电脑的 SDK 路径删除local.properties自己创建并填入本机 SDK 路径Gradle Sync 一直失败、插件下载超时gradle wrapper 版本与网络环境不匹配在gradle-wrapper.properties里改成已经下载过的版本tag number over 30 is not supportedJDK 版本高于 AGP 支持的范围D8 无法识别高版本字节码常量池把 JDK 切回 17或升级 AGP 到 8.x 以上SDK 组件勾选置灰无法安装SDK 目录没有写权限或代理下载失败修改 SDK 目录权限或换成已下载的 build-tools 版本Cleartext HTTP traffic not permittedtargetSdk 28 开始默认禁止明文 HTTP全部改用 HTTPS 请求QWeather 本身就是 HTTPS不涉及这个问题运行起来全部数据为空白但无异常对方把 API Key 删了请求返回业务错误码换成自己的 Key并检查code字段打印到日志表格里第一条最容易被忽略Git 仓库或 zip 包里带local.properties和.idea目录是源码包交付时的大忌。别人的电脑打开会报错还显得项目不专业。自查命令是看压缩包里有没有app/build、~/.gradle相关路径泄露。5. 源码打包与答辩演示把 Android Studio 项目变成可交付的课设作品功能完整、权限正确之后最后一步是签名打包和整理交付物。这一章讲三个直接落地的操作生成签名包、整理源码 zip、准备演示脚本。5.1 制作签名 Release 包开发调试用的是 debug 签名课设最好交一个正式的 release 包。先用命令行生成签名文件keytool -genkeypair -v -keystore weather.jks -keyalg RSA -keysize 2048 -validity 10000 -alias weather参数说明-keystore指定签名文件输出路径和名字-keyalg RSA -keysize 2048是当前兼容性最好的密钥组合-validity 10000是有效天数约 27 年课设足够-alias weather是密钥别名打包时要用到。执行过程中会要求输入密钥库密码和姓名组织等信息记住密码后面两个地方要用。然后在模块build.gradle里配置签名android { signingConfigs { release { storeFile file(weather.jks) storePassword 你的密码 keyAlias weather keyPassword 你的密码 } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt) } } }最后在 Android Studio 右侧 Gradle 面板执行app - Tasks - build - assembleRelease产物在app/build/outputs/apk/release/。minifyEnabled false是课设阶段的正确选择混淆规则没配好会导致 Retrofit 实体类被删除线上加载直接闪退。5.2 源码 zip 的目录规范与答辩演示脚本zip 打包前对照清理三样东西删掉根目录的local.properties、~/.gradle相关缓存、.idea目录确认API_KEY已替换为占位符在项目根目录补一份README.md写清楚三块内容——运行环境JDK 版本、AGP 版本、compileSdk、Key 申请步骤、功能清单和测试账号。README 存在本身就是一个满分级文档意识。答辩演示按三幕准备第一幕冷启动展示加载动画到数据出现的过程把 Logcat 里code200的日志停住给老师看第二幕断网打开飞行模式再刷新演示 onFailure 的 Toast 兜底第三幕切城市从搜索框输入广州展示lookupCity拿到 LocationID 后天气列表联动刷新。三幕对应网络成功的正常链路、失败的异常链路、业务状态码的判断链路基本覆盖老师能问的运行时问题。最后一个小技巧把 Key 放在ApiConstants类里而不要写进 git 历史可追溯的提交中模拟器冷启动比热启动更能展示真实加载演示前先adb shell am force-stop com.example.weather结束一次进程现场再点图标启动比界面已经缓存好数据更有说服力。本文还有配套的精品资源点击获取