简介:Android Studio天气预报小程序完整项目源码,面向Android初中级开发者,适合课程设计、毕业设计或入门实战。项目基于Retrofit和Gson构建网络请求层,通过OpenWeatherMap天气接口获取实时数据,涵盖初始化、依赖配置、接口定义、异步回调、JSON解析、UI绑定和刷新等核心开发流程,并加入错误处理、下拉刷新、本地缓存、图标显示等优化细节。压缩包为RAR格式,包体约9.76MB,共1260个文件,其中既有33个Java源码、76个XML布局与资源、21个PNG图标,也包含大量编译生成的flat、dex、class、json构建文件,并附有Gradle构建脚本和可直接安装的APK,目录结构清晰,便于对照运行和排查问题。已有3876人浏览学习,参考价值较高。通过研读与运行源码,可掌握Retrofit注解使用、异步请求管理、Gson数据转换、控件绑定及常见异常处理等技能,并体会完整项目从搭建到发布的流程,为独立开发Android应用打下基础。
1. AndroidStudio天气预报小程序源码:先确认它不是微信小程序
第一次拿到这份 AndroidStudio 天气预报小程序源码时,先别急着往 Android Studio 里拖,它不是微信小程序,而是一个跑在模拟器或真机上的 Android 原生小应用。很多人在网盘里下了源码,编译通过后一运行,界面白屏、天气数据一直不出来,然后就开始怀疑 API Key、怀疑模拟器,最后把锅甩给网络。这套东西的逻辑链其实很固定:界面层拿城市编码 → 请求天气 API → 解析 JSON → 列表渲染,任何一个环节断了都会白屏。
这份源码把整条链路完整走了一遍:布局文件、RecyclerView 列表、OkHttp 网络请求、JSON 解析、城市编码管理。对刚把 Activity、Intent、RecyclerView 基础过完的人来说,它是一份能编译、能运行、能看懂的完整工程;对要交 Java 课程设计或毕业设计的人来说,改改包名、换个城市、加个缓存,就是一套能拿出手的演示项目。下面按我自己拆源码的习惯,从工程结构、数据链路、避坑清单到验证方法一层层说。
2. 工程结构先立住:manifest权限、界面分层与列表适配器
2.1 权限声明与明文流量:manifest里要改的两个地方
拿到源码第一步,先看 AndroidManifest.xml,别急着跑。天气应用必须要网络权限,这是明面上的;暗处的坑是 targetSdk 版本。从 Android 9(API 28)开始,系统默认禁止应用访问明文 HTTP 流量,而很多老教程里写死的接口地址还是http://开头。如果源码里正好是这种情况,运行时不报编译错误,但日志里会打出Cleartext HTTP traffic to xxx not permitted,结果是界面正常、数据区域永远空白。
<uses-permission android:name="android.permission.INTERNET" /> <application android:usesCleartextTraffic="true" android:allowBackup="true" android:label="@string/app_name" android:theme="@style/Theme.AppCompat.Light">逻辑说明:INTERNET权限负责网络访问,android:usesCleartextTraffic="true"允许应用走明文 HTTP。这两处缺一不可。参数说明:usesCleartextTraffic是 application 级别的属性,不是 activity 级别;如果你的接口地址能换成 HTTPS,这一行其实可以不加,但保留它能让旧教程里写死的 HTTP 接口也能通,我一般会留着,省得排查时多一个变量。
2.2 布局怎么搭:ScrollView + RecyclerView,列表别和滑动打架
天气类应用的界面结构很固定:顶部是当前城市和今日天气,往下是未来几天的预报列表。源码的activity_main.xml一般用垂直 LinearLayout 包两个区域,列表区用 RecyclerView。这里有一个新手必踩的布局坑:RecyclerView 如果直接嵌进 ScrollView,滑动会打架,表现为列表滑不动或者整个页面卡顿。
原因不复杂,两种控件的手势事件互相竞争。这个场景下预报只有 5 到 7 天,列表很短,我的习惯是给 RecyclerView 加一行android:nestedScrollingEnabled="false",让它不参与嵌套滑动,把滚动权完全交给外层 ScrollView。如果数据源要变成几十天的列表,那就反过来,外层别用 ScrollView,直接让 RecyclerView 自己滚。
列表项布局item_weather.xml是三个 TextView 纵向排列,分别占日期、白天天气现象、温度区间。三个控件 id 一定要和适配器里 findViewById 的 id 对得上,源码最容易翻车的点就在这:改布局文件时动了一个 id,适配器还在用旧 id,运行时直接空指针闪退。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp"> <TextView android:id="@+id/tv_date" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" /> <TextView android:id="@+id/tv_weather" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> <TextView android:id="@+id/tv_temp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> </LinearLayout>逻辑说明:三个控件的排列顺序就是界面上日期的阅读顺序,日期在上、天气现象居中、温度在底部。参数说明:padding="12dp"保证列表项之间不会挤在一起,textSize用 sp 不用 dp,这是文本排版的基本规范。
2.3 适配器与ViewHolder:把天气数据填进列表项
RecyclerView 的适配器是这份源码里 Java 代码的第一块核心。WeatherAdapter 继承RecyclerView.Adapter<WeatherAdapter.ViewHolder>,构造方法接收一个List<WeatherItem>,onBindViewHolder 里按 position 取数据填到三个 TextView 上。
public class WeatherAdapter extends RecyclerView.Adapter<WeatherAdapter.ViewHolder> { private List<WeatherItem> weatherList; public WeatherAdapter(List<WeatherItem> list) { this.weatherList = 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) { WeatherItem item = weatherList.get(position); holder.tvDate.setText(item.getDate()); holder.tvWeather.setText(item.getDayWeather()); holder.tvTemp.setText(item.getDayTemp() + "℃ / " + item.getNightTemp() + "℃"); } @Override public int getItemCount() { return weatherList == null ? 0 : weatherList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvDate, tvWeather, tvTemp; ViewHolder(View view) { super(view); tvDate = view.findViewById(R.id.tv_date); tvWeather = view.findViewById(R.id.tv_weather); tvTemp = view.findViewById(R.id.tv_temp); } } }逻辑说明:getItemCount里做了空判断,数据源没初始化时返回 0,这行能避免列表页一进来就崩溃的尴尬。参数说明:onCreateViewHolder的第三个参数传 false,表示不需要立即挂载到父布局,由 RecyclerView 自己决定何时 attach;如果你传 true,每次创建 ViewHolder 都会提前绑定,列表会莫名多出间距问题。
3. 数据链路打通:高德天气API、OkHttp请求与JSON解析
3.1 为什么选高德开放平台:接口稳定,返回结构可预期
天气数据接口的选择直接决定这份源码能不能跑通。源码用的是高德开放平台的天气查询接口,选它有三个理由:第一,免费额度对课程设计和个人学习完全够用;第二,不需要复杂的签名算法,一个 Key 加城市编码就能请求;第三,返回的 JSON 结构固定且文档写得清楚,解析代码一次写对后面不用动。
相比之下,直接爬中国天气网的 HTML 页面看起来很省事,实际是给自己挖坑:页面结构说改就改,今天能解析的标签明天就不在了,而且还要处理编码和反爬,调试成本远超收益。高德接口只需要关心城市编码(adcode)对不对、Key 有没有生效,返回的 JSON 里status、forecasts、casts字段是稳定的。
3.2 用OkHttp发请求:超时参数与回调线程切换
网络请求层源码用的是 OkHttp,这个库体积小、回调用起来顺手,是 Android 开发的事实标准。请求参数里有四个必传项:key(你的 API Key)、city(城市 adcode)、extensions(all 返回几天预报)、output(返回格式固定 JSON)。
public class WeatherService { private static final String API_KEY = "你的高德Key"; private static final String WEATHER_URL = "https://restapi.amap.com/v3/weather/weatherInfo"; public void requestWeather(String cityCode, final WeatherCallback callback) { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); HttpUrl url = HttpUrl.get(WEATHER_URL) .newBuilder() .addQueryParameter("key", API_KEY) .addQueryParameter("city", cityCode) .addQueryParameter("extensions", "all") .addQueryParameter("output", "JSON") .build(); Request request = new Request.Builder().url(url).build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { callback.onError(e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { String json = response.body().string(); List<WeatherItem> list = parseWeatherJson(json); callback.onSuccess(list); } else { callback.onError("HTTP " + response.code()); } } }); } }逻辑说明:回调里的onSuccess和onError最终会在子线程执行,Activity 里收到结果后必须先切到主线程再改 UI,这一步漏掉必闪退。参数说明:connectTimeout和readTimeout各设 10 秒,太短在弱网下容易误判失败,太长会让用户对着空白界面等;用HttpUrl的addQueryParameter而不是手动拼字符串,城市参数里如果出现特殊字符,它会替你做好编码,少一类隐藏 bug。
3.3 解析三层JSON:status、forecasts、casts别少走一层
高德天气接口的返回结构是三层嵌套,新手解析时最容易在层级上翻车。最外层是status和info,中间是forecasts数组,第三层才是真正要用的casts数组,里面按日期排列每一天的预报数据。解析代码要一层一层取,每一层都用 JSONObject 或 JSONArray 接住。
private List<WeatherItem> parseWeatherJson(String json) { List<WeatherItem> result = new ArrayList<>(); try { JSONObject root = new JSONObject(json); if (!"1".equals(root.getString("status"))) { Log.e("Weather", "API返回异常: " + root.optString("info")); return result; } JSONArray forecasts = root.getJSONArray("forecasts"); JSONObject cityForecast = forecasts.getJSONObject(0); JSONArray casts = cityForecast.getJSONArray("casts"); for (int i = 0; i < casts.length(); i++) { JSONObject cast = casts.getJSONObject(i); WeatherItem item = new WeatherItem(); item.setDate(cast.getString("date")); item.setDayWeather(cast.getString("dayweather")); item.setNightWeather(cast.getString("nightweather")); item.setDayTemp(cast.getString("daytemp")); item.setNightTemp(cast.getString("nighttemp")); result.add(item); } } catch (JSONException e) { e.printStackTrace(); } return result; }逻辑说明:核心在这行"1".equals(root.getString("status")),高德接口返回的status是字符串"1",写成status == 1判断永远为 false,这是这个接口最经典的坑,后面避坑章还会单独说。参数说明:extensions=all时casts里会有 7 天数据,extensions=base只返回今天,解析代码不用改,列表长度自然不同。
| 字段 | 含义 | 层级 |
|---|---|---|
| status | 接口状态,"1"为成功,注意是字符串 | 最外层 |
| info | 状态描述,成功时返回"OK" | 最外层 |
| forecasts | 城市预报数组,取第 0 个 | 嵌套一层 |
| casts | 每日预报数组,按日期排列 | 嵌套两层 |
| daytemp / nighttemp | 白天 / 夜间温度 | 每日数据 |
| dayweather / nightweather | 白天 / 夜间天气现象 | 每日数据 |
4. 避坑清单:天气数据出不来,五个现场对症下药
4.1 现场一:模拟器白屏,请求根本没发出去
现象:App 能启动,界面布局正常,但天气区域永远是初始文字,Logcat 里有一条Cleartext HTTP traffic to xxx not permitted或者干脆没有网络日志。原因:接口地址是http://明文流量,被 targetSdk 28 以上的系统策略拦掉了;另外如果没在 manifest 里声明 INTERNET 权限,网络层会直接抛 SecurityException,连日志都不打。解决:先确认 manifest 里usesCleartextTraffic="true"已配置,再用adb shell观察请求是否发出;最省心的做法是把 URL 换成 HTTPS 版本,一劳永逸。
4.2 现场二:数据回调里直接setText,闪退 CalledFromWrongThreadException
现象:Logcat 报CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views,然后闪退。原因:OkHttp 的onResponse运行在 OkHttp 的线程池里,不是主线程,Android 不允许在子线程直接操作 UI。解决:在 Activity 里收到回调结果时,用runOnUiThread包裹 UI 更新代码,或者用 Handler 发消息切回主线程。我习惯在 WeatherCallback 的实现类里统一做切换,这样不管哪个请求走到回调,UI 更新都在同一个地方处理。
4.3 现场三:status判断写成布尔,JSONException来得莫名其妙
现象:解析代码一执行就进 catch,打印出来的异常是JSONException,但手动把 JSON 字符串粘到在线解析工具里看完全没问题。原因:最常见的写法是把status当成布尔值比较,或者把forecasts数组当成对象直接取字段,这都源于没看清返回结构。解决:先打印原始 JSON,逐层确认类型,再改解析代码。高德接口的status字段类型是字符串不是数字,比较时用"1".equals(root.getString("status"));forecasts是数组,取第 0 个元素后再往里走一层。
4.4 现场四:Key与包名/SHA1不匹配,真机永远返回错误
现象:模拟器上数据正常,打包到真机后接口返回USER_KEY_PLATFORM_NOT_MATCH。原因:高德开放平台的 Key 创建时绑定了应用包名和签名 SHA1,模拟器上跑的是 debug 签名,真机装的是 release 签名,两个 SHA1 不同,Key 自然失效。解决:去高德控制台重新配置 Key,把 debug 和 release 两种签名的 SHA1 都加上;或者开发阶段统一用 debug 签名安装。查 SHA1 的命令是keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android,拿到值填进控制台再等几分钟生效。
4.5 现场五:city传中文名,以及模拟器卡在Starting up
现象:请求发出去了,返回体里info是INVALID_USER_CITY。原因:city参数要求传城市编码 adcode,比如北京是110100,长沙是430100,传中文名"长沙"接口不认识。解决:在源码里维护一张常用城市编码映射表,城市切换时走这份表,而不是让用户手输编码。另外如果你卡在模拟器启动阶段,从相关热搜的androidstudio 模拟器starting up也能看出来这是普遍问题:Android Studio 默认创建的 AVD 如果是 ARM 镜像,在 x86 电脑上启动极慢,去 Device Manager 里新建一个 x86_64 镜像的 AVD,能省一半启动时间。
5. 把源码改厚:城市切换、本地缓存与下拉刷新
5.1 城市缓存与数据缓存:SharedPreferences的键名设计
天气 App 的体验分水岭在缓存。没有缓存的版本,每次打开都闪一下加载态,断网就一片空白。源码改造的第一步是把城市编码和最后一次请求到的 JSON 都存进 SharedPreferences。城市编码存的是用户上次选的城市,下次启动直接恢复;JSON 存的是原始字符串,启动时先渲染缓存再发网络请求,两秒内就能看到上次的天气,网络回来后再覆盖。
public class WeatherCache { private static final String PREF_NAME = "weather_pref"; private static final String KEY_CITY_CODE = "city_code"; private static final String KEY_LAST_JSON = "last_weather_json"; public static void saveLastJson(Context context, String json) { context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) .edit() .putString(KEY_LAST_JSON, json) .apply(); } public static String readLastJson(Context context) { return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) .getString(KEY_LAST_JSON, ""); } }逻辑说明:缓存里保存的是 JSON 字符串而不是解析后的对象,目的是让读取路径和网络路径走同一个parseWeatherJson方法,少维护一套字段复制逻辑。参数说明:apply()是异步落盘,commit()是同步落盘,缓存场景用apply()不卡主线程;如果连续多次写入,apply()会自动合并写操作,比commit()高效。
| 键名 | 存什么 | 什么时候读 |
|---|---|---|
| city_code | 当前城市 adcode | 启动时恢复上次选择 |
| last_weather_json | 最后一次请求到的完整 JSON | 启动先渲染缓存,再刷新 |
5.2 城市切换怎么做:对话框输入中文名,映射到adcode
很多课程设计版本的天气 App 城市切换做得很粗暴:用户输什么就传什么,传中文名就翻车。我在改造时会让用户输入中文城市名,程序内部维护一张HashMap<中文名, adcode>,查完映射再发起请求。支付宝上同样的问题,换成活动参数三要素就绕不开。
private static final HashMap<String, String> CITY_MAP = new HashMap<>(); static { CITY_MAP.put("北京", "110100"); CITY_MAP.put("长沙", "430100"); CITY_MAP.put("上海", "310100"); } private void switchCity(String cityName) { String adcode = CITY_MAP.get(cityName); if (adcode == null) { Toast.makeText(this, "暂不支持该城市", Toast.LENGTH_SHORT).show(); return; } WeatherCache.saveCityCode(this, adcode); requestWeatherAndUpdate(adcode); }逻辑说明:switchCity拿中文名查映射,查不到直接提示,查到了先存缓存再发请求,保证下次启动还是这个城市。参数说明:这张映射表只维护少量常用城市,课程设计完全够;如果要覆盖全国所有城市,就得接高德的输入提示接口,那属于进阶玩法,工程量和收益在演示阶段不成比例。
5.3 SwipeRefreshLayout下拉刷新:时机与收尾状态
下拉刷新是天气 App 的标配。SwipeRefreshLayout 包住外层 ScrollView,监听刷新事件后重新请求接口,数据回来后用notifyDataSetChanged()刷新列表。最大的坑是忘记收起刷新动画:请求慢的时候用户一直看着转圈,请求完了圈还在转,看起来像卡死。
SwipeRefreshLayout swipeRefresh = findViewById(R.id.swipe_refresh); swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { requestWeatherAndUpdate(currentCityCode); } }); // 在请求回调的 finally 或 onResponse 末尾执行 swipeRefresh.setRefreshing(false);逻辑说明:setRefreshing(false)必须执行,不管请求成功还是失败。参数说明:setRefreshing的时机放在数据渲染完再收,不要在 onRefresh 里立刻收,否则网络慢的用户会看到内容跳闪,体验反而更差。
6. 验证先行:curl确认接口,再把JSON留在本地调试
6.1 curl先跑通:状态码、info字段和你的Key
拿到源码,我第一件事不是打开 Android Studio,而是先拿终端把接口打一遍。用 curl 请求高德天气接口,确认 Key 生效、城市编码正确、返回结构符合预期,再回头改代码。这一步能在 30 秒内把网络层问题排除干净,省掉后续所有玄学排查。
curl "https://restapi.amap.com/v3/weather/weatherInfo?city=110100&key=你的Key&extensions=all&output=JSON"看返回体的status是不是"1"、info是不是"OK"。如果status不是 1,优先检查 Key 是否过期、城市编码是否存在;如果请求被拒,再去控制台核对包名和签名 SHA1。curl 通了,代码里跑不通,问题才在工程内部。
6.2 把返回JSON做成assets本地资源,UI调试不再依赖网络
接口频繁调用会被限流,调试界面布局时又不想每次都等网络。我的做法是把 curl 拿到的真实返回保存到assets/weather.json,调试阶段直接从本地读,真实返回和最终的数据流完全一致。网络不通、Key 失效都不影响 UI 调试,等界面调好了再切回线上接口。
private String readLocalJson(Context context) { try (InputStream in = context.getAssets().open("weather.json")) { return new BufferedReader(new InputStreamReader(in)) .lines().collect(Collectors.joining("\n")); } catch (IOException e) { e.printStackTrace(); return ""; } }这个本地 JSON 还能顺手做成单元测试的测试数据:把parseWeatherJson传进去,断言返回列表长度和字段值。从那以后我拿到任何一份带网络请求的源码,第一件事不是打开布局文件,而是先 curl 一把接口,确认数据链路通再回头看代码,这个习惯帮我省掉了不知道多少小时的瞎猜。希望这些验证方法对你有用,在你自己的项目里少踩几个坑。
本文还有配套的精品资源,点击获取