news 2026/10/23 15:23:12

手把手拆解CoolWeather:Android新手必做的天气项目实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手拆解CoolWeather:Android新手必做的天气项目实战

如果你学 Android 学到一定程度,整天对着 TextView、Button 打转,想找个完整项目练手,那 CoolWeather 这个名字多半听过。它是《第一行代码》里贯穿后半本的重头戏,一个联网的天气应用——启动之后先选城市,然后展示当前温度和天气状况,支持手动刷新,还能在后台定时更新天气。听起来不复杂,但它把 Android 开发里最常用的几块硬骨头全过了一遍:网络请求、JSON 解析、SQLite 数据库、Service 后台任务、Activity 和 Fragment 的配合,还有列表界面的展示。

我当时做这个项目时,连 Gradle 依赖冲突都整不明白,照着原书敲代码,一路踩坑一路搜答案,最后看到模拟器里天气数据刷出来的那一刻,才觉得自己对 Android 开发终于有了“整体感”。这篇文章不打算复述书里每一行代码,而是从一个小白的真实视角,把这个项目到底做了什么、开发前要准备什么、核心功能怎么一步步实现,以及我自己踩过的坑和排查思路,尽量完整地分享出来。如果你正准备拿这个项目练手,或者已经做到一半卡住了,里面提到的不少问题你应该都会遇到。

1. 项目拆解:这个天气应用究竟包含多少核心功能

1.1 功能清单和用户使用路径

先把这个应用当产品来看,不要一开始钻代码。CoolWeather 的用户路径非常清晰:第一次打开 App,看到的是省市县三级联动选择界面,你一层层点进去,选到具体的县或城市,App 会记住这个选择,然后进入天气展示界面。天气界面会显示当前城市、更新时间、当前温度、天气状况、最高最低温,还有未来几天的天气预报、空气质量、生活建议(比如穿衣、运动、洗车指数)等等。

展示出来之后,你可以在界面上手动点击刷新,也可以让它自己在后台每隔一段时间重新请求一次天气数据。如果用户把 App 放在后台或锁屏,Service 会继续干活,下次打开主界面时看到的数据就是新的。另外,选过的城市和最后一次天气数据会存到本地数据库和 SharedPreferences 里,下次启动直接显示,不用每次联网拉全量数据。

这个功能范围对新手来说刚刚好:不大不小,五脏俱全。它有交互、有网络、有存储、有后台逻辑,做完之后你对 Android 的四大组件基本都能上手一遍,尤其是 Service 和 Activity 的关系,很多人学的时候云里雾里,做完这个项目就会有实感。

1.2 技术栈盘点:每个名词背后是什么

不用急着把每个技术都吃透,但至少要知道这个项目里会用到哪些东西。

  • UI 层:Activity + Fragment + XML 布局,城市选择界面用 Fragment 来管理三个层级的列表,天气主界面是自定义布局,配合 RecyclerView 展示未来几天的预报。
  • 网络层:基于 OkHttp 封装一个简单的工具类,发起 HTTP 请求获取天气数据。
  • 数据解析:Gson 把 JSON 字符串转成 Java 对象。这一步是很多新手的噩梦,因为天气服务商返回的 JSON 嵌套层次特别多,一不小心 Gson 解析就崩。
  • 数据库:SQLite + LitePal 这个 ORM 框架来存储省市县数据。原书把全国省市区数据打包在 assets 目录下的 JSON 文件里,第一次启动时解析并存入数据库,之后直接从数据库读,速度快也省流量。
  • 后台任务:用 Service 做后台循环请求,通过线程里的 sleep 控制更新间隔,同时结合广播或通知来提示用户。
  • 图片加载:天气图标是图片,用 Glide 来加载,顺便处理复用和缓存的问题。

这些技术随便单拎一个出来都够学一阵子,但在这个项目里它们被组合成一条完整的数据流:数据库读城市信息,网络请求天气 JSON,解析成对象,存储缓存,再绑定到 UI 上。把这套流程走通,比单独看任何一篇教程都管用。

