简介:这是基于Android Studio开发的天气预报小程序完整源码,面向初学Android网络请求、JSON解析与UI数据绑定的开发者,也适合用作课程设计或毕业设计的基础项目。项目使用Retrofit + Gson访问OpenWeatherMap API,利用@GET与@Query定义接口,通过GsonConverterFactory将JSON响应映射为WeatherResponse对象,再提取城市温度、湿度等字段并刷新界面控件;同时代码中处理了异常、加载态、下拉刷新及本地缓存,能够直接体现一个实际App从构建到运行的关键链路。压缩包共1260个文件,约9.76MB,包含33个Java源码、76个XML布局与配置、120个JSON数据文件,以及编译生成的509个flat资源、194个dex、192个class和可直接安装的APK,类型覆盖资源编译、字节码生成、打包安装各阶段,便于按目录对照学习。该资源已有3876人学习下载,无论想快速搭建可用天气应用,还是想分析第三方API接入和Gradle配置细节,都能在这份代码里找到对应实现。
1. 用AndroidStudio跑通一个天气预报源码:新手到改包只差三步
这个项目是一份用AndroidStudio从零搭建的天气预报小程序源码,适合刚学完安卓四大组件、想找一个功能完整的小项目练手的开发者。整包不依赖付费商业服务,天气数据走公开接口,界面包含定位天气、城市搜索、多城市管理和未来三天预报,核心链路是“网络请求 → JSON解析 → UI刷新”。我最初拆这个包,是想看一个小项目怎么组织网络层和界面层代码,结果发现它比想象中干净:请求封装、数据模型、页面展示三个层次分得清清楚楚,新手照着走一遍,基本就能改出属于自己的天气应用。下面按“读懂架构 → 拆透核心 → 跑通工程 → 上手改造”的顺序来展开。
2. 项目架构与技术选型:OkHttp、Gson与SQLite的分层思路
2.1 技术栈选型:为什么这个组合够用
这个小程序没有引RxJava,也没有上MVVM,而是用了Android开发里最稳妥的一套组合:OkHttp做网络请求,Gson做JSON反序列化,SharedPreferences存用户城市列表,SQLite做天气数据缓存,界面走传统XML布局加RecyclerView列表。这种选型对天气预报这种中小规模项目是合理的:核心诉求是“快速请求、干净解析、稳定展示”,引入协程或者RxJava反而会抬高理解门槛。尤其是对还在AndroidStudio调试阶段摸索的新手,传统的回调式异步请求读起来更直白,断点进去看得懂变量是怎么流动的。
为什么不直接用Retrofit?Retrofit确实在类型转换和注解式接口定义上更省事,但多包一层convert-factory之后,新手容易把“网络请求”和“接口定义”混在一起理解。OkHttp裸写反而能看清URL拼接、请求头、回调线程这些底层细节。等把这份源码吃透了,再去换Retrofit,你会明白它省掉的到底是什么。
从架构上看,项目做了三层拆分:数据层负责和天气接口打交道,模型层定义天气实体,UI层负责展示和交互。三层之间通过方法返回值传递数据,没有引入EventBus之类的消息组件。我一般会建议新手看项目时先找这三层的边界,边界清楚了,改功能才不容易把界面代码和网络代码搅在一起。
2.2 目录结构与模块职责
下面是去掉多余空目录之后的工程结构:
app/src/main/ ├── java/com/example/weather/ │ ├── MainActivity.java // 应用入口,负责城市管理和页面切换 │ ├── WeatherActivity.java // 天气展示主界面 │ ├── CityManageActivity.java // 多城市管理页面 │ ├── adapter/ │ │ ├── CityAdapter.java // 城市列表适配器 │ │ └── WeatherAdapter.java // 多日天气预报适配器 │ ├── model/ │ │ ├── WeatherData.java // 天气响应实体 │ │ ├── Realtime.java // 实时天气内部类 │ │ └── Daily.java // 逐日预报内部类 │ ├── util/ │ │ ├── HttpUtil.java // OkHttp请求封装 │ │ └── CityStorage.java // SharedPreferences持久化 │ └── db/ │ └── CityDBHelper.java // SQLite城市缓存辅助类 ├── res/ │ ├── layout/ │ │ ├── activity_weather.xml │ │ ├── activity_city_manage.xml │ │ ├── item_city.xml │ │ └── item_daily_weather.xml │ └── drawable/ │ ├── ic_sunny.xml │ ├── ic_cloudy.xml │ ├── ic_rain.xml │ └── ic_snow.xml └── AndroidManifest.xml注意,这是我把源码里实际类按职责梳理之后的呈现,包名和具体文件名以你下载到的为准。这里值得重点看的是util包和db包:HttpUtil管网络入口,CityStorage管用户城市列表的本地读写,CityDBHelper管天气数据缓存。这三处是改包时最常动的文件。drawable目录下的XML矢量图是天气图标的本地映射,没有走网络图片,离线也能显示,这个设计后面细说。
2.3 数据流设计:一次天气查询怎么流转
一次完整的天气查询流程是这样走的:用户在天气主界面输入或选择一个城市,WeatherActivity拿到城市名称后,先查SQLite缓存,看有没有该城市的天气数据;如果有且没有过期,直接展示;如果缓存为空或者超过设定的刷新时间(源码里写的是10分钟),就走HttpUtil请求天气接口。服务端返回JSON后,Gson把JSON解析成WeatherData实体,适配器拿到实体里的Realtime和Daily列表,逐个绑定到界面上。整个数据流是单向的,没有跨模块跳转通信,出了问题很好定位。
这个流程里有个细节值得注意:天气数据的缓存和用户城市列表走的是两条持久化通路。天气数据用SQLite存,因为体量大、要按城市批量检索;城市列表用SharedPreferences存一组字符串数组,因为用户添加的城市最多也就十几个,用键值对反而更简单。很多新手会把所有状态都塞进SharedPreferences,等数据变复杂以后读写逻辑就乱套了。这份源码的划分方式是一个不错的参照:结构化、量大的走SQLite,量小、键值型的走SharedPreferences。
2.4 生命周期与页面交互
应用的主界面不是单Activity结构,而是MainActivity作为容器,用Fragment承载天气展示与城市管理两个页面。切换城市时先把选中的城市名写入CityStorage,再通过回调通知天气Fragment从缓存或网络刷新数据。这个交互模式避免了Activity之间反复传Intent的麻烦,也让多城市状态有了一个统一的管理入口。如果之后想加“删除城市后自动切换到第一个城市”的逻辑,只需要在MainActivity的列表回调里判断一下选中城市是否还存在,不存在就默认取列表第一个,改动范围很小。
3. 核心模块拆解:天气数据从网络请求到界面渲染的完整链路
3.1 网络层:HttpUtil的封装与参数细节
HttpUtil是整套代码里最值得抄的模块,它把OkHttp的异步请求封装成了一个带回调的工具类,调用方不用关心线程切换和请求头,只需要把接口地址和回调传进去。
public class HttpUtil { private static final int CONNECT_TIMEOUT = 10; private static final int READ_TIMEOUT = 10; private static OkHttpClient client; private static OkHttpClient getClient() { if (client == null) { synchronized (HttpUtil.class) { if (client == null) { client = new OkHttpClient.Builder() .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .build(); } } } return client; } public static void get(String url, Callback callback) { Request request = new Request.Builder() .url(url) .addHeader("Accept", "application/json") .build(); getClient().newCall(request).enqueue(callback); } }这段代码的逻辑很直白:connectTimeout控制建立连接的超时时间,readTimeout控制读取响应的超时时间,都是10秒,对天气接口这种响应速度中等的公共接口是够用的。addHeader("Accept", "application/json")是告诉服务端只接受JSON格式响应,避免拿到一段纯文本导致Gson解析报错。getClient方法用了双重检查锁,保证全局只有一个OkHttpClient实例,因为OkHttp内部维护了连接池,复用同一个实例能减少重复建连的开销。这个工具类没有封装post请求,因为项目里所有数据获取都是GET,够用就好。
值得注意的一点是OkHttp的enqueue是异步回调,回调线程在工作线程而非主线程,所以拿到响应结果后不能直接在回调里改UI,要么用runOnUiThread,要么用主线程Handler做切换。源码里用的是runOnUiThread,后面碰到网络回调里直接操作TextView的情况,要警惕这类线程问题。如果你把回调里拿到response后直接setText,AndroidStudio会抛Only the original thread that created a view hierarchy can touch its views,这是一个很典型的新手翻车点。
3.2 数据模型与Gson解析
天气接口返回的JSON通常是嵌套结构,外层是响应状态和城市信息,里层是当天的实时温度、天气现象描述和未来几天的预报数组。这份源码没有把JSON里所有字段全部建出来,只保留了界面展示真正需要的部分。这个取舍很重要,因为接口里往往有大量用不到的字段,模型字段建多了,Gson只会在解析时多消耗一点时间,但字段少了会导致运行时空指针,所以“够用但不过量”是基本原则。
public class WeatherData { private String city; // 城市名称 private String updateTime; // 数据更新时间 private Realtime realtime; // 实时天气 private List<Daily> daily; // 逐日预报列表 public static class Realtime { private String temperature; // 当前温度 private String info; // 天气现象描述 private String humidity; // 相对湿度 } public static class Daily { private String date; // 预报日期 private String dayWeather; // 白天天气现象 private String nightWeather; // 夜间天气现象 private String high; // 最高气温 private String low; // 最低气温 } }Gson在解析时会把JSON里的字段名和Java类字段名逐一匹配,所以这里的字段名要和接口返回的键名保持一致。如果接口返回的键名是"tmp_high",Java里用驼峰high,解析出来就是null,这是JSON解析最常见的一类坑。为了避免这种问题,我一般会在字段上加@SerializedName注解,把JSON键名显式标注出来:
@SerializedName("tmp_high") private String high;注意,加了注解之后,Gson优先按注解里的名称匹配JSON键,这样Java字段名就可以随便取了,不再受接口返回命名风格约束。解析调用本身只有一行:WeatherData data = new Gson().fromJson(response, WeatherData.class);。但这里有个细节容易漏:response必须是JSON字符串,而不是OkHttp的ResponseBody对象,需要先response.body().string()转成字符串。这一步很多人会忘记,导致Gson直接抛JsonSyntaxException,日志看起来像JSON格式问题,其实只是入参类型不对。
3.3 UI展示层:从数据到卡片的渲染逻辑
天气主界面用的是RecyclerView加多类型布局,第一项是当前城市的实时天气卡片,展示温度、天气现象和湿度;后面的每一项是未来几天的预报,展示日期、白天夜间天气和温度区间。WeatherAdapter在onBindViewHolder里根据不同的viewType做不同的数据绑定。
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof TodayViewHolder) { TodayViewHolder todayHolder = (TodayViewHolder) holder; todayHolder.tvTemperature.setText(data.getRealtime().getTemperature() + "℃"); todayHolder.tvInfo.setText(data.getRealtime().getInfo()); todayHolder.tvCity.setText(data.getCity()); } else if (holder instanceof DailyViewHolder) { DailyViewHolder dailyHolder = (DailyViewHolder) holder; // position从0开始,0号位置是实时天气卡片,预报从1开始 WeatherData.Daily daily = data.getDaily().get(position - 1); dailyHolder.tvDate.setText(daily.getDate()); dailyHolder.tvTempRange.setText(daily.getLow() + "℃~" + daily.getHigh() + "℃"); dailyHolder.tvDayWeather.setText(daily.getDayWeather()); } }position的换算是个容易出错的地方。RecyclerView的position从0开始,而Daily列表下标也是从0开始,但0号position被实时天气卡片占用了,所以预报数据必须用position - 1去取。如果直接拿position当下标访问daily列表,第一次访问的是0号位置,但界面已经显示实时天气卡片,数据就对不上了。改这段代码时要注意:只要在item类型里多插一种卡片,这个偏移量就要同步重新算一遍,源码里用getItemViewType区分类型,而viewType的总数决定了偏移关系。
天气图标这一块,源码没有用网络图片,而是把天气现象描述和本地drawable资源做了映射,写在了一个静态方法里。比如“晴”对应ic_sunny,“多云”对应ic_cloudy,“小雨”对应ic_rain,适配器在绑定数据时通过getInfo()去匹配图标ID。这样做的好处是不依赖图片CDN,离线也能显示,缺点是图标资源只能是有限的几种,新增一种天气现象时,要同步在映射方法里补一个case分支。如果你想换成SVG图标,直接替换drawable目录下的矢量文件即可,适配器代码一行都不用动。
3.4 缓存模块:SQLite与SharedPreferences的配合
CityDBHelper负责天气数据的缓存。每次网络请求成功后,会把城市名、JSON字符串、时间戳写入SQLite;查询时先按城市名取数据,再用当前时间减去存储时间戳,超过10分钟就视为过期,重新请求网络。这个“时间戳比对”逻辑虽然简单,却是缓存模块能不能真正省流量的关键。
public WeatherData getCachedWeather(String cityName) { Cursor cursor = db.query("weather_cache", null, "city = ?", new String[]{cityName}, null, null, null); if (cursor != null && cursor.moveToFirst()) { long cacheTime = cursor.getLong(cursor.getColumnIndex("update_time")); if (System.currentTimeMillis() - cacheTime < CACHE_EXPIRED) { String json = cursor.getString(cursor.getColumnIndex("json_data")); return new Gson().fromJson(json, WeatherData.class); } } return null; }这段代码把SQLite查询、时间校验、JSON反序列化放在了一个方法里,调用方不用关心缓存细节,拿不到就直接走网络。注意CACHE_EXPIRED的单位是毫秒,源码里写的是10 * 60 * 1000,即10分钟。这里有个常见误用:有人会把expired时间直接写成1000乘60,忘记补两个零,导致缓存永远不命中,每次都走网络。我碰到过好几次这种问题,排查方式是在日志里打一条缓存的命中记录,用Log.d("cache", "hit or miss")标记一下,几秒内就能看出逻辑对不对。
CityStorage则用SharedPreferences存用户添加的城市列表。它内部处理的是字符串数组的读写,存的时候把List转为逗号分隔的字符串,读取时按逗号拆分回List。选逗号分隔而不是JSON序列化,是因为城市名里出现逗号的概率极低,怎么简单怎么来。如果你的城市列表里要存更多字段,比如城市ID加城市名,就得换JSON或者SQLite,这个要根据业务量级来定,不要一上来就上重方案。
4. 跑通路上的常见问题与避坑指南:网络、JSON与模拟器三座山
4.1 问题一:请求报IOException,连接不上天气接口
现象:模拟器里运行App,点击查询后Logcat打印“Failed to connect to...”,页面上没有任何天气数据,也不弹任何错误提示。
原因:两个因素叠加导致。第一,Android 9及以上默认禁止明文HTTP流量,如果接口地址是http://开头,系统会直接拦截,根本走不到连接逻辑;第二,模拟器访问宿主机服务有特殊的IP规则,如果接口地址写的是localhost,模拟器里的localhost指向的是模拟器自己,不是开发电脑,连接自然失败。
解决:先在AndroidManifest.xml的application节点加android:usesCleartextTraffic="true",允许明文流量。如果接口本身已经是https,就略过这一步。再检查接口地址是公网域名还是局域网IP,模拟器里不能用localhost,要换成局域网IP或者直接用公网域名。注意,usesCleartextTraffic这个属性上线前要关掉,比较规范的做法是只在debug包开启,release包走networkSecurityConfig配置只允许特定域名走明文,避免数据裸奔。
4.2 问题二:Gson解析抛JsonSyntaxException,App直接闪退
现象:网络请求成功,日志显示拿到了JSON字符串,但Gson在fromJson时抛出异常,界面还没渲染就崩了,堆栈指向解析代码那一行。
原因:绝大多数情况是模型字段和JSON键名不一致,或者JSON里某个字段是null,而Java模型用了基本类型int接收。比如接口返回"visibility":"10km",模型里写的是int visibility,Gson尝试把字符串转成int,转不过去就抛异常。还有一种情况是JSON里明确写了null,Gson把null赋给Integer没问题,但赋给int会直接NPE。
解决:把模型里可能为空的字段全部用包装类型Integer、String定义,避免拆箱空指针。对照接口返回的实际JSON逐层检查字段名,必要时加@SerializedName注解强制映射。还有一个小技巧:在fromJson外面包一层try-catch,解析失败时打印完整JSON和异常堆栈,方便快速定位是哪一层字段出了问题,而不是崩溃后一屏红。日志里看不到关键信息是最浪费时间的。
4.3 问题三:城市中文名传到URL里乱码,查询不到数据
现象:查询“北京”返回空数据,查询“beijing”却正常;或者日志里看到请求URL里的城市参数是一串百分号加数字,看着像加密字符串。
原因:HTTP URL里不能直接携带中文,必须做URL编码。很多新手直接把城市名拼接进url字符串,服务端收到的中文已经乱码,自然匹配不到城市ID。AndroidStudio里日志可能显示正常,因为Logcat对中文做了编码处理,肉眼看着没问题,但实际发出的请求包已经变形。
解决:用URLEncoder.encode(cityName, "UTF-8")对中文城市名编码后再拼URL,服务端按UTF-8解码就恢复正常。同样,从SharedPreferences读出的城市名也要在拼接前编码。这条经验在改造成支持拼音搜索时一样适用。建议把编码逻辑固定在HttpUtil内部统一处理,而不是散落在每个调用方,这样后续维护不用在多个Activity里找哪里漏了编码。我后来给这套代码加城市搜索功能时,就因为只在Adaper里编码、网络层漏了一步,排查了快一个小时。
4.4 问题四:缓存不生效,每次冷启动都重新请求接口
现象:退出App再进来,天气数据仍然是重新拉取的,SQLite缓存形同虚设,每次打开都转圈加载。
原因:SQLite确实存了数据,但读之前缺少“缓存是否过期”的判断,或者写入时用的城市名和查询时不一致。比如写入缓存时城市名是“北京市”,查询时用的是“北京”,主键对不上就永远走网络。另一个容易被忽略的原因是:写入缓存用的键和查询用的键用了不同来源,一个来自输入框,一个来自列表选择,表面上都是中文名,内部可能有一个带了省后缀,另一个没有。
解决:在查询缓存时增加过期时间字段,存数据时记录System.currentTimeMillis(),超过10分钟视为过期;城市名统一经过同一个归一化函数处理后再做主键,比如统一去掉“市”“省”后缀,避免“北京市”和“北京”变成两条记录。这个小改动能把冷启动的加载时间从两三秒降到几百毫秒,体验提升非常明显。我给这个项目做国内城市适配时,特意把所有城市名都过了一遍归一化处理,列表和缓存才对齐。
4.5 问题五:模拟器运行正常,真机却拿不到定位城市
现象:天气功能在模拟器里一切正常,安装到手机上运行时,定位天气一直转圈,最后显示获取定位失败,其他城市手动查询却是好的。
原因:模拟器里的定位坐标是模拟器自带的默认位置,生态完整时不会暴露问题;真机上App缺少定位权限,或者没有在代码里做运行时权限申请。Android 6.0以上,定位权限需要在运行时动态申请,冷启动时弹窗询问,用户一旦拒绝,后续代码再拿locationmanager.getLastKnownLocation()就返回null。
解决:检查AndroidManifest.xml里有没有声明ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION,再检查MainActivity里有没有在onCreate里发起运行时权限申请。源码里用ActivityCompat.requestPermissions走的是动态申请逻辑,申请回调里要同时处理“同意”和“拒绝”两种分支,拒绝时降级到默认城市北京,不要直接崩溃。这条经验告诉我们:模拟器只能验证网络和UI逻辑,定位相关代码一定要上真机测。
5. 从源码到真机:Gradle导入、接口替换与多城市管理调试
5.1 工程导入与Gradle配置
拿到源码后,不要直接在AndroidStudio里双击打开目录,而是用File → Open选中项目根目录,等待Gradle同步。第一次构建会下载依赖,耗时取决于网络环境,国内建议提前配好镜像仓库,在build.gradle的repositories里把google()和mavenCentral()替换成阿里云镜像,能省不少时间。
android { compileSdkVersion 33 defaultConfig { applicationId "com.example.weather" minSdkVersion 21 targetSdkVersion 33 } }minSdkVersion 21意味着Android 5.0以上的设备都能跑,覆盖了绝大多数存量机型。targetSdkVersion 33决定了运行时权限模型的严格程度,如果你手头的测试机是Android 10以上,需要注意源码里的动态权限申请代码是否完整。构建报错时先看两处:一是Gradle版本号是否和本机AndroidStudio匹配,二是compileSdkVersion本地是否已安装。AndroidStudio的SDK Manager里装一下对应的SDK Platform即可,一般不用动代码就能解决。
5.2 接口地址与KEY的替换位置
源码里的天气接口地址和私钥参数集中在HttpUtil或者一个独立的Constants类里,改包前需要替换成你自己申请到的接口凭证。常见做法是“城市ID加请求key”的方式调用公共天气服务,拿到JSON后,字段名和模型里的@SerializedName一一对应。替换时注意三个地方:接口域名、请求路径、key参数名,有的服务商把key叫作“apikey”或者“token”,参数名不同会导致401。
我的建议是别在第一遍就急着跑通全部功能,先只配置好一个城市,验证网络请求和数据展示,确认链路通了之后,再逐个打开多城市管理。多城市管理的入口在MainActivity,底部有一个城市列表,长按可以删除,点击加号会弹输入框添加城市。添加城市的逻辑要注意:输入框里可能是中文名也可能是拼音,源码里没有做中英文自动识别,需要靠接口自身的匹配能力。如果返回空列表,多半是城市名编码或者城市ID传参不对,对照第4章的排查思路走一遍。这个模块改起来最费心思的是城市列表的增删时机:添加成功后要刷新天气Fragment,当前展示的城市被删除时要自动切换,源码里这块处理得比较顺,值得学习它的事件回调写法。
5.3 真机调试时的网络定位
真机调试比模拟器多一个坑:手机和电脑不在同一网络时,访问内网服务会失败,用公网接口则不受影响。所以改包阶段建议先用公网接口,等到了联调阶段再用内网服务。测试时用AndroidStudio的Logcat过滤标签“OkHttp”,可以看到每次请求的耗时和状态码。403多半是key过期或参数名不对,404是城市ID不存在或接口路径错误,500是服务端问题,重试即可。
如果日志里看不到网络请求,先确认是不是Android 9的明文流量限制把请求拦了,这个拦在系统层面,Logcat往往只打一行W/System.err,非常隐蔽。还有一种情况:接口通了但JSON解析失败,页面没有数据,此时断点打在Gson解析那一行,看捕获的异常信息,比看堆栈更直观。真机上调试定位失败时,记得在开发者选项里把“仅定位模拟位置”关掉,有些机型默认开启这个选项,会导致真机一直在跑模拟坐标。这个小开关排查了我一个下午,记录下来提醒大家先查这个。
5.4 构建常见报错与处理
Gradle同步报错里最常见的是依赖下载失败,报错信息形如Could not resolve io.reactivex。这种问题要么是网络无法访问maven仓库,要么是仓库地址被墙。解决方式是换镜像源,或者下载源码包里自带的libs目录下的jar包手动引入。这份天气预报源码依赖项不算多,核心就是OkHttp、Gson、RecyclerView,实在不行可以把gradle依赖改成本地jar形式,减少外网依赖。
另一个高频报错是Manifest merger failed,原因是本地某个依赖库的minSdkVersion比项目的高。比如项目里写minSdkVersion 21,某个依赖要求24,合并时直接失败。解决方式是看报错提示具体是哪个库,把项目的minSdkVersion调高到依赖要求的版本,或者去掉那个库换一个低版本兼容的。这个思路适用于所有Android依赖冲突,不只是这个天气项目。
6. 二次开发思路:给天气小程序加一个生活指数卡片
把核心链路跑通后,可以试着给这个天气小程序加一个“生活指数”模块,这是最贴合天气预报场景的改造方向,也是完整覆盖“数据模型、网络请求、UI展示”三个层面的练习。实现思路不复杂:在WeatherData模型里增加一个LifeIndex内部类,字段包括穿衣建议、紫外线强度、运动指数;网络层在请求时追加一个可选的接口参数,或者在原JSON里直接取服务端已返回的指数字段;UI层在RecyclerView的item类型里增加一种“指数卡片”,放在实时天气卡片下方。
public static class LifeIndex { @SerializedName("dressing") private String dressing; // 穿衣指数 @SerializedName("uv") private String uv; // 紫外线强度 @SerializedName("sport") private String sport; // 运动指数 }适配器里新增一个LifeViewHolder,绑定三条文本,代码只有十几行。关键点在于index偏移量:RecyclerView当前位置多了一项,需要把“实时天气、指数卡片、逐日预报”三类item的下标换算重新算一遍,否则逐日预报会跟着错位。这个改造的核心难点不在写新代码,而在理解原有的position偏移逻辑,正好逼着你把上一章的代码真正读懂。
还有一条我从这个项目上得来的血泪经验:每次改完模型字段,记得先用接口返回的真实JSON做一次Gson解析验证,而不是只在界面上看效果。因为界面上没显示出来的字段,解析失败不会崩,但隐藏的字段错误会在下一次调用时变成空指针。从那以后,我每次做完接口对接,都强制走一遍“抓JSON → 跑Gson → 看日志”三步,确认无误再回到界面验证,这个习惯让我少调了很多次“看起来正常但一换城市就闪退”的bug。希望帮到你。
本文还有配套的精品资源,点击获取