
简介面向移动开发初学者的Android Studio天气预报小程序完整源码包基于Retrofit与Gson实现网络请求覆盖项目配置、接口封装、数据解析、UI绑定及下拉刷新等核心环节适合正在学习Android网络编程或需要课程设计参考的开发者。资源包共1260个文件包含Java源码、XML布局、Gradle配置、APK安装包及大量编译中间产物dex/class/flat等压缩包约9.76MB可直接导入Android Studio运行调试。已有3876人学习下载包内附详细教程式描述从创建工程、添加依赖到构建WeatherApiService接口、处理异步回调再到SwipeRefreshLayout优化逐步演示完整开发流程同时提供SharedPreferences缓存、Glide天气图标加载等扩展思路可帮助读者快速搭建可用项目并理解移动端API调用与UI更新的典型模式。1. 拿到这份 AndroidStudio 天气预报源码先别急着点 Run从 AndroidStudio 打开这份天气预报小程序源码第一件事不是点那个绿色三角的 Run。新手的常见剧本是Gradle 同步还没走完就 build十分钟后收获一整片红色报错。这份源码本质是一个轻量级 Android 应用工程主界面展示当前城市的实时温度、天气现象、湿度和风力切换城市后界面跟着刷新数据通过网络请求读取。它能帮你省掉从零写网络请求、JSON 解析和布局交互的重复工作适合刚学完 Android 基础、想找一个真正能跑起来且改得动的完整练手项目的人。下面按我实际拆这份源码的顺序从环境对齐、数据接入一步步讲到最后那些坑。2. 先看清项目家底Gradle 版本、SDK 与依赖选型2.1 工程结构打开后先认识这几个文件拿到源码后我不会急着点 Sync而是先把 AndroidStudio 左侧的工程树扫一遍确认工程组织形式。这份源码对应的是标准单模块应用没有引入多模块或组件化包名是 com.example.weatherdemo你拿到手之后想发布到自己的渠道改 applicationId 就行。最需要关注的文件有四个app/build.gradle 管依赖和编译参数AndroidManifest.xml 管权限和入口 ActivityMainActivity.java 管主界面逻辑和网络回调WeatherBean.java 是天气数据类和 JSON 字段映射的枢纽。排查问题时按这四个文件分工去查定位速度会快很多。文件作用什么场景下要改它app/build.gradle依赖版本、SDK 编译版本升级依赖库、调整 Java 版本AndroidManifest.xml网络权限、Activity 声明加权限、换启动页MainActivity.java页面逻辑、网络回调、UI 更新改业务逻辑、加新功能ApiService.javaRetrofit 接口定义换数据源、改接口路径WeatherBean.javaJSON 到对象的字段映射服务商字段名不一致时activity_main.xml主界面布局与控件 ID调整展示样式有些源码会把接口和字段映射合并到一个文件里少一个文件不代表缺东西但要换数据源时就得仔细找。我一般先看有没有 ApiService.java没有的话直接搜 GET 注解一下就定位到网络层。2.2 build.gradle三个 SDK 版本值决定兼容边界同步工程时大部分报错都出在 SDK 版本和 Gradle 版本不匹配。这份源码的 app/build.gradle 里关键配置长这样android { namespace com.example.weatherdemo compileSdk 34 defaultConfig { applicationId com.example.weatherdemo minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.9.3 implementation com.google.code.gson:gson:2.10.1 }compileSdk 34 表示本地需要安装 Android SDK 34 以及对应版本的 Build-Tools这个值决定你能用到哪些系统 API。minSdk 21 表示支持 Android 5.0 及以上的设备配合 targetSdk 34 一起看targetSdk 会影响系统行为变更比如 Android 9 开始默认禁止明文 HTTPAndroid 12 开始定位权限需要动态申请。Java 11 对应 JDK 11你的 AndroidStudio 如果内置 JDK 17跑这个配置完全没问题如果本地 JDK 是 8编译会直接报错。我拿到源码会先看一眼这三个值再动其他代码能避免至少一半的无关报错。2.3 依赖选型为什么是 Retrofit Gson 而不是手写线程很多早期 Demo 喜欢自己写 HttpURLConnection 加 Thread几十行的小例子能跑一旦页面多了、请求多了就难受。这份源码用的是 Retrofit OkHttp Gson 这套成熟组合三个库各自负责一块Retrofit 把接口定义转成可调用方法OkHttp 负责底层连接池、超时和拦截器Gson 负责把 JSON 字符串转成 Java 对象。这套组合的最大好处是社区资料多遇到问题搜索时几乎能搜到一模一样的场景。依赖版本不要盲目追新。Retrofit 2.9.0 和 OkHttp 4.9.3 是经过大量验证的稳定组合升到高版本有可能遇到 API 变更引发的编译错误。如果你在同步时发现某些依赖下载不下来常见做法是在根目录的 settings.gradle 里配置镜像仓库替换默认的 Maven 中央仓库地址避免墙外仓库连接超时。2.4 本地环境对齐SDK 缺失和 Gradle 版本冲突第一次打开工程最常见的问题不是代码本身而是本地的 Android SDK 路径不对。AndroidStudio 找不到 SDK 时会提示 SDK location not found这时需要在 local.properties 文件里显式指定路径sdk.dir/用户目录/Android/Sdk如果你在两台电脑之间切换工程这个文件最容易漏。另一个高发问题在 Gradle 版本上源码工程内 gradle-wrapper.properties 里指定了 Gradle 版本这个版本对 JDK 有要求。我一般对对应关系记一个简化判断Gradle 8.x 配 JDK 17Gradle 7.x 配 JDK 8 或 11。把 AndroidStudio 里的 Gradle JDK 设置成 17大部分编译问题都能解决。3. 天气数据接入从申请 Key 到 JSON 字段映射3.1 选天气数据源免费额度、HTTPS 与字段稳定性天气数据是这份源码的命脉。我选择天气服务商时只看三个点。第一免费额度够不够很多开放平台每天几千次免费请求个人练手完全够用第二是否强制 HTTPS如果服务商只提供 http:// 地址在 targetSdk 28 以上会被 Android 系统直接拦掉后面第 5 章会专门讲这个坑第三字段稳不稳定不同服务商返回的 JSON 结构差异很大有的温度字段叫 temp有的叫 tmp有的叫 temperature字段不稳定意味着每次接口升级你都要跟着改代码。这份源码默认对接的接口地址和服务商控制台是配套的改造空间也不小换一家服务商时只需要改接口路径和字段注解。3.2 创建应用拿 Key包名和签名 SHA1 别填错数据服务商一般都会要求注册账号、创建应用、填包名然后把生成的 Key 交给客户端。我在帮别人排查时发现403 错误十有八九出在这一步。填包名时要用源码真实 applicationId而不是新建工程时的默认值。如果服务商要求填签名 SHA1在 AndroidStudio 的终端里执行keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android输出结果里 SHA1 指纹是冒号分隔的一串十六进制字符复制时别漏字符也别把空格带进去。我把拿到的 Key 统一放进常量类public class Constant { // 在服务商控制台创建应用后生成的 Key public static final String WEATHER_KEY 你的Key; // 接口基础地址注意末尾斜杠不能少 public static final String BASE_URL https://api.example.com/; }这里要特别注意keytool 拿到的是 debug 签名。之后如果用 release 签名打包上架需要重新生成 SHA1 并在控制台更新否则线上包会一直报权限校验失败。3.3 网络请求封装Retrofit 接口定义与 OkHttp 拦截器拿完 Key 就开始接触网络层。这份源码用 Retrofit 定义接口一个典型的天气查询接口长这样public interface ApiService { GET(weather/now) CallWeatherBean getWeather( Query(city) String cityCode, Query(key) String key ); }GET 里的 weather/now 是相对路径最终请求的完整地址是 BASE_URL 加上这个路径拼接出来的。Query 注解表示这两个参数会拼在 URL 后面的查询字符串里比如 weather/now?city101010100keyxxxx。用 Call 而不是直接用 RxJava是因为这个工程足够小Retrofit 原生的异步回调就够用不引入额外复杂度。真正发送请求之前需要配置 OkHttpClientOkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .addInterceptor(new HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(Constant.BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); ApiService apiService retrofit.create(ApiService.class);connectTimeout 是建立 TCP 连接的超时时间readTimeout 是等待响应数据的超时时间我习惯都设 10 秒。遇到弱网环境超过 10 秒直接在回调里提示用户检查网络比干等要好。HttpLoggingInterceptor 的 BASIC 级别打印会输出请求方法、URL 和响应码调试时够用想看完整 JSON 响应体就切到 BODY 级别但上线前记得关掉否则请求数据会全部打到 Logcat 里。转换器用的是 GsonConverterFactory它决定了后续 JSON 字符串如何映射到 WeatherBean。3.4 JSON 映射把返回字段变成可读对象天气接口返回数据一般是 JSON 格式我的调试习惯是先拿到一段真实样例再写数据类。样例结构大致如下{ city: A市, now: { temp: 28, weather: 晴, humidity: 60, windDir: 东南风, windSc: 3级 } }对应的数据类是public class WeatherBean { SerializedName(city) public String city; SerializedName(now) public Now now; public static class Now { SerializedName(temp) public String temp; SerializedName(weather) public String weather; SerializedName(humidity) public String humidity; SerializedName(windDir) public String windDir; SerializedName(windSc) public String windSc; } }SerializedName 是 Gson 的注解作用是把 JSON 字段名和 Java 字段名绑定。服务商返回的字段名如果和 Java 字段命名风格不一致就只改注解值不要轻易改 Java 字段名因为界面层到处都在用这些字段。温度、湿度、风向这些我都声明为 String 而不是 int 或 double原因是天气接口有时会返回带单位的内容字符串能装下所有情况避免解析时出现数字转换异常。如果 JSON 嵌套层级比这深一层比如 now 外面多包了 data 节点就在 WeatherBean 里再加一个 data 字段把 Now 放到 data 下面。4. 界面与交互布局、城市切换和默认城市持久化4.1 activity_main.xml三段式结构怎么写主界面布局走的是简单三段式顶部是城市名中间是大号温度底部是天气现象、湿度和风力的组合。用 LinearLayout 加垂直方向就能搞定嵌套少、逻辑清楚适合作为模板改造成其他样式。LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp TextView android:idid/tv_city android:layout_widthwrap_content android:layout_heightwrap_content android:textSize18sp android:text定位中... / TextView android:idid/tv_temp android:layout_widthwrap_content android:layout_heightwrap_content android:textSize40sp android:text-- / TextView android:idid/tv_weather android:layout_widthwrap_content android:layout_heightwrap_content android:textSize16sp android:text-- / /LinearLayouttv_city 是城市名tv_temp 是温度主展示tv_weather 是天气、湿度、风向的汇总行。XML 里的默认文本我给的是占位符而不是空字符串这样请求还没返回时界面不会白屏。想换成卡片风格把外层 LinearLayout 换成 MaterialCardView内部逻辑代码不用动。如果还要加刷新控件就在这个 LinearLayout 外面再包一层 SwipeRefreshLayout后面进阶章节会提到。4.2 城市切换内置列表加 SharedPreferences 记住选择城市切换是这份源码里交互部分的核心。点击顶部城市名弹出一个选择器选中之后重新请求天气数据。城市数据用一个二维数组维护public class CityData { // 格式城市名, 城市编码 public static final String[][] CITIES { {A市, 101010100}, {B市, 101020200}, {C市, 101030300} }; }城市编码是服务商要求的区域代码不同数据源编码规则不一样换数据源时要一起换掉。这里用 A 市、B 市、C 市做示例实际使用时替换成你需要的城市名和对应编码即可。选择城市后把当前选择存到 SharedPreferencesSharedPreferences sp getSharedPreferences(weather_config, MODE_PRIVATE); sp.edit() .putString(last_city_name, cityName) .putString(last_city_code, cityCode) .apply();MODE_PRIVATE 表示只有当前应用能访问数据量很小不需要上数据库。apply() 是异步写磁盘不会阻塞主线程。下一次启动时在 onCreate 里读出来String cityName sp.getString(last_city_name, A市); String cityCode sp.getString(last_city_code, 101010100);默认值设为 A 市用户第一次打开不用做选择就能看到天气数据。这个持久化逻辑几乎适用于所有“记住用户选择”的场景换成其他配置项只改 key 名和默认值就行。4.3 权限声明与网络状态判断没有权限一切网络请求都是空谈。检查 AndroidManifest.xml 有没有这两行uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /INTERNET 是发起网络请求的基础权限ACCESS_NETWORK_STATE 用来在请求前判断当前网络是否可用。我习惯在请求前先做一次网络判断而不是直接发请求再等失败回调ConnectivityManager cm (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info cm.getActiveNetworkInfo(); if (info null || !info.isConnected()) { Toast.makeText(this, 网络不可用, Toast.LENGTH_SHORT).show(); return; }这段代码里 isConnected() 为 false 就提示用户不会让界面卡在加载状态。如果数据源只提供明文 HTTP 接口还需要在 application 节点加 android:usesCleartextTraffictrue但这会降低安全性我的建议是优先换用支持 HTTPS 的数据源。5. 避坑排查编译失败、Key 失效与数据不显示的处理方法5.1 Gradle 同步过不去报错 JAVA_HOME 或 SDK location not found现象打开工程后AndroidStudio 一直在转圈Sync 阶段报错找不到 JDK 或 SDK location not found工程目录旁边全是红叉。原因本地 JDK 版本和工程要求的 Gradle 版本不匹配或者 Android SDK 没有安装在默认位置。最常见的情况是 AndroidStudio 内置的 JDK 和命令行配置的 JDK 不是同一个版本Gradle 认了其中一个另一个却对不上。解决在 AndroidStudio 的 Settings 里打开 Gradle 设置手动选择本地 JDK 17SDK 路径在 local.properties 中显式指定。改完再点一次 Sync Project基本能恢复正常。如果还报错就把 gradle-wrapper.properties 里的 distributionUrl 换成你本地已有的 Gradle 版本让 AndroidStudio 不重新下载。5.2 请求返回 403Key 校验失败现象Logcat 打印出 HTTP 403 Forbidden界面一直显示转圈或者加载失败。原因服务商在校验请求时发现 code 里的 applicationId 和控制台填的不一致或者 Key 本身就是从别的项目复制的。还有一种情况是你换了电脑debug.keystore 重新生成了签名 SHA1 变了但控制台里填的还是旧指纹。解决先核对 Constant 类里的 Key 是否和控制台一致再核对 applicationId 与控制台填写的包名是否完全相同注意大小写和标点。如果服务商要求签名校验回到 3.2 节执行一次 keytool 命令把新的 SHA1 更新到控制台。从那以后我拿到任何带 Key 的源码第一件事就是核对包名和指纹能省掉一大半网络层排错时间。5.3 请求失败但日志里没有异常明文 HTTP 被 Android 拦了现象请求地址明明是 http:// 开头代码也没写错但回调一直走 onFailure日志里出现 CLEARTEXT communication not permitted 类似字样。原因targetSdk 28 及以上版本Android 系统默认禁止明文 HTTP 流量。这是系统层面的安全限制不是代码 bug。解决优先让数据源支持 HTTPS把 BASE_URL 改成 https:// 就行如果只是为了本地调试在 AndroidManifest.xml 的 application 节点加 android:usesCleartextTraffictrue。我不会把这个属性带到正式包因为明文流量在公共网络环境下一抓就是一批数据代价有点大。5.4 数据拿回来了但界面全是空JSON 字段映射对不上现象网络请求成功了日志显示 200但界面上的温度、湿度都是空字符串或者显示默认的 --。原因服务商返回的 JSON 字段名和 WeatherBean 里的 SerializedName 不一致。比如接口返回的是 txt数据类里写的是 textGson 找不到对应字段就直接给 null。另一个常见原因是 JSON 嵌套层级比数据类深一层now 节点被包在 data 节点里字段全错位。解决把 Logcat 里打印的原始 JSON 复制出来和 WeatherBean 里的字段逐层对照。调试阶段我经常把 JSON 存到本地文件里离线看比对完再改注解值。改完一个字段就重新请求一次别一次性改一堆再验证否则出了问题不好定位是哪个字段错的。5.5 界面闪退切换城市时 NullPointerException现象点开城市选择器选择靠后几个城市时直接闪退Logcat 报 NullPointerException位置指向城市编码读取。原因城市数组的编码字段为空或者索引越界。有些源码手工维护城市列表增删城市时漏掉了对应的编码后面请求把 null 拼进 URL网络层直接抛异常。解决给城市数组加防护逻辑if (index 0 || index CityData.CITIES.length) { return; }更稳妥的做法是城市列表只展示城市名编码跟着名称走选择时始终从数组里取成对的数据不依赖外部传入的索引。6. 验证与进阶从 Logcat 排查到定时刷新6.1 在 Logcat 里验证完整请求链路验证数据是否真的来自网络而不是代码里写死的假数据最直接的方式是在请求回调里打日志。主界面的请求核心代码apiService.getWeather(cityCode, Constant.WEATHER_KEY) .enqueue(new CallbackWeatherBean() { Override public void onResponse(CallWeatherBean call, ResponseWeatherBean response) { if (response.isSuccessful() response.body() ! null) { WeatherBean bean response.body(); tvTemp.setText(bean.now.temp ℃); Log.d(Weather, city bean.city , temp bean.now.temp); } } Override public void onFailure(CallWeatherBean call, Throwable t) { Log.e(Weather, request failed, t); } });Retrofit 的 enqueue 是异步方法回调默认切回 Android 主线程所以可以在 onResponse 里直接操作 UI。看到 Logcat 里打出 city 和 temp 日志就说明从界面到服务商的整条链路是通的。如果只有失败日志按第 5 章的顺序逐个排查。6.2 把手动刷新升级成定时轮询如果想让界面每隔一段时间自动更新最简单的做法是 Handler 加 postDelayedprivate final Handler handler new Handler(Looper.getMainLooper()); private final Runnable refreshTask new Runnable() { Override public void run() { requestWeather(cityCode); handler.postDelayed(this, 30 * 60 * 1000L); } }; Override protected void onStart() { super.onStart(); handler.post(refreshTask); } Override protected void onStop() { super.onStop(); handler.removeCallbacksAndMessages(null); }30 分钟换算成毫秒写出来比直接写 1800000 更容易看懂。注意 onStop 里要移除回调否则 Activity 不可见时请求还在继续白耗流量。这套写法只适合本地练手生产环境我一般会换 WorkManager 做周期任务它能在系统省电策略下更合理地调度。从那以后我拿到一份天气类源码第一件事不是看界面而是先看刚才这几处SharedPreferences 的默认值、BASE_URL 是 http 还是 https、以及 JSON 字段映射的数据类。这三个地方顺手了整个工程基本就能跑通。希望帮到你。本文还有配套的精品资源点击获取