1.3 为什么说这个项目适合小白

很多人一上来就去啃大型开源项目,结果连工程结构都看不懂,越看越受挫。CoolWeather 的规模控制得比较好,它不像商业项目那样有复杂的架构和几十个模块,但也比教科书里单页面的小 Demo 高级不少。你可以跟着原书一步步做下去,每完成一个模块,都能在 App 里看到实际效果,这种正向反馈对小白的坚持非常重要。

更重要的是,这个项目逼着你接触“真实开发”的思维方式。比如网络请求不能写在主线程,你会发现不这么干界面直接卡死甚至 ANR;数据从服务器拿回来是 JSON,不能直接用,必须解析;每次刷新界面之前要判断用户有没有选城市,各种边界条件很多。这些意识不是看理论能建立的,必须亲手把一个功能从接口跑到界面走一遍,才会刻在脑子里。

2. 开发前的准备:环境、数据和工程规划

2.1 开发环境与版本匹配

如果你用的还是老旧的教程配置,项目很可能一上来就编译不过。我的建议是直接用较新的稳定版 Android Studio,JDK 用 17 或 21,Gradle 版本跟着 Android Studio 自动匹配就行,不要手动去改。然后是 Android SDK,至少要包含你当前设备或模拟器对应的 API Level,同时项目里的 compileSdk 和 targetSdk 建议保持一致。

容易踩的坑是号不对齐。比如原书案例基于某个较老的 Android 版本,如果你把 compileSdk 升得很高,有些依赖库的旧版本反而会出现兼容问题。我当时就在 OkHttp 版本上卡了很久,后来统一升级到库作者推荐的版本,问题才消失。所以一开始不要迷信“越新越好”,稳定能跑比什么都重要。

模拟器和真机我建议都准备。模拟器调试方便,但天气定位和后台服务的行为和真机还是有区别,尤其是国产手机的省电策略会在后面给你上眼药。我自己是先模拟器跑通功能,再放到真机上观察后台更新,两个环境都得测。

2.2 天气数据从哪来

原书用的是某个免费天气数据服务商的开放接口。你需要去它的开放平台注册一个账号,创建一个应用拿到 API Key,然后请求接口时会带上城市 ID 和 Key,服务商返回 JSON 格式的天气数据。

这里有两个重要提醒。第一,注册后不要急着写代码,先在浏览器或接口调试工具里请求一次,看看实际返回的 JSON 长什么样。很多新手上来就写解析代码,结果字段名对不上,Gson 解析报错时一脸懵。第二,免费接口会有访问频率限制,调试时不要疯狂点击刷新,否则可能被临时封禁,我当时就因为这个白等了一个小时。

城市 ID 的问题也要注意。原书的 assets 目录里有一个全国省市区数据文件,每个城市都会绑定一个 weather_id,比如某个城市对应的是“CN101010100”这种格式。选城市时拿到这个 weather_id,后面请求天气数据时把它拼进 URL 里。所以这个项目的数据流是:内置 JSON 文件 -> 数据库 -> 界面选择 -> 拿到 weather_id -> 拼接天气接口 URL -> 请求并解析。

2.3 工程目录设计要提前想好

原书案例的包结构很清楚,我建议你也照着分一遍,不要图省事全部写在 MainActivity 里。大致可以分成下面几个包:

  • activity:存放主界面、天气展示界面的 Activity。
  • service:后台更新天气的服务。
  • db:数据库相关的实体类和数据操作。
  • util:网络请求工具、解析工具、常量类。
  • model:对应天气 JSON 结构的实体类,比如 Province、City、County、Weather、AQI、Forecast 等等。
  • fragment:城市选择界面用的 Fragment。

这种分包的好处是职责清晰。网络找 util,数据找 model 和 db,界面逻辑找 activity 和 fragment。后面如果你要改成 MVVM 架构,也是在分包的基础上再增加一层,不用大改。我见过很多新手把全部逻辑揉在 Activity 里,先不说维护,自己过几天回来看代码都找不到方法在哪。

