
简介基于Android Studio的天气预报小程序完整源码包面向具备Android基础、希望学习网络请求与JSON解析的移动开发初学者及毕业设计参考者。资源共1260个文件压缩包大小9.76MB包含Java源码、XML布局、Gradle配置、APK安装包及大量编译中间文件其中flat、dex、class等构建产物可直接对照项目结构理解Android工程组织方式。项目采用Retrofit与Gson实现天气API调用涵盖城市查询、温度湿度显示、异步回调处理、下拉刷新及本地缓存等常见功能模块源码注释清晰适合边读边改快速上手。已有3876人学习下载对想要掌握Android Studio项目初始化、第三方接口接入和UI数据绑定流程的开发者具有直接参考价值。1. 用 AndroidStudio 做天气预报小程序先想清楚数据源、线程和 UI 三件事很多人拿着“AndroidStudio 天气预报小程序源码”这个关键词搜索第一反应以为是微信小程序。其实这里说的是一个跑在 Android 手机上的原生小工具工程入口叫 MainActivity打包后装进手机就是一个能查天气的 App。做这件事的核心不是写布局而是三件事天气数据从哪个接口来网络请求放在哪个线程跑接口返回的 JSON 怎么映射到界面。把这三件事理清楚剩下的代码都是体力活。如果你是想做成微信小程序请去用微信开发者工具或者 uniapp 那套这篇讲的是 AndroidStudio 原生工程适合课程设计、个人工具箱、或者工作室接小单的场景。2. 把天气接口的数据结构吃透选 API、定字段、建最小项目2.1 免费天气 API 怎么挑我用过三种方案后的选择天气 App 的数据源只有两条路自己买气象数据服务或者用开放平台接口。个人项目一定走后者。市场上常见的几类和风天气、聚合数据、心知天气以及很多教程里会出现的不需要 key 的公开接口。我的建议是学习阶段不要一上来就去注册和风天气、聚合数据拿 appKey因为你还要处理密钥管理、服务条款这些额外因素而公开接口能让你把注意力集中在 Android 的请求与解析上。我平时给学生演示用的是wthrcdn.etouch.cn/weather_mini?city北京这个接口。它不需要 key返回的是 JSON字段完整有当天预报、温度、风力和污染指数能在浏览器里直接打开验证。缺点是不保证长期可用生产环境千万别依赖这种接口。如果你要做一个能上架的产品去和风天气注册开发者账号免费档足够个人 App 用如果想在企业内网做演示也可以自己在后端用高德天气那份数据包一层 JSON。选接口时看三样东西是否需要 key、是否支持 HTTPS、返回 JSON 是否稳定。下面是我常用的一个判断表。方案是否需要 key返回格式适合场景主要坑公开接口wthrcdn 等否JSON学习 / 课程设计随时可能失效或被限流和风天气是JSON个人作品 / 小商单免费版有请求频率限制聚合数据是JSON学生项目部分数据要付费文档改动频繁高德开放平台是JSON已经依赖高德地图的项目天气作为增值接口权限配置较烦结论很直接只想跑通源码用第一条想拿出去展示给人用用第二条。到最后统一用 https 接口下面代码为了演示方便先用 http避坑章里会专门讲明文流量限制。2.2 接口返回的 JSON 长什么样先看字段再写解析拿到任何接口第一件事不是写 Activity而是先取一段真实返回样本把结构剥开。wthrcdn.etouch.cn/weather_mini?city北京返回的大致结构如下{ data: { yesterday: { date: 4日星期五, high: 高温 30℃, low: 低温 21℃, type: 晴 }, city: 北京, forecast: [ { date: 5日星期六, high: 高温 31℃, low: 低温 22℃, type: 多云, fengxiang: 东南风 } ], wendu: 26, ganmao: 昼夜温差大风力较强易感冒 }, status: 1000, desc: OK }注意几个容易搞错的点。城市名在data.city这是你在 EditText 里输入北京后接口回传的标准城市名当天天气在data.forecast[0]数组第一位是今天不是明天当前实时温度在data.wendu它不在 forecast 数组里通常是一个字符串数字感冒建议在data.ganmao用来做文案展示非常合适。写解析的时候先找forecast数组取[0]再从它里面取date、type、high、low。这四个字段决定了 UI 上要展示的内容。很多人写代码时以为data.temp就是当前温度拿回来的 JSON 里根本没有然后getString抛异常这就是没做第一步的后果。拿到样本之后建议把 JSON 存成一个本地文件后面可以用它做单元测试这一点在最后一章会展开。2.3 项目最小化文件清单与 AndroidManifest 权限很多“源码包”下载下来一堆文件但你能跑通的最少工程应该只有这几样。WeatherDemo/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/weather/MainActivity.java │ │ ├── res/layout/activity_main.xml │ │ └── AndroidManifest.xml │ └── build.gradle ├── build.gradle └── settings.gradle不要把网络库、图片加载库、城市数据库一上来全引进去。先跑通再加东西。Manifest 是这个小程序能不能联网的第一道关卡。在 AndroidStudio 中新建工程后如果忘了加 INTERNET 权限所有请求都会报Permission denied。同时因为接口是 http 明文低版本没事高版本必须给 Application 加上usesCleartextTraffictrue。manifest xmlns:androidhttp://schemas.android.com/apk/res/android uses-permission android:nameandroid.permission.INTERNET / application android:usesCleartextTraffictrue android:labelstring/app_name activity android:name.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifestusesCleartextTraffic这个属性只适合开发调试。如果你面向 Android 9 以上做正式发布最好是选支持 https 的天气接口或者在后端做转发。曾见过有人把usesCleartextTraffictrue留在线上包里的审计时会被挑出来这个坑要在最早期就避免掉。布局方面最小可行方案是一个输入框、一个查询按钮、一个文本展示区。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding20dp EditText android:idid/etCity android:layout_widthmatch_parent android:layout_heightwrap_content android:hint输入城市名如北京 / Button android:idid/btnQuery android:layout_widthmatch_parent android:layout_heightwrap_content android:text查询天气 / TextView android:idid/tvInfo android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop16dp android:textSize16sp android:lineSpacingExtra6dp / /LinearLayout这里的两个控件 id 后面 MainActivity 要引用不要改名字。所有布局都写在一个LinearLayout里避免 ScrollView 嵌套的麻烦。等天气字段多了再把 TextView 改成 RecyclerView但那是后话。3. 在 MainActivity 里跑通请求与解析线程模型和完整 Java 代码3.1 核心逻辑用 HttpURLConnection 请求天气接口AndroidStudio 创建的默认工程使用 androidxMainActivity 继承AppCompatActivity。先声明 UI 控件在onCreate里绑定然后在按钮点击时读取城市输入框的值。public class MainActivity extends AppCompatActivity { private EditText etCity; private Button btnQuery; private TextView tvInfo; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etCity findViewById(R.id.etCity); btnQuery findViewById(R.id.btnQuery); tvInfo findViewById(R.id.tvInfo); btnQuery.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String city etCity.getText().toString().trim(); if (city.isEmpty()) { tvInfo.setText(请输入城市名); return; } queryWeather(city); } }); } }queryWeather方法要负责两件事拼 URL发请求。拼 URL 时城市名必须做URLEncoder.encode不然用户输入“上海”时是中文直接拼进地址里会变成无效请求。private void queryWeather(final String city) { new Thread(new Runnable() { Override public void run() { HttpURLConnection connection null; try { String urlStr http://wthrcdn.etouch.cn/weather_mini?city URLEncoder.encode(city, UTF-8); URL url new URL(urlStr); connection (HttpURLConnection) url.openConnection(); connection.setRequestMethod(GET); connection.setConnectTimeout(8000); connection.setReadTimeout(8000); connection.setRequestProperty(User-Agent, Mozilla/5.0); connection.setRequestProperty(Accept-Encoding, identity); if (connection.getResponseCode() 200) { InputStream is connection.getInputStream(); ByteArrayOutputStream baos new ByteArrayOutputStream(); byte[] buffer new byte[1024]; int len; while ((len is.read(buffer)) ! -1) { baos.write(buffer, 0, len); } String json baos.toString(UTF-8); baos.close(); is.close(); parseAndShow(json); } else { showError(服务器返回 connection.getResponseCode()); } } catch (Exception e) { e.printStackTrace(); showError(请求失败 e.getMessage()); } finally { if (connection ! null) { connection.disconnect(); } } } }).start(); }这里新开了一个 Thread原因写在代码后面Android 不允许在主线程执行网络访问强行访问直接抛NetworkOnMainThreadException。超时时间设置为 8 秒因为免费天气接口在移动网络下经常超过 5 秒太短会频繁失败太长会让用户一直盯着空白页。Accept-Encoding设置成identity是为了避免服务器返回 gzip 压缩流否则我们没解压就读出来会是乱码这个坑在下一章专门讲。3.2 JsonObject 解析与控件回写注意 wendu 并不在 forecast 里拿到 JSON 字符串后不要立刻 setText先把它解析成 Java 对象。我用的是原生org.json包是 Android 自带的不用额外引依赖。private void parseAndShow(String json) throws JSONException { JSONObject root new JSONObject(json); int status root.getInt(status); if (status ! 1000) { showError(接口状态异常 status); return; } JSONObject data root.getJSONObject(data); String city data.getString(city); String wendu data.getString(wendu); String ganmao data.getString(ganmao); JSONArray forecast data.getJSONArray(forecast); JSONObject today forecast.getJSONObject(0); String date today.getString(date); String type today.getString(type); String high today.getString(high); String low today.getString(low); String fengxiang today.getString(fengxiang); String text 城市 city \n当前温度 wendu ℃ \n date \n天气 type \n最高温 high \n最低温 low \n风向 fengxiang \n提醒 ganmao; runOnUiThread(new Runnable() { Override public void run() { tvInfo.setText(text); } }); }JSONObject的getString在字段不存在时会抛异常所以代码里做了status的检查。还有一点high和low返回的是“高温 31℃”“低温 22℃”这种带前缀的字符串如果你打算把它们抠成数字应该自己写一个替换逻辑String highPure high.replace(高温, ).replace(℃, ).trim();因为在 AndroidStudio 里用 debug 模式看 JSONObject 结构很不直观我建议把接口返回的先Log.d打出来再动手解析。这不是偷懒而是很多“源码包”里解析错误都出在字段看不到、凭感觉瞎写这一步。3.3 请求失败时的兜底文案 完整 MainActivity网络请求不能指望永远成功。免费接口经常在晚上被刷挂或者手机网络超时所以要给用户一个明确的失败提示而不是直接闪退。逻辑是解析成功用runOnUiThread刷新 TextView失败也用runOnUiThread刷新成错误信息。private void showError(final String message) { runOnUiThread(new Runnable() { Override public void run() { tvInfo.setText(message); } }); }showError和parseAndShow都用到了异步线程所以无论如何 UI 更新都发生在主线程避免在子线程里直接改控件导致的CalledFromWrongThreadException。把前面几个方法合到一起就是一个能跑的 MainActivity。实际应用里我还喜欢在点击查询后先把按钮 disable 掉防止用户连点十几个请求把自己的 IP 送进限流名单btnQuery.setEnabled(false); // 请求返回后再 setEnabled(true)这个防抖逻辑虽然朴素但效果明显。你可以把它放在queryWeather开头和parseAndShow/showError的尾部。核心流程跑通后你会发现天气小程序的主干根本不是 UI而是线程模型和异常处理。这个项目小正是学线程协作的好案例。4. 天气预报小程序避坑指南5 个新手最容易翻车的地方4.1 崩溃NetworkOnMainThreadException现象是点击查询按钮后 App 直接闪退Logcat 里写着android.os.NetworkOnMainThreadException。原因是 Android 从 API 11 开始禁止在 UI 线程执行网络请求HttpURLConnection 的connect()一旦在主线程调用必崩。解决方式就是前面演示过的new Thread加runOnUiThread。更现代的做法是用协程或 RxJava但理解线程模型之前不要用那些包装好的工具。你也许会问为什么我用模拟器打开浏览器能访问天气地址App 里就崩因为浏览器自己也用了网络线程。这不是普通异常它是由系统安全策略强制抛出的无法通过 try-catch 吞掉。4.2 中文乱码压缩流和编码不一致现象是接口请求成功返回的 JSON 能打到 Logcat 里但城市名是乱码温度是乱码。这里有两个常见原因。第一个是编码HttpURLConnection 读取输入流时你没有指定 UTF-8而是直接拿了stream.toString()得到的是操作系统默认编码的解码结果。第二个原因是服务器开启了 gzip 压缩响应头里有Content-Encoding: gzip而客户端没有解压读出来全是乱码二进制。解决方式是两行connection.setRequestProperty(Accept-Encoding, identity); String json baos.toString(UTF-8);第一行告诉服务器不要压缩第二行强制按 UTF-8 解码。如果要用 gzip也可以前提是自己包一层GZIPInputStream但能少一事就少一事。这个坑在网上很多源码包里都没处理因为它只在特定网络环境下才暴露遇到一次就记住一辈子。4.3 请求被拒cleartext 限制与模拟器 10.0.2.2现象是 AndroidStudio 自带模拟器上运行Logcat 报错CLEARTEXT communication to wthrcdn.etouch.cn not permitted by network security policy。从 Android 9API 28开始系统默认禁止明文 HTTP 请求。如果你还用系统生成的 default config访问 http 接口就会报这个错。解决方式是在 Application 标签上加android:usesCleartextTraffictrue或者把天气接口换成 https。要说明的是usesCleartextTraffic只影响当前 App不会影响系统设置。如果你自己在本机写了一个 mock 接口在模拟器上访问localhost或127.0.0.1永远连不上。模拟器里的localhost是模拟器自己不是你的开发电脑。Android 模拟器约定10.0.2.2指向宿主机。调试本地接口时地址应写成String urlStr http://10.0.2.2:8080/weather_mini?city北京;很多新手卡在这一关折腾半天发现不是代码问题而是地址写错。真机调试要用电脑的局域网 IP还要保证同一个 Wi-Fi这已经是基础网络常识了。4.4 接口又超时了免费接口的限流与重试策略现象是应用偶尔返回超时偶尔返回空数据在某个时间段内频繁失败。原因是免费接口没有 SLA 承诺尤其是不需要 key 的公开接口经常限流或者被系统挡掉。解决办法有三个层级第一设置合理的超时8 秒是我的经验值再长用户就按退出键了第二在本地做缓存把上一次成功的数据存到 SharedPreferences至少让用户下次打开能看到离线结果第三做一次重试但重试要退避不要点击一下瞬间发三次请求。重试我一般写得很保守int retryCount 0; while (retryCount 2) { try { // 执行请求 break; } catch (SocketTimeoutException e) { retryCount; if (retryCount 2) { showError(天气接口超时请稍后重试); } } }SocketTimeoutException和IOException要分开处理前者是网络慢后者可能是地址失效。这个区分能帮你判断是接口挂了还是自己网络不行。4.5 模拟器第一次启动一直卡在 AndroidStudio 的 Starting up现象是新装完 AndroidStudio创建天气项目后启动模拟器窗口卡在Starting up很久。原因不是天气代码的问题而是模拟器选择了一个慢的 AVD 镜像或者在下载系统镜像。解决方式是先检查AVD Manager里系统镜像是不是 x86_64太老的 ARM 镜像在 Windows 上慢到怀疑人生。这个和天气接口无关但既然你是从 AndroidStudio 开始大概率会遇到。别在模拟器没起来时就去排查代码先确认模拟器本身状态。5. 让小程序像正式项目一样长跑封装、本地 JSON 验证与发布注意5.1 把网络请求抽成 WeatherData 类当 MainActivity 已经跑通下一步就是把请求逻辑从 Activity 里抽出去不然以后想切 OkHttp、换协程都要改 UI 层代码。我一般定义一个WeatherDataSourcepublic class WeatherDataSource { public void query(final String city, final WeatherCallback callback) { new Thread(new Runnable() { Override public void run() { try { // 原来的 HttpURLConnection 逻辑 callback.onSuccess(weatherText); } catch (Exception e) { callback.onError(e.getMessage()); } } }).start(); } public interface WeatherCallback { void onSuccess(String weatherText); void onError(String error); } }Activity 只依赖WeatherDataSource和WeatherCallback不再直接写 URL。这个封装大约二十行但收益是后续换 OkHttp 时只有这个文件要动。注意回调仍然是子线程等引入协程后可以再改成 suspend 函数但在当前阶段这样足够清晰。5.2 用 Assets 里的本地 JSON 验证解析逻辑我在写解析逻辑前会把接口返回先存一份到app/src/main/assets/sample_weather.json。然后在解析方法里临时绕过网络直接读本地文件用来验证JSONObject的字段名是否正确。这个方法很土但极其有效。特别是当接口升级字段名改变时你只需要更新 assets 里的样本跑一次解析用例立刻知道哪里崩。下面是一个简单的工具方法private String loadAsset(Context context, String fileName) { try { InputStream is context.getAssets().open(fileName); ByteArrayOutputStream baos new ByteArrayOutputStream(); byte[] buf new byte[1024]; int len; while ((len is.read(buf)) ! -1) { baos.write(buf, 0, len); } return baos.toString(UTF-8); } catch (IOException e) { return null; } }然后把parseAndShow的输入改成parseAndShow(loadAsset(this, sample_weather.json));这样跑 App 时不联网也能把 UI 调好。我处理 Android 项目时标准顺序是先离线调 UI再切回在线请求最后做真机测试。不要反过来一边断网一边猜字段永远写不顺。5.3 发布前必须做的三件小事如果你想把天气小程序打包给别人安装有三个点要处理。第一usesCleartextTraffic是否保留为了上架或外出演示建议换成 https 接口这个属性最好去掉。第二城市输入要做空值和非法字符校验我已经在点击事件里做了isEmpty判断还可以用正则过滤掉数字和符号。第三字体大小和行间距在深色模式下要测试TextInfo 默认是黑色字如果用户开了深色主题黑色字体会看不清。我现在的习惯是拿到任何一个天气类项目先把接口返回样本存到 Assets再写解析最后才开始写界面。这个顺序救过我太多次。希望这篇 AndroidStudio 天气预报小程序的实战笔记能帮到你。本文还有配套的精品资源点击获取