多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Android天气预报APP毕业设计:定位请求解析缓存全链路实战

Android天气预报APP毕业设计:定位请求解析缓存全链路实战 简介这是一套面向计算机相关专业学生的Android毕业设计完整项目基于AndroidStudio开发的天气预报APP附带详细文档说明适合正在准备毕设、课程设计或期末大作业的学习者参考使用。项目经导师指导并获评审98分认可源码均经本地编译与严格调试可稳定运行难度适中兼顾学习与实战需求。资源包共666个文件约23.93MB涵盖108个java源文件、188个xml布局与配置、167个class编译文件、98个png图片资源以及jar、gradle、aidl、aar等依赖与构建文件并包含apk安装包与数据库文件结构完整便于按模块研读。目前已有185人学习关注。通过该资源读者可获得一套可直接运行的天气APP完整方案理解界面布局、数据请求与本地存储等核心实现同时借助文档说明梳理项目结构与开发思路为毕设答辩与项目实战提供有力参考。1. 天气预报 APP 的毕业设计真正拉开差距的是数据链路每年到了毕设选题季基于 Android Studio 的天气预报 APP 都是高频选项。原因很直接需求清晰、界面直观、答辩时老师一眼能看懂不像某些算法类题目讲十分钟还没进入正题。但问题也恰恰出在这里——十个人做天气预报八个人的界面长得差不多剩下两个连数据都是写死的。真正能拿到高分的从来不是谁的 UI 更花哨而是谁把「定位 → 请求 → 解析 → 缓存 → 展示」这条数据链路做扎实了。这篇笔记面向正在做这个题目的同学也面向想快速搭一个可演示天气模块的开发者。我会按真实开发顺序把项目结构、网络请求、数据解析、本地缓存、界面绑定和答辩前最容易翻车的地方讲清楚。你照着做能跑出一个联网可用、断网可看、定位可切换的完整 APP而不是一个只能截图交差的壳子。2. 先把工程骨架搭对Android Studio 项目结构与依赖选型很多人一上来就写 Activity写到一半发现权限没配、依赖冲突、模拟器没网然后开始到处搜报错。正确的顺序是先定架构再写代码。这一章解决的是「项目怎么搭、库怎么选、权限怎么配」的问题。2.1 用 MVVM 还是 MVC毕设场景下的取舍教科书上讲 MVC实际开发里 MVVM 更省心。原因在于天气 APP 的数据流是单向的定位拿到经纬度 → 请求接口 → 返回 JSON → 解析成实体 → 更新 UI。如果全塞进 Activity一个文件轻松超过八百行答辩时老师翻代码会直接问「你这 Activity 怎么这么臃肿」。我一般会这样分层ui包Activity、Fragment、Adapter只负责展示和用户交互data包网络请求、JSON 解析、本地缓存model包实体类比如WeatherResponse、DailyForecastutil包定位工具、时间格式化、网络状态判断这样分的好处是答辩时你可以指着目录说「这是数据层这是展示层」逻辑清晰老师印象分直接上去。不需要引入完整的 Jetpack 全家桶毕设体量用不上反而增加学习成本。2.2 依赖库怎么选Retrofit Gson OkHttp 的最小组合网络请求这块常见做法是 Retrofit 配 Gson底层用 OkHttp。这三个库配合成熟、文档多、出问题好搜。在app/build.gradle里加dependencies { // 网络请求核心 implementation com.squareup.retrofit2:retrofit:2.9.0 // Gson 转换器把 JSON 自动映射成实体类 implementation com.squareup.retrofit2:converter-gson:2.9.0 // OkHttp 日志拦截器调试时打印请求和响应 implementation com.squareup.okhttp3:logging-interceptor:4.9.3 // 定位用系统自带的即可不必引入第三方 implementation com.google.android.gms:play-services-location:21.0.1 }这里有个坑要提前说converter-gson的版本必须和retrofit主版本对应否则运行时会抛NoClassDefFoundError。我见过有人 retrofit 用 2.9.0converter 用 2.6.0编译能过一请求就崩查了半天。2.3 权限配置定位和网络一个都不能少在AndroidManifest.xml里声明权限!-- 网络权限请求天气接口必须 -- uses-permission android:nameandroid.permission.INTERNET / !-- 粗略定位用于获取城市 -- uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / !-- 精确定位精度更高但耗电 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 读取网络状态用于判断是否离线 -- uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /注意 Android 6.0 以后定位是危险权限必须在运行时动态申请只在 Manifest 里写是没用的。动态申请的逻辑放在首页 Activity 的onCreate里用户拒绝后要给一个「去设置」的引导否则定位永远拿不到值。2.4 接口选型为什么建议用聚合类天气 API天气数据源大致分两类一类是官方气象接口数据权威但接入门槛高、需要申请 key另一类是聚合类 API注册即用、返回 JSON 结构清晰适合毕设。选型时重点看三点是否免费、是否有逐小时预报、返回字段是否包含湿度风力等常用指标。我一般会选一个提供「实时天气 未来七天 逐小时」三类接口的服务这样界面能做出层次感。接口地址和 key 不要硬编码在 Java 文件里放到gradle.properties或单独的Constants类答辩时老师问「key 泄露怎么办」你能答上来。3. 网络请求与 JSON 解析从经纬度到天气实体的完整链路骨架搭好后核心工作就是打通数据链路。这一章是整篇笔记最厚的部分因为大部分 bug 都出在这里。3.1 定义实体类字段名对不上是解析失败的头号原因假设接口返回的 JSON 长这样{ location: {name: 某市, lat: 30.2, lon: 120.1}, now: {temp: 26, text: 多云, humidity: 65, windDir: 东南风}, daily: [ {date: 2024-06-01, tempMax: 30, tempMin: 22, textDay: 晴} ] }对应的实体类要这样写public class WeatherResponse { // 字段名必须和 JSON 的 key 完全一致大小写敏感 private Location location; private Now now; private ListDaily daily; // getter/setter 省略但必须有Gson 靠反射赋值 public static class Location { private String name; private double lat; private double lon; } public static class Now { private int temp; private String text; private int humidity; private String windDir; } public static class Daily { private String date; private int tempMax; private int tempMin; private String textDay; } }逻辑说明Gson 解析时按字段名匹配JSON 里叫tempMaxJava 字段就必须叫tempMax写成temp_max或maxTemp都会得到 null。如果接口字段名和 Java 命名规范冲突比如接口用下划线用SerializedName(temp_max)注解映射不要改字段名去迁就接口。参数说明int和double的选择看数据范围温度用 int 够用经纬度必须用 double用 float 会丢精度导致定位偏移。3.2 封装 Retrofit 客户端单例 拦截器 超时public class ApiClient { private static final String BASE_URL https://api.example.com/; private static Retrofit retrofit; public static Retrofit getInstance() { if (retrofit null) { // 日志拦截器只在 Debug 版本开启Release 要关掉 HttpLoggingInterceptor logging new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 超时设置天气接口响应快10 秒足够 OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .addInterceptor(logging) .build(); retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } }逻辑说明用双重检查锁保证单例避免每次请求都新建 OkHttpClient 导致连接池浪费。日志拦截器在调试时能直接看到请求 URL 和返回 JSON是排查解析问题的第一工具。参数说明connectTimeout是建立连接的超时readTimeout是等待响应的超时。两个都要设只设一个在弱网下仍会卡死。超时时间不要设太长否则用户等半天没反应会以为 APP 崩了。3.3 定义接口与发起请求public interface WeatherService { // GET 请求路径参数用 Path 占位 GET(weather) CallWeatherResponse getWeather( Query(lat) double lat, Query(lon) double lon, Query(key) String key ); }调用时WeatherService service ApiClient.getInstance().create(WeatherService.class); service.getWeather(lat, lon, Constants.API_KEY) .enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { if (response.isSuccessful() response.body() ! null) { // 主线程更新 UI updateUI(response.body()); } else { // HTTP 码非 200比如 401 key 失效、429 限流 showError(请求失败 response.code()); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { // 网络异常比如无网、DNS 失败 showError(网络异常 t.getMessage()); } });逻辑说明enqueue是异步请求回调默认在子线程更新 UI 必须切回主线程用runOnUiThread或 Handler。onResponse里先判断isSuccessful()再判断body()非空两层都要判否则接口返回空数据时会空指针。参数说明Query会把参数拼到 URL 后面Path是替换路径中的占位符。key 建议放在请求头而不是 URL避免日志里泄露但毕设用 Query 也能接受。3.4 定位获取经纬度LocationManager 的正确用法LocationManager lm (LocationManager) getSystemService(Context.LOCATION_SERVICE); // 先检查权限没授权直接返回 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { requestPermission(); return; } // 用 GPS 和网络双通道室内 GPS 弱时靠网络兜底 lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, new LocationListener() { Override public void onLocationChanged(Location location) { double lat location.getLatitude(); double lon location.getLongitude(); // 拿到坐标后立刻请求天气 loadWeather(lat, lon); } // 其余回调省略 });逻辑说明定位是异步的不能拿到 LocationManager 就以为有坐标了必须等onLocationChanged回调。很多新手在这里翻车——直接在onCreate里调getLastKnownLocation模拟器上返回 null界面一直空白。参数说明requestLocationUpdates的第二个参数是最小更新时间毫秒第三个是最小更新距离米。都设 0 表示实时更新但耗电高。毕设场景设 60000 和 100 就够避免频繁回调。4. 本地缓存与离线展示断网时 APP 不能是白板答辩现场网络往往不稳定如果断网就白屏老师会直接质疑可用性。缓存不只是加分项是保命项。4.1 用 SharedPreferences 存最近一次天气 JSON最轻量的方案是把整个响应 JSON 字符串存下来下次启动先读缓存渲染再发请求刷新。// 存 SharedPreferences sp getSharedPreferences(weather_cache, MODE_PRIVATE); sp.edit().putString(last_json, new Gson().toJson(response)).apply(); // 读 String cached sp.getString(last_json, null); if (cached ! null) { WeatherResponse data new Gson().fromJson(cached, WeatherResponse.class); updateUI(data); // 先用旧数据渲染 }逻辑说明存的是序列化后的 JSON 字符串读的时候反序列化回实体。这样缓存和网络返回的数据结构完全一致UI 更新逻辑可以复用不用写两套。参数说明apply()是异步写入commit()是同步写入。缓存场景用apply()即可不阻塞主线程。SharedPreferences 单个 value 不宜超过 100KB天气 JSON 通常几 KB完全够用。4.2 加时间戳判断缓存是否过期只存数据不存时间用户可能看到三天前的天气。加一个时间戳long cacheTime sp.getLong(cache_time, 0); long now System.currentTimeMillis(); // 超过 30 分钟就算过期但仍先展示再后台刷新 boolean expired (now - cacheTime) 30 * 60 * 1000; if (expired) { // 触发网络请求成功后覆盖缓存 loadWeatherFromNet(); }逻辑说明过期不代表不能用而是「先展示旧的再静默刷新」。这样用户打开 APP 立刻有内容不会盯着 loading 转圈。参数说明30 分钟是天气类 APP 的常见刷新间隔太短浪费流量太长数据不准。可以做成常量方便调整。4.3 网络状态判断避免无网时傻等超时ConnectivityManager cm (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info cm.getActiveNetworkInfo(); boolean online (info ! null info.isConnected()); if (!online) { // 直接读缓存不发请求 loadFromCache(); Toast.makeText(this, 当前无网络展示缓存数据, Toast.LENGTH_SHORT).show(); }逻辑说明先判断网络再决定走网络还是走缓存能避免无网时请求卡 10 秒超时用户体验差别很大。参数说明getActiveNetworkInfo在 Android 10 以后被标记废弃但毕设兼容低版本仍可用。新项目建议用NetworkCapabilities这里不展开。5. 避坑与排查那些让答辩当场卡壳的问题这一章是我带过几届学生后总结的高频翻车点每条都按「现象 → 原因 → 解决」写照着排查能省大量时间。5.1 模拟器能联网真机请求失败现象在 Android Studio 自带模拟器上天气正常显示装到真机上一直提示网络异常。原因模拟器走的是宿主机网络真机走的是移动数据或 WiFi如果接口是 HTTP 而非 HTTPSAndroid 9 以后默认禁止明文流量。解决在AndroidManifest.xml的application标签加android:usesCleartextTraffictrue或者把接口换成 HTTPS。生产环境必须用 HTTPS毕设如果接口只支持 HTTP加这个属性即可。5.2 定位一直返回 null现象权限也申请了代码也写了onLocationChanged就是不回调。原因三种可能——室内 GPS 信号弱、模拟器没设置虚拟位置、权限被用户拒绝后没重新申请。解决模拟器在 Extended Controls 里手动设置经纬度真机到窗边测试权限拒绝后引导用户去系统设置页手动开启。另外检查是否在onCreate里就调用了getLastKnownLocation这个方法在冷启动时经常返回 null必须配合requestLocationUpdates使用。5.3 JSON 解析后字段全是 null现象请求成功response.body()不为 null但里面的温度、城市名都是 null。原因字段名不匹配。接口返回temp实体类写temperature或者接口返回嵌套对象实体类写成了平铺。解决打开日志拦截器把返回的 JSON 完整打印出来逐字段对照实体类。嵌套结构必须用内部类对应不能拍平。命名不一致的用SerializedName注解。5.4 界面更新时崩溃报 CalledFromWrongThreadException现象请求成功后调textView.setText()直接崩。原因Retrofit 的onResponse回调在子线程Android 不允许子线程操作 UI。解决用runOnUiThread(() - updateUI(data))包一层或者用 Handler 切主线程。如果用了 LiveData 或 RxJava注意 observe 的线程切换。5.5 打包 release 版本后接口报 401现象debug 版本正常签名打包后请求全部失败。原因代码里用了BuildConfig.DEBUG判断release 下走了不同的 key 或 URL或者混淆规则把实体类字段名改了导致 Gson 解析失败。解决检查build.gradle的buildTypes配置确认 release 的 key 正确。在proguard-rules.pro里加-keep class com.xxx.model.** { *; }保留实体类不被混淆。6. 让界面和数据对得上Adapter 绑定与刷新时机的一个技巧最后一章讲一个具体技巧也是我踩过坑之后固定下来的习惯列表类天气数据比如未来七天用 RecyclerView 绑定时刷新时机比绑定逻辑更容易出问题。常见写法是在请求成功后直接adapter.notifyDataSetChanged()。这在数据量小的时候没问题但如果用户快速切换城市两次请求的回调顺序可能颠倒——先发的请求后返回后发的请求先返回结果界面显示的是旧城市的数据。这个 bug 在答辩演示时如果被触发非常尴尬。我的做法是给每次请求打一个自增的 requestId回调时比对当前最新的 requestId不一致就丢弃private int latestRequestId 0; private void loadWeather(double lat, double lon) { final int currentId latestRequestId; service.getWeather(lat, lon, Constants.API_KEY) .enqueue(new CallbackWeatherResponse() { Override public void onResponse(CallWeatherResponse call, ResponseWeatherResponse response) { // 关键只处理最新一次请求的结果 if (currentId ! latestRequestId) return; if (response.isSuccessful() response.body() ! null) { runOnUiThread(() - { adapter.updateData(response.body().getDaily()); adapter.notifyDataSetChanged(); }); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { if (currentId ! latestRequestId) return; runOnUiThread(() - showError(t.getMessage())); } }); }逻辑说明latestRequestId每次请求前自增回调里比对只有最后一次请求的结果会被渲染。这样即使用户连续切换城市界面最终显示的也是最后一次选择的数据不会出现「切到 B 城市却显示 A 城市天气」的错乱。参数说明currentId必须是 final 或 effectively final因为要在匿名内部类里引用。adapter.updateData里先清空旧列表再添加新数据避免数据叠加。另外一个小习惯RecyclerView 的 item 布局里温度、天气状况这些文本控件宽度设成wrap_content而不是固定 dp因为不同城市的天气描述长度不一样「多云」和「雷阵雨转中到大雨」差很多固定宽度会截断。这个细节答辩时老师不一定问但界面看起来舒服印象分是实打实的。做这个题目最大的体会是天气预报 APP 的技术难点不在界面而在数据链路的稳定性。把定位、请求、解析、缓存、刷新这五步都考虑到异常情况代码量不大但经得起断网、切城市、快速点击这些真实操作的考验。我现在的习惯是每写完一个数据模块先手动断网跑一遍再连续切换三次城市能扛住这两下基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
返回列表