3. 一步步实现核心功能:从选城市到显示天气

3.1 省市区三级联动:读数据库还是写死数组

城市选择功能的核心是一套三级列表联动,界面长这样:左边或当前屏显示省份列表,点击某个省份进入市列表,点击市进入县/区列表,选到最末级之后把选择结果回传给天气界面。

在实现时,不要把这些城市数据硬编码成一个超长数组写在 Java 代码里。正确做法是把它们存在 SQLite 数据库里。原因很简单:数据有层级关系,筛选查询方便;而且数据量不小,存库比写死数组干净得多。原书用的是 LitePal 操作数据库,实体类定义好之后,直接用 LitePal.find() 就能查出来。

// 查询省列表 List<Province> provinceList = LitePal.findAll(Province.class); // 查询某个省下的市 List<City> cityList = LitePal.where("provinceId = ?", province.getId()).find(City.class); // 查询某个市下的县区 List<County> countyList = LitePal.where("cityId = ?", city.getId()).find(County.class);

第一次启动时,需要把 assets 里的省市区 JSON 数据解析出来并存入数据库。这里有一个关键性能点:解析和写入数据库应该放到子线程,不要在主线程执行,否则启动画面会卡顿甚至 ANR。

我当时在这个功能上遇到的问题比较低级:数据解析成功了,但数据库里查出来是空的。排查了半天发现是因为 LitePal 的实体类对应表名和默认映射规则不一致,我建表时改过表名,但查询时用了默认的类名去找,自然查不出东西。建议刚开始用 LitePal 时保持类名和表名对应关系简单统一。

3.2 天气主界面:UI 和数据怎么绑

天气主界面是用户最终看到的核心页面。它通常包含一个可上下滑动的 ScrollView,内部嵌套多个布局块:顶部是当前城市和更新时间,中间是当前温度、天气状况图标,下面依次是未来几天预报、空气质量、生活建议。

界面本身不难,难的是数据怎么从网络请求到 JSON,再解析成对象,最后填到布局里。我习惯写一个工具方法,比如 loadWeather(String weatherId),方法内部开一个子线程去请求接口,拿到 JSON 后用 Gson 解析成 Weather 对象,再通过 runOnUiThread 切回主线程更新界面。

