我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

AndroidStudio天气预报小程序实战:OkHttp请求、Gson解析与避坑指南

AndroidStudio天气预报小程序实战:OkHttp请求、Gson解析与避坑指南 简介基于Android Studio开发的天气预报小程序完整源码面向Android入门开发者与移动应用课程设计人员帮助理解如何借助第三方天气API完成从项目创建、网络请求、数据解析到界面展示的完整流程。工程采用Retrofit与Gson搭建网络层包含OpenWeatherMap接口的GET请求定义、异步enqueue回调、天气响应模型编写、城市与温度湿度UI绑定、SwipeRefreshLayout下拉刷新、错误处理与进度加载等关键实现代码注释清晰可循。压缩包内共1260个文件以Java源码、XML布局、JSON配置、Gradle构建脚本为主同时附带DEX/Class编译中间文件与可直接安装的app-debug.apk整包约9.76MB便于对照源码查看布局、配置与构建产物之间的对应关系。目前已有3876人学习下载可用于课程设计或自学实战适合对照完整工程逐段调试也可在现有基础上扩展多城市切换、多日预报或图标动画等进阶功能稳步提升Android Studio应用开发能力。1. AndroidStudio 天气预报小程序先看清楚这份源码能干什么用 AndroidStudio 写完一个能跑的天气预报小程序比大多数人预想的更考配置。这份源码我拆过的人至少有一半在“第一次跑通”上就卡了半天要么密钥填错位置返回 401要么明文 HTTP 被 Android 9 之后的默认策略直接拦掉。它不是炫技项目而是一个标准的“网络请求 → JSON 解析 → 列表渲染 → 城市切换”闭环适合正在练 Android 网络层和列表开发的新手也适合想拿现成代码改改马上用的从业者。下文从数据层开始拆中间包含可直接抄的代码和参数说明最后落到我踩过的具体坑上。2. 数据层第三方天气 API 选型与网络请求模块搭建2.1 API 选型为什么用“实时天气 逐天预报”两个接口写天气类 App 要过的第一关就是数据源。市面上的天气 API 有免费也有付费返回结构差别很大。我拆这份源码时看到的设计是“一个密钥 两个接口”实时天气接口拿当前温度、天气现象、湿度、风向逐天预报接口拿未来 3 到 7 天的最高最低温和白天夜间天气描述。这样组合的原因很直接实时天气数据量小请求快首页头部的“当前温度”必须靠它逐天预报数据量大但不需要每秒钟更新放列表页正好。选接口时主要看四个参数请求协议必须 HTTPS否则下面避坑会提到明文流量问题、响应格式JSON 最好解析、免费配额免费 Key 通常有每分钟请求数限制、字段命名不同服务商字段差异极大。很多人在这一步翻车比如拿到的一天天字段不是tempMax而是tmp_max解析直接崩。所以第三方的免费 API 不等于不用读文档至少把返回示例拷贝下来对着建实体类这是最稳的做法。这份源码里用到的核心请求参数大致是这几个参数示例值说明location101020100城市代码不是城市拼音需要先用搜索接口换keyxxxx你自己的密钥在服务商控制台申请QPS 受限unitm公制单位温度返回摄氏度langzh返回中文字段比如“晴”“多云”如果你用的是另一个常见天气平台参数名会略有不同但整体思路一致先通过“城市搜索”接口把用户输入的城市名换成 location 代码再拿着这个代码去请求实时和逐天数据。这份源码里已经内置了热门城市列表所以我一般会说新手先用热门城市列表跑通流程再去研究搜索接口的细节。2.2 请求模块搭建OkHttp 封装与超时参数网络请求部分这份源码用的是 OkHttp。相比 HttpURLConnectionOkHttp 自带连接池、超时控制和响应缓存代码量少一半出问题也好定位。我拆到的请求模块长这样public class WeatherApi { private static final String BASE_URL https://www.exampleweather.com/api/; private static final String API_KEY 你的密钥; private static final OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); public static void fetchWeather(String cityCode, final Callback callback) { String url BASE_URL now?location cityCode key API_KEY unitmlangzh; Request request new Request.Builder() .url(url) .get() .build(); client.newCall(request).enqueue(new okhttp3.Callback() { Override public void onFailure(Call call, IOException e) { callback.onError(e.getMessage()); } Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { String body response.body().string(); callback.onSuccess(body); } else { callback.onError(HTTP response.code()); } } }); } }这个封装的逻辑不复杂拼 URL发起 GET 请求回调里把结果字符串回传。两个参数值得留意connectTimeout是建立 TCP 连接的超时readTimeout是拿到响应体的超时我一般两个都设 10 秒。很多天气接口在弱网环境下响应很慢这个值设太小会频繁失败设太大用户会感觉卡死。另一个细节是response.body().string()必须在子线程调用OkHttp 的enqueue已经帮你切到了子线程回调所以这里不用手动开线程。真正要小心的是回调里更新 UI 那一步后面避坑章节会单独说这个问题。天气数据拿到之后还有一个关键点是接口错误码。常见的有401 无效密钥、400 参数错误、404 城市不存在。不管请求成功还是失败返回体里都会有一个code字段所以在回调里只判断isSuccessful()是不够的的还要拿到返回体再解析一次code。我见过有人看到 HTTP 200 就直接解析结果 JSON 里写的是“当前城市欠费”列表渲染全是默认值。3. 解析层从 JSON 字符串到可绑定的天气实体3.1 实体建模字段命名与响应结构的映射关系网络请求拿回来的是 JSON 字符串界面层不能直接消费它必须转成 Java 对象。实体类怎么建决定了后面所有ViewHolder绑定的代码是干净还是啰嗦。天气接口的典型返回结构分三层最外层是整个响应的code、updateTime第二层是具体的天气数据对象实时就是一个now对象预报就是一个daily数组第三层才是温度、湿度这些实际字段。所以实体类也应该照这个结构建三层。public class WeatherResponse { private String code; private String updateTime; private Now now; private ListDaily daily; public String getCode() { return code; } public void setCode(String code) { this.code code; } // getter / setter 省略 public static class Now { private String temp; // 实时温度 private String text; // 天气现象文字描述 private String humidity; // 相对湿度 private String windDir; // 风向 private String windScale; // 风力等级 public String getTemp() { return temp; } public void setTemp(String temp) { this.temp temp; } // getter / setter 省略 } public static class Daily { private String fxDate; // 预报日期 private String tempMax; // 最高温度 private String tempMin; // 最低温度 private String textDay; // 白天天气现象 private String textNight; // 夜间天气现象 public String getFxDate() { return fxDate; } public void setFxDate(String fxDate) { this.fxDate fxDate; } // getter / setter 省略 } }实体类字段名的设计原则就一条与 JSON 返回的字段名保持一致。这样用 Gson 解析时就是“零配置”如果接口字段是下划线风格比如temp_max要么在 Gson 加SerializedName注解要么直接把实体字段名改成下划线风格。我习惯用注解保留 Java 的驼峰命名这样代码规范一点。这里多说一句温度字段在接口里返回的是字符串而不是整数。原因是有些天气场景下可能返回23也可能返回23.5用 String 接收最不容易抛异常需要计算时再Double.parseDouble()转一下。这是个很小的设计取舍但能避免很多NumberFormatException。3.2 Gson 解析三步拿到可渲染的数据实体建好后解析就变得很机械。Gson 的fromJson一行代码就能完成难点反而在解析之后的空判断和异常处理。下面是这份源码里解析方法的典型写法public static WeatherResponse parseWeather(String json) throws JsonSyntaxException { Gson gson new Gson(); WeatherResponse response gson.fromJson(json, WeatherResponse.class); if (response null || !200.equals(response.getCode())) { return null; } if (response.getNow() null response.getDaily() null) { return null; } return response; }逻辑说明先跑一次fromJson把 JSON 字符串按WeatherResponse的结构映射成对象然后连续做两个防御判断。第一个判断拦住“接口业务失败但 HTTP 200”的情况第二个判断拦住“响应空壳”的情况。参数说明fromJson的第二个参数是TypeToken或Class这里因为daily是ListDaily如果整个解析用泛型复杂结构就需要new TypeTokenListDaily() {}.getType()。但我建议把ListDaily直接放进外层实体这样fromJson只需要传WeatherResponse.class就行避免嵌套泛型带来的坑。解析完成之后数据怎么流到 UI 层是下一个问题。我推荐的常见做法是Activity 持有一个WeatherResponse实例onSuccess回调里先runOnUiThread切换主线程再刷新控件不要把网络回调里的线程直接更新 UI这个问题等避坑章详细说。要注意的是有时候接口返回的daily数组长度为 0但now有值界面要能降级显示头部实时温度正常渲染下面的逐天列表显示“暂无更多预报数据”。所以parseWeather里的防御判断不能一个把所有情况都拦截要区分“实时接口”“预报接口”单独解析。我拆到这份源码时发现它的做法是直接分别写两个解析方法代码虽重复但逻辑清晰对新手更友好。4. UI 层主界面布局、城市切换与下拉刷新的完整落地4.1 主界面布局从 XML 到 RecyclerView 绑定界面部分这份源码走的是标准套路顶部一个大卡片显示当前城市、实时温度、天气现象下面是一个RecyclerView渲染未来几天的预报列表。布局文件不复杂但有几个尺寸细节值得注意温度文字建议用超大号textSize60sp 左右才有天气 App 的感觉天气现象文字次之温区信息放最后。布局的骨架像这样LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical TextView android:idid/tv_city android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:textSize20sp / TextView android:idid/tv_temp android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:textSize60sp / TextView android:idid/tv_weather android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:textSize18sp / /LinearLayout逻辑说明这是主界面上半部分的大卡片布局三个TextView从上到下排列layout_gravity用的center_horizontal保证了水平居中。温度文字的textSize是 60sp这是天气类 App 的通用做法视觉重心全在那个大数字上。下半部分的预报列表用RecyclerView加LinearLayoutManager就够了。这个场景不需要GridLayoutManager因为列表就是一天一行。每行的高度我建议固定为wrap_content不用match_parent这样即使daily数组里只有 3 天数据也不会出现空白占位。4.2 城市切换与下拉刷新列表数据更新的流转逻辑城市切换是这份源码的核心交互。用户点城市列表里的“上海”“北京”界面数据要全部更新。最粗暴的做法是Activity里写一个updateWeather(String cityCode)方法里面重新请求并刷新所有控件。这个方法要处理的细节包括切换时旧请求的回调可能晚于新请求返回导致界面显示错乱。我拆到的做法的核心思路是“每次请求前先取消上一次请求”。OkHttp的Call对象支持cancel()方法在发起新请求之前把旧call取消然后再enqueue新的能规避大部分竞态问题。虽然不能完全避免但成本极低值得写。下拉刷新用SwipeRefreshLayout包住整个页面。它的回调onRefresh里重新请求数据成功之后调setRefreshing(false)收起刷新圈。这个流程有个坑如果请求失败也要调setRefreshing(false)否则刷新圈会一直转用户只能杀进程。看源码里有没有加finally判断是快速判断这份源码完成度的一个角度。适配器的实现没什么特别唯一的经验是列表的onBindViewHolder里面不要做字符串拼接以外的计算比如日期格式化、温度单位换算这些提前在Daily实体里算好用新字段保存。原因是RecyclerView滑动时会频繁调用这个回调任何耗时的 CPU 操作都会让列表帧率肉眼可见地下降。5. 避坑指南权限、线程与数据更新踩过的五个典型坑5.1 崩溃型问题明文流量被拦截与主线程网络请求第一个坑是 Android 9 之后默认禁止明文 HTTP 流量。现象在模拟器或新手机上跑 App请求直接IOException: Cleartext HTTP traffic to xxx not permitted连不到服务器。原因Android PAPI 28开始默认的网络安全策略禁止未加密的 HTTP 流量而很多免费天气 API 的免费套餐只给 HTTP 地址。解决在AndroidManifest.xml的application标签里加android:usesCleartextTraffictrue或者更规范的做法是配置network_security_config.xml只允许指定域名走明文。我一般建议直接用 HTTPS 接口一劳永逸。第二个坑是网络请求直接写在主线程一运行就闪退。现象日志里出现android.os.NetworkOnMainThreadExceptionApp 启动后立刻崩溃。原因这是 Android 系统的StrictMode策略主线程不允许进行耗时网络操作否则直接抛异常。解决同步请求必须放到子线程new Thread(...).start()或者像第 2 章那样用OkHttp.enqueue异步回调。记住一条铁律网络请求的execute()永远不要出现在onCreate里。5.2 数据型问题字段错位、城市代码与缓存不刷新第三个坑是 JSON 解析成功但界面全是默认值。现象不崩溃列表能渲染但温度显示 0天气现象显示空字符串。原因接口字段名和实体类字段名不一致。比如接口返回tmp_max你的实体写的tempMaxGson 默认不匹配就直接留空parseWeather校验又过滤掉了空值。解决抓一份接口实际返回的 JSON 原文对照着逐字段检查实体类。如果不想改字段名在字段上加SerializedName(tmp_max)注解。这是最典型的“编译通过但数据不通”的问题调试时一定要看 JSON 原文。第四个坑是城市名能查出来但天气请求返回城市不存在。现象城市搜索接口返回了结果列表但拿location去请求天气时候返回404。原因免费天气 API 的location参数要求的城市代码类似101020100不是拼音也不是汉字。很多人在搜索接口里看到cityName字段就拿来当定位参数。解决先请求城市搜索接口从返回的 JSON 里取出真正的location字段再拼进天气接口的 URL。这份源码里内置了热门城市列表就是为了避免让新手一头扎进搜索接口的细节里。第五个坑是下拉刷新之后数据没有变化。现象改了城市代码或者等了很久页面数据还是旧的。原因免费 API 的免费 Key 有缓存策略同一个location在短时间内的多次请求可能返回同一份数据另外 Android 端如果做了内存缓存刷新逻辑没有清缓存也会拿旧数据。解决API 侧加一个时间戳参数很多服务商支持t当前时间戳让请求绕开缓存本地缓存则在刷新开始时主动清除并重新请求。注意 QPS 限制高频刷新会被限流返回 429那就不是数据旧的问题了。6. 进阶技巧离线缓存与多城市记忆的两个实战改动当主流程跑通之后离“一个能日常用的天气 App”还差两步离线缓存和历史城市记忆。这两个改动加在一起不超过 200 行但使用的体感会提升一个档次。离线缓存的思路是利用SharedPreferences存最后一次请求成功的 JSON 字符串App 启动时先读缓存直接渲染再去后台拉新数据。这样即使没网也能看到上次的天气而不是白屏。我倾向把缓存放在Application级别的单例里存成JSONObject以城市代码为 key不要整个页面一个 key否则切换城市后没网只能看到固定城市的旧数据。多城市记忆的做法也简单用SharedPreferences存一个SetString每次切换城市成功就把城市代码加进去下拉刷新时读出来作为“我的城市列表”。这里有两个细节Set的读写要保证线程安全建议放在onPause或每次切换成功后立即持久化存储的城市代码要同时存中文名否则恢复列表时渲染不出中文城市名。从那以后我每次写完带网络请求的 Demo都会强制走一遍“明文流量、主线程网络、字段匹配、刷新停止、空数据降级”这五个检查点再交给测试的同学跑真机。这套习惯帮我省了大量翻车时间希望帮到你。本文还有配套的精品资源点击获取
返回列表