资讯动态

Android天气预报小程序源码解析:从网络请求到UI展示完整链路

发布时间:2026/10/17 7:11:41 来源:尧图企业网站定制
简介这是一份基于Android Studio实现的天气预报小程序完整源码包面向移动开发入门者及需要参考完整项目结构的Android学习者。项目围绕天气查询功能展开涵盖Retrofit网络请求、Gson数据解析、异步回调更新UI等关键环节并融入下拉刷新、数据缓存、异常处理等实用设计适合直接导入Android Studio运行调试也可作为课程设计或毕业设计的参照模板。压缩包共1260个文件大小约9.76MB文件类型包含java源码、xml布局与配置、json数据、gradle构建脚本以及flat、dex、class等编译中间产物和最终apk便于按开发流程对照理解。该资源已有3876人学习下载配套说明文档梳理了从项目初始化到功能优化的完整步骤。通过学习这份源码能够快速掌握Android调用第三方天气API的方法并在此基础上扩展城市切换、多日预报等功能。1. 天气预报小程序源码一个打开就能跑的 Android Studio 工程核心链路完整且能改拿到这份源码之后你几乎不需要做任何多余操作用 Android Studio 打开工程等 Gradle 同步完成直接 Run 到模拟器上就能看到当前城市的实时温度和未来几天的天气列表。整个项目没有依赖第三方网络框架网络请求基于 HttpURLConnectionJSON 解析用的是 Android 自带的 org.json 包UI 层由 RecyclerView 承载列表数据。它不像企业级项目那样层层封装但恰恰是一个适合课程设计、毕业设计或者第一次接触 Android 网络请求的开发者去拆解的最小完整闭环从输入一个城市名到屏幕上出现温度数字中间所有的步骤都是可见的、可以打断点去跟的。2. 项目结构拆解与技术选型为什么不依赖框架反而更适合二次开发2.1 源码目录分工activity、adapter、utils、model 各司其职先把压缩包解压然后在 Android Studio 里通过 Open 选中工程根目录等 Gradle 同步结束后重点看app/src/main下面这五个区域。java 目录里按包名划分了职责。activity 包里有 MainActivity 和 SearchCityActivity 两个界面前者负责展示天气主页包括城市名、实时温度、湿度、风向和七天预报列表后者是一个搜索城市页面用户输入城市名后点击搜索结果会保存下来并回传。adapter 包里是 WeatherAdapter它承担 RecyclerView 列表条目的数据绑定。utils 包中 HttpUtil 负责发起 GET 请求JsonParseUtil 负责把返回的 JSON 字符串解析成对象。model 包里是 WeatherBean 实体类定义了字段和内部类 ForecastItem——这个类虽然在代码量上不起眼但它是数据解析和 UI 绑定之间最关键的桥梁。res 目录里面对应着三个值得关注的模块。layout 文件夹下有三个 xml 文件activity_main.xml 主界面用的竖向 LinearLayout从上到下依次放着城市名、温度、天气现象、湿度、风向和列表activity_search.xml 搜索页只有一个输入框加一个搜索按钮item_weather.xml 是七天预报列表的单条布局左右排布日期、天气和温度区间。drawable 文件夹里存的是天气状态的矢量图图标这种格式在不同屏幕密度下不会失真也不需要为每种分辨率单独切图。values 文件夹下 colors.xml 定义了主题配色strings.xml 集中管理所有文案后期如果要把应用改成英文界面只需要改这个文件。还有 AndroidManifest.xml 里的两个配置需要特别留意。第一个是权限声明uses-permission android:nameandroid.permission.INTERNET/这是应用访问网络的基本前提缺失时网络请求会在运行时抛异常。第二个是android:usesCleartextTraffictrue这是 Android 9 之后引入的安全策略开关不开启的话应用会默认禁止 http 明文请求导致请求在没到达服务器之前就被系统拦截。2.2 技术选型HttpURLConnection 和原生 JSON 这套组合的优势现在写 Android 的网络请求默认答案基本是 Retrofit 加 OkHttp 加 Gson 三件套。但这个项目刻意避开这些选择用 HttpURLConnection 加 org.json 手动解析。第一次看到会觉得有些复古跑起来之后会发现这种组合在这种体量的项目里反而更顺手。第一个优势是零额外依赖。打开工程根目录的 build.gradledependencies 里只有 AndroidX 的 appcompat、recyclerview 和 constraintlayout。这意味着整个项目同步速度非常快不会出现因为拉取某个第三方库版本冲突导致的玄学问题。对初学者来说排除的依赖越少出问题的环节就越少。第二个优势是链路透明。用 OkHttp 发请求时背后经过拦截器链、连接池复用、Dispatcher 调度那一整套机制任何一个环节出问题都不好定位。而 HttpURLConnection 的调用步骤是固定的openConnection、setRequestMethod、setConnectTimeout、getInputStream每一步都有清晰的语义断点打下去能看到请求状态的完整变化这比黑匣子式的框架更适合理解底层的网络模型。第三个优势是改造成本低。这个项目接的是公共天气接口如果你之后想换成另一个天气数据服务商只需要改 HttpUtil 里拼的 URL 和 JsonParseUtil 里的字段名。UI 层、适配器层都不需要动。如果你最后还是要换 Retrofit那么把 HttpUtil 的 get 方法替换成 Retrofit 的 Call 对象对业务层的影响也局限在一个方法内部。3. 核心模块拆解天气数据从请求发起到渲染进界面的完整链路3.1 HttpUtilGET 请求封装与超时参数设定utils 包里的 HttpUtil 是网络请求的入口整个工程只有一个静态方法 get没有多余的设计调用方传入一个拼接好的 URL 字符串返回服务器响应体字符串。核心代码是这样的public class HttpUtil { private static final int CONNECT_TIMEOUT 5000; private static final int READ_TIMEOUT 8000; public static String get(String url) { HttpURLConnection connection null; try { URL requestUrl new URL(url); connection (HttpURLConnection) requestUrl.openConnection(); connection.setRequestMethod(GET); connection.setConnectTimeout(CONNECT_TIMEOUT); connection.setReadTimeout(READ_TIMEOUT); connection.setRequestProperty(Accept, application/json); int responseCode connection.getResponseCode(); if (responseCode HttpURLConnection.HTTP_OK) { InputStream is connection.getInputStream(); BufferedReader reader new BufferedReader(new InputStreamReader(is, UTF-8)); StringBuilder result new StringBuilder(); String line; while ((line reader.readLine()) ! null) { result.append(line); } reader.close(); is.close(); return result.toString(); } } catch (Exception e) { e.printStackTrace(); } finally { if (connection ! null) { connection.disconnect(); } } return null; } }代码里两个超时参数需要理解清楚。connectTimeout 是建立 TCP 连接的等待时间5 秒意味着在 WiFi 信号弱或者服务器 IP 不通的情况下最多等 5 秒就放弃readTimeout 是连接建立之后、等待服务器返回数据的超时时间8 秒比较合理地覆盖了天气接口这种数据量小、响应快的 GET 请求。这两个参数如果设得太小在移动网络下容易误判请求失败设得太大则会让用户界面长时间停留在加载状态。5 和 8 是实践中比较稳妥的起点后面可以按实际场景微调。finally 块里的 disconnect 是为了释放连接资源这个细节容易被新手漏掉但它的意义恰好是避免长时间运行的应用积累大量未关闭的连接导致崩溃。请求失败时返回值是 null调用方通过判断 null 来决定是重试还是提示用户而不是在工具类内部弹错误提示——工具类保持纯粹性把决策权留给上层这是我在工程里一贯的拆分习惯。3.2 URL 拼接与中文城市名编码问题天气接口的请求地址通常是这样拼出来的String baseUrl https://xxx.com/weather; String url baseUrl ?city cityName; String response HttpUtil.get(url);这段代码在 cityName 是英文时没有任何问题但一旦用户输入的是“上海”这样的中文请求就会失败。因为 HTTP 协议要求 URL 里的字符必须是 ASCII 可打印字符中文字符不在这个范围内服务器收到这种请求要么返回 400要么直接拒收。解决方法是先编码再拼接String encodeName URLEncoder.encode(cityName, UTF-8); String url baseUrl ?city encodeName;用 URLEncoder 对城市名编码后中文会被转成%E4%B8%8A%E6%B5%B7这类百分号编码服务器端会识别这是合法的 URL 参数。这也是我在第 4 章避坑环节会再次提到的一个高频问题可以在搜索页提交之前统一做编码处理也可以用拦截器在请求层统一处理。3.3 JsonParseUtil把 JSON 字符串逐层映射成 WeatherBean服务端返回的 JSON 结构一般长这样一个根节点下挂着状态码和业务数据data 里既有当前天气字段也包含一个 forecast 数组用于存放七天预报{ code: 200, data: { city: 北京, temperature: 23, weather: 晴, humidity: 40, wind: 东南风, forecast: [ {date: 周一, dayWeather: 晴, nightWeather: 多云, high: 26, low: 18}, {date: 周二, dayWeather: 阴, nightWeather: 小雨, high: 22, low: 16} ] } }JsonParseUtil 的 parseWeather 方法做的事情就是把这个结构逐层拆开填充到 WeatherBean 里public static WeatherBean parseWeather(String json) { WeatherBean bean new WeatherBean(); try { JSONObject root new JSONObject(json); JSONObject data root.getJSONObject(data); bean.setCity(data.optString(city)); bean.setTemperature(data.optString(temperature)); bean.setWeather(data.optString(weather)); bean.setHumidity(data.optString(humidity)); bean.setWind(data.optString(wind)); JSONArray forecastArray data.getJSONArray(forecast); ListWeatherBean.ForecastItem itemList new ArrayList(); for (int i 0; i forecastArray.length(); i) { JSONObject itemObj forecastArray.getJSONObject(i); WeatherBean.ForecastItem item new WeatherBean.ForecastItem(); item.setDate(itemObj.optString(date)); item.setDayWeather(itemObj.optString(dayWeather)); item.setNightWeather(itemObj.optString(nightWeather)); item.setHigh(itemObj.optString(high)); item.setLow(itemObj.optString(low)); itemList.add(item); } bean.setForecastList(itemList); } catch (JSONException e) { e.printStackTrace(); } return bean; }这里刻意使用 optString 而不是 getString两者的行为差别在字段缺失时非常明显optString 返回空字符串解析流程继续执行getString 直接抛 JSONException整个解析过程中断。对于天气接口这种数据来自第三方服务商的场景结构稳定性不能完全保证用 optString 解析会让个别字段的缺失表现为界面某处显示空白而不是导致整个应用崩溃。关键字段上我才会用 getString用异常来暴露问题。catch 块里目前只是打印堆栈但没有在业务层做错误暴露。所以调用方拿回 bean 之后代码里最好养成检查一次 city 字段的习惯。如果 city 是空的说明解析过程没有拿到有效数据此时应该在 UI 层提示用户而不是继续去绑定列表。这个习惯在二次开发换数据源的时候尤其重要因为新服务的 JSON 字段名和嵌套结构很可能是另一种风格。3.4 MainActivity主线程发起、子线程请求、UI 线程刷新的协作流程主界面业务逻辑并不复杂但有一个 Android 的硬性要求必须遵守网络请求不能跑在主线程。所以 MainActivity 的做法是开一个子线程去请求数据拿到结果后再切回主线程更新界面。public class MainActivity extends AppCompatActivity { TextView tvCity, tvTemperature, tvWeather, tvHumidity, tvWind; RecyclerView recyclerView; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvCity findViewById(R.id.tv_city); tvTemperature findViewById(R.id.tv_temperature); tvWeather findViewById(R.id.tv_weather); tvHumidity findViewById(R.id.tv_humidity); tvWind findViewById(R.id.tv_wind); recyclerView findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); String cityName getSharedPreferences(weather_pref, MODE_PRIVATE) .getString(city, 北京); loadWeather(cityName); } private void loadWeather(final String cityName) { final String url https://xxx.com/weather?city URLEncoder.encode(cityName, UTF-8); new Thread(new Runnable() { Override public void run() { String json HttpUtil.get(url); if (json null) { runOnUiThread(new Runnable() { Override public void run() { Toast.makeText(MainActivity.this, 请求失败请检查网络, Toast.LENGTH_SHORT).show(); } }); return; } final WeatherBean bean JsonParseUtil.parseWeather(json); runOnUiThread(new Runnable() { Override public void run() { if (bean.getCity() ! null !bean.getCity().isEmpty()) { bindData(bean); } else { Toast.makeText(MainActivity.this, 数据解析失败, Toast.LENGTH_SHORT).show(); } } }); } }).start(); } private void bindData(WeatherBean bean) { tvCity.setText(bean.getCity()); tvTemperature.setText(bean.getTemperature() °C); tvWeather.setText(bean.getWeather()); tvHumidity.setText(湿度 bean.getHumidity() %); tvWind.setText(风向 bean.getWind()); WeatherAdapter adapter new WeatherAdapter(bean.getForecastList()); recyclerView.setAdapter(adapter); } }关于这段代码有三个要点需要说明。new Thread 创建的匿名线程负责网络请求和 JSON 解析这两步都是耗时操作放在子线程执行是为了避免主线程阻塞runOnUiThread 的作用是把 UI 更新动作切回主线程Android 不允许在子线程里直接修改 View否则会报 CalledFromWrongThreadException。SharedPreferences 中保存的城市名是搜索页写入的首次安装没有这个值时取默认的“北京”保证第一次启动用户也能看到数据。3.5 WeatherAdapter列表条目的绑定与天气图标兜底映射七天预报的列表展示由 RecyclerView 的适配器完成逻辑就是普通的 ListView/RecyclerView 模式重点在看 getWeatherIcon 里的映射策略。public class WeatherAdapter extends RecyclerView.AdapterWeatherAdapter.ViewHolder { private ListWeatherBean.ForecastItem mList; public WeatherAdapter(ListWeatherBean.ForecastItem list) { this.mList list; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_weather, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(ViewHolder holder, int position) { WeatherBean.ForecastItem item mList.get(position); holder.tvDate.setText(item.getDate()); holder.tvDayWeather.setText(item.getDayWeather()); holder.tvNightWeather.setText(item.getNightWeather()); holder.tvHigh.setText(item.getHigh() °); holder.tvLow.setText(item.getLow() °); holder.ivWeatherIcon.setImageResource(getWeatherIcon(item.getDayWeather())); } private int getWeatherIcon(String weather) { switch (weather) { case 晴: return R.drawable.ic_sunny; case 多云: return R.drawable.ic_cloudy; case 小雨: return R.drawable.ic_rain; case 雪: return R.drawable.ic_snow; default: return R.drawable.ic_cloud; } } Override public int getItemCount() { return mList null ? 0 : mList.size(); } class ViewHolder extends RecyclerView.ViewHolder { TextView tvDate, tvDayWeather, tvNightWeather, tvHigh, tvLow; ImageView ivWeatherIcon; ViewHolder(View itemView) { super(itemView); tvDate itemView.findViewById(R.id.tv_date); tvDayWeather itemView.findViewById(R.id.tv_day_weather); tvNightWeather itemView.findViewById(R.id.tv_night_weather); tvHigh itemView.findViewById(R.id.tv_high); tvLow itemView.findViewById(R.id.tv_low); ivWeatherIcon itemView.findViewById(R.id.iv_weather_icon); } } }switch-case 的映射是写死的能覆盖晴、多云、小雨、雪这几种常见状态其他没匹配到的状态统一用 ic_cloud 兜底。这样处理的好处是服务端返回了不在映射范围内的天气描述时界面不会崩溃只会显示默认云朵图标。真实业务里的天气现象远不止这几种“晴转多云”“雷阵雨”这类组合描述也经常出现后面做二次开发时如果想精细控制图标可以把 switch-case 升级成用 contains 判断的 if-else 链或者写一个独立的 WeatherIconMapper 类。4. 编译运行与常见问题排查把源码变成模拟器上能看到的天气数据4.1 环境要求与三步编译运行流程这个项目的编译条件不苛刻。本机安装 Android Studio 任一较新版本即可Gradle 同步时如果提示缺插件按提示点 Sync Now 让它自动拉取。第一步是等待 Gradle Sync 完成过程中 Android Studio 会下载项目依赖的 AndroidX 库网络状况正常的话一分钟左右能结束。第二步确认运行设备。没有实体机的情况下直接用 Android Studio 自带的模拟器建议创建 API 30 左右的系统镜像这个版本对项目的兼容性稳。模拟器本身默认能访问宿主机网络但如果观察到请求一直超时先去模拟器设置里检查网络开关有时候镜像初始化时网络默认是断开的这是一个很容易忽略的环境变量。第三步是点击 Run 按钮部署。安装完成后 MainActivity 自动拉起并调用 loadWeather 方法在数据返回之前界面停留在初始状态。请求顺利的话两到三秒内温度、湿度和列表都会出现。如果界面一直空白或者弹 Toast去 Logcat 窗口看有没有红色异常日志这是最直接的定位入口。4.2 运行时最常见的三个异常与排查思路NetworkOnMainThreadException 是第一个高频异常。现象是应用一启动就闪退日志明确指向某个网络请求代码行。原因很简单主线程被禁止执行网络阻塞操作。解决方式是把 HttpUtil.get 这个方法放回子线程执行参考第 3 章 MainActivity 的写法问题立刻消失。UnknownHostException 或超时是第二个高频异常。现象是请求发出去了但迟迟没有响应最终回调失败。三种常见原因manifest 里少了 INTERNET 权限导致系统直接拒绝拼接的 URL 写错了域名模拟器本身没有网络。排查顺序按成本从低到高先确认权限再确认日志里的完整 URL最后在模拟器里用自带浏览器访问同一个地址测试网络连通性。JSONException 解析失败是第三个高频问题。现象是请求成功返回了但页面上城市名和温度都是空的。两种可能一种是字段名对不上服务端返回的都是英文名而解析代码里写的是拼音另一种是根节点结构变了原本有 data 包裹层新接口没有在调用 getJSONObject(data) 时就会抛异常。遇到这类问题不要猜直接把接口返回的原始 JSON 打印到 Logcat 里和解析代码逐层对照第一眼就能找出差异。4.3 避坑指南天气项目里几个经典翻车现场第一坑中文城市名没有做 URL 编码。现象是在搜索页输入“上海”后请求返回结果一直是空或者 400 错误换成英文城市名却正常。原因是 HTTP URL 不允许直接出现中文字符服务端无法理解。解决方法是在拼接 URL 前先编码String encodeName URLEncoder.encode(cityName, UTF-8); String url https://xxx.com/weather?city encodeName;第二坑保存搜索记录时把空字符串写进了 SharedPreferences。现象是应用重启后默认城市显示不确定有时候甚至出现空标题。原因分析搜索页点击按钮时没有校验输入框内容空字符串被当成城市保存下来了。解决方法是保存之前判断非空为空则直接忽略本次操作沿用上一次的旧值。第三坑usesCleartextTraffic 没有开启。现象是接口地址在浏览器里能直接访问但应用里一直提示请求失败。原因是调试阶段很多天气服务商提供的是 http 明文地址Android 9 之后默认禁止明文流量应用层面就把请求拦截了。解决方式在 manifest 的 application 节点加上android:usesCleartextTraffictrue或者把接口地址升级为 https。第四坑RecyclerView 没设置 LayoutManager。现象是列表数据解析正常适配器也绑定好了但页面上什么都看不见。原因是列表控件缺一个排列规则来告诉它条目怎么摆放。解决方式给 recyclerView 设置 LinearLayoutManager一行代码的事但忘了就是黑屏跑起来之后表现为数据加载成功但界面空白。5. 二次开发方向把天气 Demo 改造成你自己的应用5.1 换数据源的替换范围如果你要把天气数据服务从当前默认接口换成其他服务商需要改动的文件非常集中。第一处是 HttpUtil 的调用方 MainActivity 里拼接的 URL按照新服务商提供的接口文档重新组织参数和请求头通常需要附带一个身份标识用的 key。第二处是 JsonParseUtil 里的字段映射对照新接口的返回样例把原来的 optString 取值参数替换成新字段名。这两处改完UI 层和适配器层基本不用动。如果新接口额外提供了 PM2.5、紫外线指数这类字段在 WeatherBean 加两个字段、在解析方法里补两行、再在 activity_main.xml 里增加 TextView 并绑定即可整个扩展路径是直线式的。5.2 城市管理列表的实现思路把单城市切换升级成多城市列表的思路值得单独说一下。可以在搜索页基础上新建一个城市管理页用 RecyclerView 展示已经添加的城市每条数据后面带一个删除按钮。城市列表以字符串形式存进 SharedPreferences用逗号分隔多个城市名。MainActivity 启动时读取这份列表默认加载第一个城市。切换城市时把目标城市名写入 Preferences然后重新调用 loadWeather。整个流程不需要引入任何数据库框架SharedPreferences 在这个数据量级别已经足够可靠。注意在城市管理页每次操作完数据后都需要刷新列表并同步写入 Preferences避免重启后数据不一致。5.3 改造后的自测清单每次改完建议走一遍这四步验证。第一步确认接口地址能在浏览器里直接访问排除服务端不可达的问题第二步在 JsonParseUtil 里用一段真实的 JSON 样例数据手动调用 parseWeather 方法确认字段映射准确无误第三步跑一次模拟器确认主页温度、天气、列表三个区域都有数据展示第四步测试城市切换功能确保切换后既刷新了主页也保存了持久化数据。我之前在这个工程上做二次开发时翻过一次车为了适配新天气服务的字段结构顺手把 JSON 解析的 catch 块删掉了结果服务器返回的数据结构稍有异常整个应用直接崩溃。后来每次改动解析逻辑我都先拿一段真实的 JSON 数据放在本地跑一遍单元测试跑通了再进模拟器这个习惯帮我节省了大量调试时间。从那以后我每次换数据源都会强制走一遍接口校验流程希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