String response = HttpUtil.sendOkHttpRequest(url); final Weather weather = Utility.handleWeatherResponse(response); runOnUiThread(new Runnable() { @Override public void run() { if (weather != null && "ok".equals(weather.status)) { // 把缓存存到 SharedPreferences SharedPreferences.Editor editor = pref.edit(); editor.putString("weather", response); editor.apply(); // 刷新各个控件的显示 titleCity.setText(weather.basic.cityName); nowTemp.setText(weather.now.temperature + "°C"); // ... } else { // 提示用户加载失败 } } });

这段逻辑在原书里反复出现:请求、解析、切换线程、更新 UI。我强烈建议把这条链路记到脑子里,它会伴随你整个 Android 开发生涯。无论是登录、列表还是详情页,套路都差不多。很多人卡住的原因就是没理解为什么不能直接在主线程请求,其实你只要在代码里故意这么写一次,看到应用直接崩溃或者卡住,就一辈子记住了。

3.3 后台自动更新:Service 不是万能保险

自动更新天气是一个典型的后台任务需求。原书用 Service 来实现:在主界面 Activity 的 onStart 方法里启动服务,在 onStop 里停止服务,而服务内部维护一个循环,每次请求天气数据之后让线程睡一段时间,比如 8 小时,然后再请求下一次。

public class AutoUpdateService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { while (true) { // 请求最新天气并更新缓存 updateWeather(); try { Thread.sleep(8 * 60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); return START_STICKY; } // ... }

这里有一个非常重要的背景知识:Android 8.0 之后,纯后台服务很容易被系统限制,一不小心就被杀掉。如果你的 targetSdk 比较高,常见做法是把 Service 改造成前台服务,也就是在通知栏显示一个常驻通知,告诉用户“当前有服务在后台运行”。从产品角度,一个天气应用常驻通知确实不太友好,但在学这个项目阶段,理解 Service 生命周期比纠结产品体验更重要。

我在真机调试时发现,老代码里那种while (true) + Thread.sleep的方式在省电模式下完全撑不过去,手机锁屏半小时后服务就没了。后来我把更新逻辑改成 WorkManager 定时任务,利用系统提供的周期任务框架,可靠性高了不少。这部分属于进阶优化,如果你想彻底搞清楚 Android 后台机制,可以往这个方向研究,绝对值得。

3.4 手动刷新和切换城市

天气主界面的刷新按钮逻辑其实和加载天气是一个方法,只不过多了一层 UI 反馈:点击后刷数据,成功更新界面,失败提示网络异常。切换城市则是从主界面跳回城市选择界面,重新选完之后把新的 weather_id 传回来,再次触发请求。

这里有一个容易被忽略的点:切换城市之后的返回键行为。如果你用 Activity 跳转的方式重新打开选择界面,用户选完城市按返回键,很容易回到旧的天气界面而不是刷新后的新城市天气。我当时处理得比较粗暴,在返回主界面时重新加载天气,结果有时候加载没完成,界面显示的还是旧数据。后来在 onActivityResult 里加了一个标识位,强制刷新成功前显示加载状态,才把这个问题解决。

关于手动刷新,还有一个下拉刷新组件可以直接用。原书较早版本没有引入下拉刷新,只是做了一个按钮。现在做的话,建议直接用 SwipeRefreshLayout 包住 ScrollView,用户体验更接近主流 App。

4. 踩坑记录与排查思路

4.1 网络请求失败:从权限到明文流量

如果你做完这个项目还没遇到“请求失败”,那可能是你运气好。我第一次运行的时候,天气死活在界面上刷不出来,Logcat 里一堆异常。排查步骤如下:

第一,检查 AndroidManifest.xml 里有没有声明网络权限。这听起来像废话,但漏的人不在少数,尤其是直接复制别人的清单文件时最容易把权限弄丢。第二,检查 URL 拼得对不对,尤其是 weather_id 和 Key 参数。我调试时习惯把最终拼出来的 URL 打印在 Log 里,再复制到浏览器打开,如果浏览器能正常返回 JSON,说明接口没问题,问题出在 App 代码。第三,检查 Android 9.0 以上对明文 HTTP 的限制。如果接口地址是 http:// 而不是 https://,系统默认不允许应用直接访问明文流量。解决办法是在 AndroidManifest.xml 的 application 标签上加android:usesCleartextTraffic="true",或者配置网络安全策略文件。

超时问题也很常见。默认连接超时时长可能太短,尤其是模拟器网络状态不好的时候,请求稍微慢一点就报 SocketTimeoutException。建议在 OkHttp 或底层连接时显式设置 connectTimeout 和 readTimeout,比如 10 秒。

4.2 JSON 解析异常:结构对照法是核心技能

Gson 解析崩是小白遇到最多的报错之一,常见提示包括 JsonSyntaxException 或 NullPointerException。根本原因几乎都是“代码里的实体类结构和接口实际返回的 JSON 对不上”。

接口返回的天气数据往往是多层嵌套的,这里就不贴完整 JSON 了,直接说几个容易踩的点。第一,顶层可能是一个数组,比如HeWeather,你必须用 List 来接收,而不是单个对象。第二,某些字段是字符串类型,比如温度"18",你在实体类里如果用 int 接收,Gson 在某些情况下也会出问题,建议和接口保持一致用 String。第三,不同的天气状况字段可能在多个层级,比如实时天气是now.cond.txt,未来预报是数组里的cond.txt_d,不要搞混。

我最推荐的排查方式是:把返回的 JSON 原样复制到一个在线 JSON 格式化工具里,一边看结构一遍对着实体类改。不要靠猜,不要靠改一个字段试一次,效率太低。另外一个技巧是在解析前先打印 response,很多时候解析失败是因为请求本身返回了错误信息,比如 Key 失效,而不是数据结构不对。

4.3 后台服务不干活:本质是系统资源的博弈

后台服务是小白最容易误以为“写完就完了”的部分,实际上它涉及 Android 系统对应用资源的限制。Samsung、华为、小米等手机都有各自的省电策略,会在特定条件下清理后台进程或限制网络访问。我自己测试时出现过这种现象:天气应用在前台一切正常,锁屏放了一个晚上,第二天解锁打开,数据显示还是昨晚的,也就是说服务在后台根本没跑起来。

排查思路是这样的:先看服务有没有被系统杀死,可以在服务里加日志,观察 onDestroy 是否被调用;再看是否是因为应用被放在了省电名单外或被限制后台联网。同时也要检查代码里有没有在服务启动条件上犯低级错误,比如没有调用 startForeground 导致 Android 8.0 以上启动服务直接被拒。做进阶优化时,可以用 WorkManager 替代纯 Service,它能在系统允许的时间窗口内执行任务,被系统杀死后还能重试,是最稳妥的姿势。

4.4 图片加载和列表卡顿

天气应用中图片不算多,但如果你把未来几天的预报做成列表,并尝试用 ListView 或 RecyclerView 加载天气图标时,就会遇到图片加载和列表复用的问题。直接在主线程用BitmapFactory.decodeFile去加载网络图片会卡到你怀疑人生,正确思路是交给 Glide。

用 Glide 很简单:

Glide.with(context) .load("https://example.com/icon/" + weatherCode + ".png") .into(imageView);

它内部会自动做图片缓存、尺寸压缩和异步加载,不需要你自己管线程。我还要提醒两个细节:一是 Glide 的with(context)最好传入 Activity 或 Fragment 的实例,不要传 Application Context,否则无法感知生命周期,可能在界面销毁后继续加载导致问题;二是 RecyclerView item 里如果有圆角或阴影效果,尽量避免在运行时动态创建 ShapeDrawable,这会加大绘制成本,我当时就因为在 item 的bindViewHolder里每次 new 一个背景,导致列表滑动时有明显掉帧。

5. 如果重新做一次,我会怎么优化

5.1 架构升级:从堆代码到有层次

原书的代码偏向功能实现,Activity 里往往包含了网络请求、解析、更新 UI 的一整套逻辑。作为学习没问题,但做完之后,下一步最好做架构层面的重构。最直观的改动是引入 ViewModel + LiveData,把 Activity 里的数据持有和更新抽离出来。

ViewModel 的好处是界面旋转时数据不会丢,LiveData 可以自动感知生命周期,避免内存泄漏。原来用 Handler 切线程更新 UI 的代码可以大幅删减。做完这个重构,你会发现 Activity 瘦了很多,逻辑边界更清楚:网络请求放仓库层,数据存放到数据库层,界面只关心状态的展示。

我当时按照这个思路改造时,顺便把网络库换成了 Retrofit 配合协程,因为 Retrofit 的接口定义方式比手写 OkHttp 工具类干净得多,配合 Gson 转换器甚至可以自动完成解析,主线程调度也由协程接管。可能有人觉得这违背了“小白项目”的初衷,但恰恰相反,从能跑到写得体面,本身就是一个工程师成长的必经路径。

5.2 功能扩展:往产品方向多想一步

技术跑通后,你可以从产品角度问自己几个问题:用户真的愿意手动打开 App 看天气吗?天气数据能不能自动定位当前城市?未来几天的气温变化能不能用图表展示?空气质量指数有没有颜色分级?极端天气有没有预警推送?这些问题每一个都可以变成一个扩展点。

我建议挑一两个做深,不要全都做。比如定位功能,用系统自带的定位 API 获取经纬度,再通过地理编码接口反查城市,最后更新天气信息。这一个功能做完,你会接触到权限申请、运行时权限处理、定位服务回调等一系列新知识,成长比重复做列表要大得多。再比如把数据和图表结合,用自定义 View 画一个未来 24 小时温度曲线,又会涉及到 Canvas 绘制的知识。

5.3 从“照书敲”到“自己设计”

最后一条建议听起来玄,但非常重要。做完原书项目之后,可以尝试不看书,从零开始重新实现一遍功能。你可以换一套架构,换一套命名,换一个 UI 风格,甚至把省市区数据源换成自建的接口模拟。这个过程你会明显感觉自己从“代码搬运工”变成了“设计者”。

我个人的体会是,照书敲代码时,很多问题是被书里的解决方案提前屏蔽掉的,你没有真正经历过“面对问题、分析方案、挑选技术”的思考过程。重新自己设计时,你会发现自己开始主动思考缓存策略、接口设计、错误提示、边界情况,这些才是开发工作中真正值钱的能力。

如果再用这个项目练一次手,我会更早接入自动化测试和日志系统,把网络请求和解析结果用日志记录下来,而不是等到报错时再一个 Toast 一个 Log 地打。记住一点:这个项目的核心价值不全在天气功能,而在于你通过它把 Android 开发的整个链路打通了。后面你再做任何 App,都会发现,很多内容不过是对这套链路的重组和扩展。

最后再分享一个小技巧:调试天气项目时,在 OkHttp 拦截器里打印完整请求和响应,或者直接用日志工具观察 API 返回。你看到的原始 JSON 永远是排查问题最直接的线索,比看任何报错堆栈都管用。踩过这些坑之后,再去看别的网络请求项目,你会淡定很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 7:00:38

P1114 非常男女:用前缀和+数组偏移求解最长平衡子段

在洛谷刷到 P1114“非常男女”计划 的时候&#xff0c;第一反应是这题名有点意思。它本质上是一个 0/1 序列的区间问题&#xff1a;一排人&#xff0c;每个位置要么是男生要么是女生&#xff0c;要求找出一段连续的子排队&#xff0c;使得这一段里面男生数量和女生数量完全一样…

作者头像 李华
网站建设 2026/10/23 15:42:43

LoopBack 4 lb4 discover:从关系数据库发现并生成模型类

后端微服务 【免费下载链接】loopback-next LoopBack makes it easy to build modern API applications that require complex integrations. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/lo/loopback-next 点击查看 免费下载 本篇指南讲解 LoopBack 4&#xff08;l…

作者头像 李华
网站建设 2026/10/23 13:35:37

归并排序解数组逆序对:原理、代码与实战避坑指南

聊一个几乎每个刷题党都绕不过去的经典问题——数组中的逆序对。它在《剑指Offer》里是排序章节的重头戏&#xff0c;LeetCode 上也对应了“交易逆序对总数”这道题。题面本身就一句话&#xff1a;给定一个数组&#xff0c;统计其中所有“前面数字大于后面数字”的数对数量。看…

作者头像 李华
网站建设 2026/10/22 7:21:28

纯Verilog手写UART串口回环:从时序原理到上板调试

简介&#xff1a;这份资源面向FPGA初学者与数字电路设计入门者&#xff0c;提供一套基于纯Verilog实现的串口通信与串口回环工程&#xff0c;可用于理解串行通信协议、掌握收发状态机设计并完成板级验证。工程实现FPGA接收串口助手数据后原样回传的回环逻辑&#xff0c;也可按需…

作者头像 李华
网站建设 2026/10/23 6:03:52

现代频谱估计实战:突破FFT分辨率极限的三大技术路线

如果你做过信号处理&#xff0c;大概率遇到过这种让人烦躁的场景&#xff1a;手里数据不算长&#xff0c;可能是一次雷达快拍&#xff0c;也可能是传感器采到的一段冲击响应&#xff0c;或者是一段只有几百个采样点的生物电记录。你想知道里面到底有哪几个频率分量&#xff0c;…

作者头像 李华