简介:这份资源面向具备一定 Java 与 Android 基础的移动开发学习者,提供一套基于 Android Studio 的天气预报小程序完整源码,帮助读者理解从项目搭建、网络请求到界面数据绑定的完整开发链路。压缩包共 1260 个文件,约 9.76MB,以 509 个 flat、194 个 dex、192 个 class 等编译产物为主,辅以 33 个 java 源码、76 个 xml 布局与配置、120 个 json 数据文件及 21 个 png 图片资源,另含 gradle 构建脚本与 apk 安装包,便于直接运行对照。源码围绕 Retrofit 网络请求、OpenWeatherMap 接口调用、异步响应处理与 UI 控件绑定展开,并涉及进度条、下拉刷新、本地缓存与天气图标加载等优化点。已有 3876 人学习下载,适合作为 Android 入门练手与课程设计参考。
1. 一个天气小程序源码,为什么值得你花半小时拆一遍
上周有个做后端的哥们问我,想给自家小团队做个内部用的天气看板,问我有没有现成的 Android 源码能直接改。我第一反应是:这种需求看着简单,真动手写起来,网络请求、JSON 解析、定位权限、UI 状态切换,四件事凑一块儿就够新手喝一壶的。他手上正好有一份基于 AndroidStudio 的天气预报小程序源码,我拿来拆了两天,结论是——这东西对练手和二次开发都挺友好,但前提是你得知道它哪些地方是“能跑就行”、哪些地方是“真上生产得改”。
这份源码解决的核心问题很明确:把「输入城市 → 拉取天气接口 → 解析返回数据 → 渲染到界面」这条链路完整跑通,并且用 AndroidStudio 的标准工程结构组织起来,你打开就能编译、能跑模拟器。适合谁?一是刚学完 Android 基础、想找个完整小项目练手的同学;二是需要快速搭一个天气展示模块、不想从零写网络层的开发者。它不是什么架构范本,但作为一条能跑通的参考链路,价值是实打实的。下面我按「它怎么组织的 → 怎么跑起来 → 怎么改 → 坑在哪」的顺序,把拆解过程摊开讲。
2. 工程结构与技术栈拆解:这份源码到底用了什么
拿到一份陌生源码,我一般不会急着点运行,而是先看目录和依赖,搞清楚作者的技术选型。这一步花十分钟,能省掉后面两小时的瞎猜。
2.1 目录骨架与模块划分
标准的 AndroidStudio 工程,核心代码集中在app/src/main下。按经验,这类天气小程序的目录大致是这么分的:
| 目录/文件 | 作用 | 拆解时重点看什么 |
|---|---|---|
java/.../MainActivity.java | 主界面入口 | 界面初始化、权限申请、首次加载逻辑 |
java/.../WeatherActivity.java | 天气详情页(若有) | 数据接收方式、Intent 传参 |
java/.../util/HttpUtil.java | 网络请求封装 | 用的什么 HTTP 客户端、超时设置 |
java/.../db/或bean/ | 数据模型 | JSON 字段与实体类的映射关系 |
res/layout/activity_main.xml | 布局文件 | 控件 id、列表/滚动容器选型 |
AndroidManifest.xml | 清单文件 | 权限声明、Activity 注册 |
先看AndroidManifest.xml,这是整个工程的“户口本”。重点确认两件事:有没有声明INTERNET权限(没有的话网络请求必挂),有没有声明定位相关权限(如果源码带自动定位功能)。很多新手跑不起来,第一步就栽在权限没声明上。
再看build.gradle(现在多是build.gradle.kts或模块级build.gradle),确认compileSdk、minSdk和依赖库版本。这一步的玄学在于:源码作者用的 Gradle 插件版本如果和你本地 AndroidStudio 差太多,同步阶段就会报一堆红。我一般会先把compileSdk对齐到本地已安装的 SDK 版本,再谈别的。
2.2 网络层与数据解析的选型逻辑
天气数据的来源无非两类:第三方天气 API,或者作者自己搭的接口。源码里通常在HttpUtil或类似工具类里能看到请求地址。拆的时候重点看三样东西:
第一,用的什么网络库。老一点的源码用HttpURLConnection裸写,新一点的用OkHttp或Retrofit。裸写的好处是零依赖、看得清每一步;坏处是回调、线程切换全得自己处理,容易在主线程发请求直接崩。看到OkHttp就说明作者至少考虑过工程化。
第二,JSON 怎么解析。常见是org.json的JSONObject手动取值,或者Gson直接映射实体类。手动取值的好处是字段少的时候直观,坏处是接口一改字段名就全崩;Gson映射则要求实体类字段名和 JSON key 严格对应,对不上就是一堆 null。
第三,线程怎么切。网络请求必须在子线程,UI 更新必须在主线程,这是铁律。源码里如果看到runOnUiThread或者Handler,说明作者处理了;如果直接在回调里改控件,那就是个隐藏的崩溃点,得记下来。
// 典型的 OkHttp 异步请求写法(源码中常见形态) OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) // 连接超时,弱网下别设太短 .readTimeout(10, TimeUnit.SECONDS) // 读取超时 .build(); Request request = new Request.Builder() .url(apiUrl) // 天气接口地址 .get() .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 网络失败回调,注意这里仍在子线程 runOnUiThread(() -> showError("网络请求失败")); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body().string(); // 拿到原始 JSON 字符串 // 解析后切回主线程更新 UI runOnUiThread(() -> renderWeather(body)); } });这段代码的逻辑说明:enqueue是异步请求,回调运行在 OkHttp 自己的线程池里,所以onResponse里绝对不能直接碰控件,必须runOnUiThread切回来。参数上,connectTimeout和readTimeout我一般设 10 秒,弱网环境设太短会频繁失败,设太长用户等得骂人。response.body().string()只能调用一次,调完流就关了,这点新手常翻车——先string()存下来再解析,别在多个地方重复取。
2.3 界面层的数据绑定方式
布局这块,天气小程序一般就两种形态:单城市详情页,或者多城市列表。看activity_main.xml里根容器是LinearLayout还是RecyclerView,基本能判断复杂度。数据绑定上,老源码多是findViewById手动赋值,新一点的用ViewBinding。手动赋值的问题是控件一多,代码又臭又长还容易空指针;ViewBinding至少能保证类型安全。
拆到这里,你应该对这份源码的“骨架”有数了:它是一条能跑通的链路,工程结构规整,但工程化程度取决于作者水平。接下来就是把它跑起来。
3. 从导入到跑通:AndroidStudio 实操四步
理论看完,动手。这一章我按实际操作的顺序走,每一步都告诉你做什么、为什么、失败了看哪里。
3.1 导入工程与 Gradle 同步
第一步,File → Open选中源码根目录(注意是含settings.gradle的那一层,不是app目录)。AndroidStudio 会自动识别为 Gradle 工程并开始同步。
同步阶段最常见的翻车是 Gradle 版本不匹配。现象是底部一直转圈,或者报Could not find method ...、Unsupported class file major version。原因通常是源码里的gradle-wrapper.properties指定的 Gradle 版本和你本地 JDK 版本对不上。解决思路:先看gradle-wrapper.properties里的distributionUrl,如果版本太老(比如 6.x),而你本地是 JDK 17,就会不兼容。两种改法——要么把 wrapper 版本升到 7.x 以上,要么在File → Project Structure里把 Gradle JDK 切到 11。我一般选前者,因为新版 AndroidStudio 对老 Gradle 支持越来越差。
提示:同步前先确认本地 SDK Manager 里装了源码
compileSdk对应的平台版本,缺了会在同步末尾报Failed to find target。
3.2 权限与接口配置
同步过了,别急着点运行。先打开AndroidManifest.xml补权限:
<!-- 网络请求必需,缺了必崩 --> <uses-permission android:name="android.permission.INTERNET" /> <!-- 若源码带自动定位,还需要下面这些 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />权限声明的逻辑:INTERNET是普通权限,声明即生效;定位是危险权限,除了声明还得在代码里动态申请,Android 6.0 以后不动态申请直接调用会抛SecurityException。源码如果只声明没动态申请,你在真机上跑就会看到定位功能静默失效——不崩,但拿不到数据,这种“黑匣子”式失败最费时间。
接着找接口地址。全局搜http关键字,定位到请求 URL 所在的常量或工具类。这里有个关键判断:源码里的接口如果是作者自建的、早就下线了,你请求会一直超时或返回 404。解决办法是换成公开的天气数据服务,把 URL 和解析字段一起对齐。换接口时注意字段映射——原接口返回temp,新接口可能叫temperature,实体类或手动取值的地方都得跟着改,漏一处就是 null。
3.3 模拟器运行与首屏验证
配置齐了,选一个 API 级别不低于minSdk的模拟器,点运行。首屏验证看三件事:界面能不能正常渲染、有没有立刻崩溃、日志里有没有网络异常。
如果一启动就崩,先看 Logcat 的FATAL EXCEPTION堆栈,最上面那行通常直接指向问题:NullPointerException多半是findViewById返回 null(布局 id 写错或setContentView没调);NetworkOnMainThreadException说明作者把请求写主线程了,得挪到子线程。如果界面出来了但数据是空的,看 Logcat 里有没有onFailure的打印,多半是接口地址失效或没网。
# 用 adb 快速看崩溃日志,比在 IDE 里翻更利索 adb logcat -s AndroidRuntime:E这条命令只过滤AndroidRuntime的 error 级日志,崩溃堆栈一目了然。参数-s是静默其他 tag,AndroidRuntime:E表示只看该 tag 的 error 级别。排查阶段我习惯先这么捞一遍,比在几千行日志里翻快得多。
3.4 换城市与数据刷新验证
首屏通了,接着验证交互。手动输入城市名触发查询,看请求参数有没有正确带上城市;下拉刷新或切城市后,看界面数据是否真的变了,而不是缓存没清。
这一步的常见问题是:请求发出去了,但界面没更新。原因往往是回调里更新了数据源,却没调notifyDataSetChanged(列表场景),或者 TextView 的setText写在了错误的生命周期里。验证方法很简单——在回调里打一行日志,确认数据到了,再确认 UI 更新代码执行了,两段日志一对比就知道断在哪。
跑通这四步,这份源码就算“活了”。但能跑不等于能用,下一章讲怎么把它改成你自己的东西。
4. 二次开发:把示例数据换成真实可用的天气源
跑通只是起点。多数人拿这份源码是为了改成自己的项目,这一章讲三个最常改的点:接口替换、数据模型对齐、界面字段扩展。
4.1 接口替换的字段对齐方法
换天气源是整个二次开发里最容易出错的环节,因为不同服务的返回结构差异很大。我的做法是先拿新接口的原始返回,和源码里的解析逻辑逐字段对照,列一张映射表再动手改代码。
假设新接口返回长这样:
{ "data": { "city": "杭州", "now": { "temp": 26, "text": "多云", "humidity": 68 } } }而源码原来解析的是扁平结构{"city":..., "temp":...}。这时候不能只改 URL,解析路径也得跟着变。用JSONObject手动取值的话,改法是:
// 原逻辑(扁平结构) // JSONObject obj = new JSONObject(body); // String city = obj.getString("city"); // 新逻辑(嵌套结构),逐层下钻 JSONObject root = new JSONObject(body); JSONObject data = root.getJSONObject("data"); // 先取 data 层 String city = data.getString("city"); JSONObject now = data.getJSONObject("now"); // 再取 now 层 int temp = now.getInt("temp"); String text = now.getString("text"); int humidity = now.getInt("humidity");逻辑说明:嵌套结构的核心是“逐层getJSONObject”,每一层都要判空——如果接口某次没返回now字段,直接getJSONObject("now")会抛JSONException。稳妥写法是先optJSONObject再判 null。参数上,getInt和getString要跟实际类型对上,接口返回字符串"26"你却用getInt,一样崩。这就是为什么我强调先看原始返回再改代码,别凭字段名猜类型。
4.2 实体类与 Gson 映射的注意点
如果源码用的是 Gson 映射,改造成本反而低一些,但有几个硬约束。实体类字段名必须和 JSON key 完全一致,或者用@SerializedName注解显式指定:
public class WeatherNow { @SerializedName("temp") private int temperature; // 字段名可以自定义,靠注解对齐 @SerializedName("text") private String weatherText; @SerializedName("humidity") private int humidity; // getter/setter 省略 }用注解的好处是字段命名可以符合 Java 规范,不用迁就接口的缩写。注意点在于:Gson 默认忽略 JSON 里多出来的字段,所以接口加字段不会崩;但接口删字段或改类型,对应属性就是默认值(int 为 0,对象为 null),这种“静默失败”比崩溃更难查。我的习惯是在解析后加一层校验,关键字段为默认值时打日志告警。
4.3 界面字段扩展与布局调整
数据层通了,界面往往要加字段——比如原来只显示温度和天气,现在要加湿度、风力。改布局时注意两点:一是TextView的 id 命名保持语义化(tv_humidity而不是tv3),二是新增控件后记得在findViewById或ViewBinding里接上,漏了就是空指针。
如果字段多到一屏放不下,把根容器换成ScrollView包一层,或者用RecyclerView做列表。这里有个性能上的小讲究:天气数据字段固定、数量少,用ScrollView完全够;只有城市列表这种不定长数据才值得上RecyclerView。别为了“显得专业”硬套列表,反而增加复杂度。
改完这三处,这份源码基本就姓你的了。但改的过程中,有些坑是绕不开的,下一章专门讲。
5. 避坑清单:这类天气源码最容易翻车的五个地方
拆源码这两天,我踩的坑和预判到的坑加起来能列一页。挑五个最典型的,按「现象 → 原因 → 解决」写清楚,你照着对一遍能省不少时间。
5.1 启动即崩:权限与主线程网络请求
现象:点运行,App 闪退,Logcat 报SecurityException或NetworkOnMainThreadException。
原因:前者是INTERNET权限没在清单里声明,或者定位权限只声明没动态申请;后者是作者把网络请求写在了主线程,Android 3.0 以后直接禁止。
解决:清单补INTERNET;定位权限在onCreate里用ActivityCompat.requestPermissions动态申请;网络请求一律挪到子线程或改用异步客户端。这类问题没有“后悔药”,只能跑之前先审一遍清单和请求代码。
5.2 数据全空:接口失效与字段错位
现象:界面正常显示,但所有天气字段都是空或 0。
原因:两种可能——源码里的接口地址已下线,请求返回 404 或超时;或者接口换了但解析字段没对齐,getString取到 null。
解决:先用浏览器或curl直接访问接口地址,确认返回内容;再对照返回 JSON 和解析代码逐字段核对。我一般会在解析前把原始body打一行日志,字段对不对一眼就看出来。
5.3 中文乱码:编码没统一
现象:城市名或天气描述显示成问号或乱码。
原因:接口返回的编码和读取时用的编码不一致,常见于用HttpURLConnection裸读流时没指定UTF-8。
解决:读流时显式指定编码,new InputStreamReader(inputStream, StandardCharsets.UTF_8);OkHttp 的response.body().string()会自动按响应头编码解析,一般不会有这问题,所以换网络库也能顺带解决。
5.4 定位拿不到:模拟器与真机差异
现象:真机上定位功能正常,模拟器上一直拿不到位置。
原因:模拟器默认没有位置数据,需要手动注入;另外模拟器的定位权限弹窗有时不弹,导致动态申请流程走不完。
解决:在模拟器的 Extended Controls 里手动设置经纬度;权限申请加回调日志,确认onRequestPermissionsResult真的被调用且grantResults为PERMISSION_GRANTED。真机验证定位功能是必须的,别只信模拟器。
5.5 刷新无效:缓存与生命周期错配
现象:切了城市,请求也发了,但界面数据纹丝不动。
原因:要么请求结果被缓存层拦了,要么 UI 更新代码写在了onPause之后的生命周期里,界面已经不可见。
解决:检查有没有加 HTTP 缓存头,调试阶段可以先禁用缓存;UI 更新统一放在数据回调里,并确认当前页面处于RESUMED状态。这类问题最隐蔽,加日志分段打点是最快的定位手段。
6. 进阶:把这份源码改造成可复用的天气模块
拆完、跑通、避完坑,最后说个进阶方向——别把它当成一个“一次性 Demo”,而是抽成一个可复用的天气模块。我自己的习惯是:任何跑通的示例代码,只要预计会用第二次,就立刻做一次模块化,否则下次还得从头翻。
具体做法分三层。第一层,把网络请求和 JSON 解析抽到一个独立的WeatherRepository类里,对外只暴露一个方法,比如fetchWeather(String city, Callback callback),界面层完全不关心底层用的是 OkHttp 还是别的。第二层,把数据模型独立成Weather实体,字段用@SerializedName对齐接口,这样换天气源时只改 Repository 和实体,界面一行不动。第三层,把“加载中 / 成功 / 失败”三种状态显式建模,用一个简单的状态枚举驱动 UI,避免到处写if (data != null)。
// 状态建模的简化写法,界面层只认状态,不认数据来源 public enum LoadState { LOADING, SUCCESS, ERROR } public void loadWeather(String city) { renderState(LoadState.LOADING); // 先切加载态 repository.fetchWeather(city, new Callback() { @Override public void onSuccess(Weather w) { runOnUiThread(() -> { bindData(w); // 数据绑定独立成方法 renderState(LoadState.SUCCESS); }); } @Override public void onError(String msg) { runOnUiThread(() -> renderState(LoadState.ERROR)); } }); }这样改完,下次再遇到“给某个页面加个天气角标”的需求,你只需要引入这个模块、调一个方法、绑一次数据,十分钟搞定,而不是再去找一份源码重新踩一遍坑。验证模块是否真的可复用,有个简单标准:把界面层整个删掉,只留 Repository 和实体,写个单元测试能不能跑通解析——能跑通,说明耦合度合格了。
说个我自己的教训。早些年我拿到能跑的示例代码,总想着“先放着,要用再改”,结果每次复用都得重新读一遍别人的代码,时间全耗在回忆上。从那以后我给自己定了个规矩:任何跑通的源码,只要判断有二次使用价值,当天就做一次最小模块化,哪怕只是把网络层抽出来。这份天气源码我拆完就顺手抽了 Repository,现在它已经成了我几个小项目里的公共依赖。希望这个习惯也能帮到你——拿到能跑的代码,别只满足于“它跑起来了”,往前多走一步,它才真正变成你的。
本文还有配套的精品资源,点击获取