ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android天气预报App开发实战:网络请求、JSON解析与缓存设计要点

2026/9/11 4:21:33 拓冰建站 浏览量
Android天气预报App开发实战:网络请求、JSON解析与缓存设计要点 简介面向安卓移动开发课程期末作业的天气预报应用完整源码适合本科及高职学生作为课程设计参考。项目内含天气信息获取、实时展示、地理位置切换、多日预报和折线图温度变化等功能覆盖网络请求、数据解析、界面适配等移动开发核心环节。资源包共177个文件以Java源码、XML布局、PNG图片资源为主并包含Gradle构建文件、配置文件及第三方天气库JAR包压缩后大小3.45MB目录组织规范导入开发工具后即可运行学习。目前已有1043人学习下载工程结构可作为课程设计或期末答辩的完整实例。通过该源码读者能够理解天气接口的调用与数据处理流程掌握城市选择交互、折线图绘制等常见实现方式并可在此基础上扩展生活指数、语音播报等新功能对系统梳理安卓开发知识具有较高参考价值。1. 为什么一份天气预报 App 源码能决定期末大作业的上限期末考试前一周你从网盘拖下来一份“天气预报App源码”打开发现 Activity 里塞了 500 行找城市和拼 URL 的逻辑ListView 滚动卡顿关了网络直接白屏。这不是个例——Android 期末大作业的评分重点从来不是“能不能显示温度”而是你的代码有没有分层、请求有没有异步、数据有没有缓存、异常有没有兜底。天气预报 App 是 Android 移动开发课程里最经典的期末项目因为它的功能边界清晰输入城市、请求天气、展示列表、刷新缓存。正好用来考察网络请求、JSON 解析、UI 适配和生命周期。如果你已经拿了别人源码别急着交先按下面这套把网络层、数据解析、UI 刷新和存储逻辑过一遍再谈高分。2. 从零搭建天气预报 App网络层与数据解析的选型2.1 天气数据源怎么选和风天气、高德天气还是第三方聚合做天气 App 第一件事不是写代码是选数据源。很多同学直接搜“免费天气 API”搜到一堆需要注册、每天限调用的第三方接口。我一般建议优先用和风天气的免费开发版或者高德开放平台的天气接口。两者都支持国内城市 ID 和经纬度查询返回 JSON 结构稳定适合期末大作业。数据源免费额度城市支持返回字段适合场景和风天气开发版每天 1000 次城市 ID、经纬度实时天气、逐小时、生活指数需要展示空气质量、湿度、风力的作业高德天气每日 10000 次需个人认证城市 adcode实时天气、未来 4 天预报只在应用内测、省去复杂字段解析OpenWeatherMap每分钟 60 次城市名、坐标天气、温度、风速英文界面、海外院校要求选好数据源后先把返回的 JSON 保存成文件用浏览器或者 Postman 直接请求一次看字段名。常见错误是文档里写tmp实际返回temp或者夜间温度叫tmp_min白天叫tmp_max解析前一定要拿真实响应比对。我写代码时会先建一个WeatherBean字段全部用SerializedName注解映射这样后端改了字段名只改一处。用 Android Studio 新建项目时建议选 Empty Activity语言选 Java 或 Kotlin。如果是 Java 项目记得勾选 AndroidXminSdk设 21 以上否则一些 JSON 解析库会有兼容问题。下面从网络层开始搭。2.2 用 Retrofit OkHttp 封装天气请求的最小可运行代码Android 移动开发大作业里网络请求最常见的就是用 Retrofit它本质是对 OkHttp 的一层封装把接口定义、请求参数、返回解析全部整合到一个接口文件里。在app/build.gradle中加上依赖implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.9.0定义一个天气服务接口public interface WeatherApi { GET(v3/weather/now.json) CallWeatherResponse getNowWeather( Query(key) String key, Query(location) String cityId ); }创建 Retrofit 实例时设置基础 URL 和日志拦截器OkHttpClient client new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(https://devapi.qweather.com/) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); WeatherApi api retrofit.create(WeatherApi.class);这里GET路径要和数据源文档完全一致Query参数名必须是接口要求的字段。日志拦截器把请求地址、响应 JSON 打到 Logcat后面排查“为什么温度解析不出来”特别有用。注意这里用了CallWeatherResponse同步调用用execute()异步用enqueue()大作业里必须让调用发生在子线程不能直接在 UI 线程调。2.3 JSON 解析的坑字段缺失、嵌套结构、日期格式天气接口返回的 JSON 几乎都是嵌套结构比如now对象里才有温度、体感温度根上的code是状态码。Gson 解析时如果字段名对不上会得到 null。下面是响应示例{ code: 200, updateTime: 2024-06-01T18:35:0008:00, now: { temp: 28, feelsLike: 30, text: 晴, windDir: 东南风, windScale: 3级, humidity: 45 } }对应的实体类写成public class WeatherResponse { public String code; public String updateTime; public Now now; public class Now { public String temp; public String feelsLike; public String text; public String windDir; public String windScale; public String humidity; } }这里一个容易扣分的地方是日期格式。updateTime是 ISO 8601不是yyyy-MM-dd HH:mm。如果要显示“更新时间”得用SimpleDateFormat转一遍SimpleDateFormat in new SimpleDateFormat(yyyy-MM-ddTHH:mm:ssXXX, Locale.ENGLISH); SimpleDateFormat out new SimpleDateFormat(MM-dd HH:mm, Locale.CHINA); try { Date date in.parse(response.updateTime); textViewUpdate.setText(更新于 out.format(date)); } catch (ParseException e) { textViewUpdate.setText(更新时间未知); }注意XXX表示带时区的 ISO 8601Android 7.0 以下对三字母时区支持有问题稳妥做法是parse失败后按字符串前 10 位截断直接用substring(0, 16).replace(T, )展示。解析时任何一个字段为空都建议兜底显示“--”而不是让 App 崩溃。2.4 用一个 Repository 类统一管理请求与缓存大作业里数据来源除了网络还有内存和文件。可以在WeatherRepository中封装public class WeatherRepository { private final WeatherApi api; private WeatherResponse lastResponse; public WeatherRepository(WeatherApi api) { this.api api; } public void loadWeather(String cityId, CallbackWeatherResponse callback) { if (lastResponse ! null) { callback.onCache(lastResponse); } api.getNowWeather(your_key, cityId).enqueue(new retrofit2.Callback() { Override public void onResponse(CallWeatherResponse call, retrofit2.ResponseWeatherResponse response) { if (response.body() ! null) { lastResponse response.body(); callback.onRemote(response.body()); } } Override public void onFailure(CallWeatherResponse call, Throwable t) { callback.onFailure(t); } }); } public interface CallbackT { void onCache(T data); void onRemote(T data); void onFailure(Throwable t); } }这样 UI 层不用关心数据是来自网络还是缓存。回调接口里定义onCache和onRemoteActivity 里可以先用缓存数据刷一次屏等网络数据回来再刷新体验更好。这个 Repository 在答辩时可以讲“用了单一职责原则”。lastResponse是内存缓存进程被杀就没了但已经比每次都空白好。3. 大作业 UI 实现布局、刷新与多城市管理的必做细节3.1 用 RecyclerView 做城市列表 天气卡片避免 ScrollView 嵌套天气预报 App 最常见的界面是顶部一个当前城市大卡片下面一个未来 7 天列表。很多初学者把整个页面写成一个垂直ScrollView里面再包一个ListView或RecyclerView运行不崩溃但滑动卡顿答辩时很容易被问“为什么不用 RecyclerView”。正确做法是外层用SwipeRefreshLayout里面只放一个RecyclerView所有天气信息都做成条目或者用两行不同布局的 item。我一般这样设计androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:idid/swipeRefresh android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/recyclerWeather android:layout_widthmatch_parent android:layout_heightmatch_parent android:clipToPaddingfalse android:padding12dp / /androidx.swiperefreshlayout.widget.SwipeRefreshLayout在Activity中设置布局管理器RecyclerView recycler findViewById(R.id.recyclerWeather); LinearLayoutManager manager new LinearLayoutManager(this); recycler.setLayoutManager(manager); weatherAdapter new WeatherAdapter(dataList); recycler.setAdapter(weatherAdapter);当页面只有一组数据时RecyclerView.setNestedScrollingEnabled(false)是过渡方案不推荐在ScrollView里用。如果有“今天”和“未来七天”两种卡片用getItemViewType()返回不同布局即可不要写两个列表。3.2 SwipeRefreshLayout 下拉刷新和生命周期绑定下拉刷新几乎是天气预报 App 的标配功能但很多人只写了setOnRefreshListener忘了在网络请求结束后关闭刷新动画结果进度圈一直转操作被扣分。标准写法是SwipeRefreshLayout refreshLayout findViewById(R.id.swipeRefresh); refreshLayout.setColorSchemeResources( android.R.color.holo_blue_bright, android.R.color.holo_green_light ); refreshLayout.setOnRefreshListener(() - { loadWeather(currentCityId, true); });在loadWeather里无论成功还是失败finally中都要调refreshLayout.setRefreshing(false)。网络请求放在onCreate里触发时如果页面还没显示完建议用Lifecycle把请求控制到onStart以后避免在onCreate中执行异步操作时 Activity 已销毁导致的内存泄漏。简单做法是加一个isDestroyed()判断回调回来后再更新 UI。3.3 多城市存储SharedPreferences 还是 SQLite大作业如果只支持一个城市功能可能太薄。加“我的城市列表”是常见加分项。城市列表的存储有两个选择SharedPreferences适合少量字符串数组SQLite适合还要记录每个城市最后更新时间、自定义备注的场景。对比SharedPreferencesSQLite适合数据量几十条以内上百条以上读写难度简单要写 SQL类型安全只能存字符串/基本类型支持多字段期末答辩容易说清楚更能体现存储设计能力选择不重要重要的是统一接口。我用 SharedPreferences 存 JSON 字符串private static final String PREF_NAME weather_city; private static final String KEY_CITY_LIST city_list; public void saveCityList(ListString cityList) { Gson gson new Gson(); String json gson.toJson(cityList); getSharedPreferences(PREF_NAME, MODE_PRIVATE) .edit() .putString(KEY_CITY_LIST, json) .apply(); } public ListString loadCityList() { String json getSharedPreferences(PREF_NAME, MODE_PRIVATE) .getString(KEY_CITY_LIST, []); Gson gson new Gson(); Type type new TypeTokenListString() {}.getType(); return gson.fromJson(json, type); }逻辑说明MODE_PRIVATE保证只有本应用能读apply()是异步写盘避免 UI 卡顿。如果增加城市时没有先去重列表会出现重复项所以在保存前用HashSet过滤加载后也要对空字符串做过滤防止删除城市时残留。3.4 用 ItemTouchHelper 做城市排序持久化如果城市列表还需要保存每个城市的排序权重比如用户长按拖动排序SharedPreferences存 JSON 已经不够直观。可以用RecyclerView的ItemTouchHelper实现拖动再把顺序写回列表。核心代码ItemTouchHelper touchHelper new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { Override public boolean onMove(RecyclerView rv, RecyclerView.ViewHolder vh, RecyclerView.ViewHolder target) { int from vh.getAdapterPosition(); int to target.getAdapterPosition(); adapter.moveItem(from, to); return true; } Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { } }); touchHelper.attachToRecyclerView(recycler);moveItem方法里先操作ArrayList再notifyItemMoved最后调用saveCityList(list)持久化。这个功能虽然只在部分大作业加分项里出现但一旦做出来就能把“数据持久化”和“交互设计”两个点都踩中。4. 要是拿别人的源码先盯这四个位置权限、线程、缓存、包名4.1 AndroidManifest.xml 权限和 app 签名不一致导致装不上期末大作业从 GitHub 或网盘下载源码后第一件事不是打开 Activity而是打开AndroidManifest.xml看权限。天气预报 App 必须声明INTERNET权限但 Android 6.0 以后如果请求的是 HTTP 明文地址还要在application标签加android:usesCleartextTraffictrue或者在network_security_config.xml里放行对应域名。常见表现是代码没报错但加载天气时一直走onFailure看日志是CLEARTEXT communication to xxx not permitted。如果你改过包名还要注意很多源码里写死了applicationId比如在build.gradle里defaultConfig { applicationId com.example.weather }直接改成自己的域名前缀否则装上一台手机后下次再装另一个项目就会提示“签名不一致”。4.2 子线程更新 UI不要把网络请求写在 main 线程初学者最容易犯的错是在onCreate里直接写api.getNowWeather(...).execute()因为execute()是同步的会抛NetworkOnMainThreadException。拿来的源码如果没做异步一般用的还是Thread Handlernew Thread(() - { CallWeatherResponse call api.getNowWeather(key, cityId); try { ResponseWeatherResponse response call.execute(); WeatherResponse body response.body(); runOnUiThread(() - { if (body ! null 200.equals(body.code)) { updateUI(body.now); } }); } catch (IOException e) { runOnUiThread(() - toast.setText(网络失败)); } }).start();这里runOnUiThread是简单方案如果源码用了 Java 8 的 lambda别忘了在compileOptions里开启 Java 8 支持。更好的做法是改成Retrofit的enqueue回调本身就位于主线程不用手动切。4.3 缓存策略如何避免每次冷启动都闪着空页面很多天气预报 App 源码只有“请求—显示”没有“上次数据”。冷启动时屏幕先是白底等网络回来才出内容答辩时体验很差。加分做法是在请求前先读本地文件File cacheFile new File(getCacheDir(), last_weather.json); if (cacheFile.exists()) { String json FileUtils.readFileToString(cacheFile, UTF-8); WeatherResponse cached new Gson().fromJson(json, WeatherResponse.class); updateUI(cached.now); }网络请求成功后把响应体原样写入这个文件。这样即使用户打开时没网也能先看到上次天气再提示“数据已过期”。文件缓存只需要处理一个异常writeBytes过程中 App 被杀会导致文件不完整所以写入时要先写.tmp再改名。4.4 改名和打包从「别人的源码」变成「你自己的大作业」拿到别人的源码后最忌讳的是只改标题栏文字就交。Android Studio 里能查到的项目名、包名、app_name以及代码里的MainActivity注释作者全部要改掉。操作步骤右键app/java/com/xxx→Refactor-Rename选择“Rename package”然后手动改build.gradle里的applicationId。改完后用Build - Clean Project再运行到模拟器上确认桌面图标名称也变了。另一个高频问题是项目用了旧的 Android Support Library而你的 Android Studio 默认新建项目用的是 AndroidX。如果导入源码后编译报一堆android.support.*找不到说明源码没有迁移到 AndroidX。在gradle.properties里加上两行android.useAndroidXtrue android.enableJetifiertrue然后同步再把import android.support.*全部替换成import androidx.*。这个替换不费脑但容易漏我一般用 Android Studio 的Refactor - Migrate to AndroidX一键处理。4.5 常见编译与安装错误排查表答辩前真机调试下面的错误出现概率很高可以对照排查错误信息原因解决Tag number over 30 is not supportedD8 编译失败通常是依赖冲突升级 Android Gradle Plugin 到 3.6或./gradlew clean后重启 Android StudioExecution failed for task :app:mergeExtDexRelease方法数超限开启 multidex或在build.gradle里去无用依赖INSTALL_FAILED_UPDATE_INCOMPATIBLE应用签名或包名冲突卸载旧应用或重新改包名CLEARTEXT communication to xxx not permittedAndroid 9 默认禁止明文 HTTP在 manifest 里声明usesCleartextTraffic或添加网络安全配置connect timeout网络连接慢或地址错误抓包检查文档 baseUrl 是否填对增加超时时间5. 高分答辩的关键给源码加上这 6 个注释级亮点答辩时老师不是看功能而是看你敢不敢说清楚每个类为什么存在。我建议给源码加 6 处注释每处都能讲 10 秒但价值很高网络层用Retrofit OkHttp注释写明“通过拦截器统一打印日志方便联调”实体类加SerializedName注释写明“防止后端字段名变更影响 UI 层”loadWeather方法里加“缓存优先请求后回写”注释这是体现数据一致性思维的关键UI 层用ViewModel LiveData或简单的Callback接口注释写明“Activity 销毁后不会再更新 UI”城市列表存储封装成独立类注释写“所有读写入口统一避免到处操作 SharedPreferences”在onFailure中捕获异常并显示错误码注释写“不让崩溃掩盖网络错误”。答辩时可以准备一条命令行用来证明缓存生效adb shell am force-stop com.example.weather adb shell am start -n com.example.weather/.MainActivity先在飞行模式下启动 App看到上一次天气数据直接显示再关掉飞行模式下拉刷新数据更新。这个过程能把“网络层、缓存、异常处理”三个评分点一次演示完。最后一个可以立刻动手的细节在updateTime的显示函数里加上“更新于 X 分钟前”的推断网络异常时保留上次成功时间并用灰色文字提示误差。这一处小逻辑成本远低于做动画但答辩时很能说明你考虑了真实用户体验。本文还有配套的精品资源点击获取