拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android城市天气预报源码实战:导入配置、数据源替换与发布检查

简介这是面向Android初学者和课程设计人群的城市天气预报App源码工程以Java/Kotlin实现了完整的天气查询场景覆盖从网络接口获取数据到界面渲染的常用链路。压缩包共40个文件包含8个java源文件、16个class编译文件、3个xml布局文件、1个可直接安装的apk以及配置文件、图标资源和源码说明txt整体仅230KB结构清晰轻量。已有443人学习下载适合作为理解Android网络编程的入门实例。通过研读源码可重点掌握使用Retrofit/OkHttp这类HTTP库请求天气API、JSON解析、RecyclerView或ListView展示多城市天气、SharedPreferences保存用户偏好等技能同时项目还涉及异步任务、生命周期回调、权限声明和图片加载优化源码说明txt对模块划分与关键实现有补充注释方便边读边改。对于正在做移动开发课设或准备面试的读者这是一个能在小体量中快速建立整体认知的良好范本。1. 城市天气预报源码不是玩具先弄清 ZIP 里装的是什么拿到「Android 城市天气预报源码.zip」多数人的第一反应是解压、用 Android Studio 打开、点 Run然后在 Gradle 同步阶段被一串红色报错劝退。天气预报 App 几乎覆盖了客户端核心链路网络请求、JSON 解析、列表渲染、本地存储、定位权限是最经典的实战入门项目。但正因为经典这个标题下的源码包质量差异极大课程设计、培训机构 Demo、GitHub 克隆重新打包三种来历对应三种完全不同的代码风格和依赖栈。这里默认你手里有这样一个 ZIP目标是先本地跑通再换上自己的天气数据源最后改造成可发布的形态。新手按顺序从环境对齐开始熟手可以直接跳到第三章的数据源替换和最后一章的发布前检查。全文按「看骨架、对齐环境、换数据源、改交互、做收尾」推进不假定源码出自谁手只按这类项目最常见的技术栈讲一套能落地的路径。2. 导入前的骨架检查ZIP 内容、工程结构与版本对齐2.1 解压后先判断源码来历课程设计、Demo 还是 GitHub 重打包打开 ZIP 之前先用压缩软件看一眼文件分布这一步能省下大量排错时间。课程设计类源码的特征很典型包名是 com.example.xxx只有一个 MainActivity网络请求用 HttpURLConnection 或者早已废弃的 AsyncTask 直接写在 Activity 里界面是一层 LinearLayout 套若干 TextView。培训机构 Demo 类源码通常有简单的 MVP 分层依赖了 OkHttp 或 Volleyassets 目录下多半有一个城市列表 JSON但类名和注释风格很随意。GitHub 克隆后重新打包的源码会保留 .git、README、LICENSE 这些痕迹甚至带着多个 module这类工程一般能编译但依赖版本往往落后当前环境三年以上。判断工具不复杂。macOS 或 Linux 下执行unzip -l 文件名.zip查看完整清单Windows 下用 7-Zip 或资源管理器预览重点关注根目录有没有 settings.gradle、gradlew 脚本、.git 目录以及 app/src/main 下 java 和 res 的目录深度。解压时容易丢掉符号链接和隐藏文件发现缺少 .gradle 或 build 目录不用在意那本来就是编译产物但如果连 settings.gradle 都不存在说明这份 ZIP 只导出了部分源码需要手动补一个 Gradle 工程外壳才能被 Android Studio 识别。另外如果 ZIP 本身加了密码而提供方联系不上先看压缩包自带的注释和 README很多老旧源码把密码写在注释里这比找第三方工具更直接。2.2 导入前必查的四个关键文件解压完成后不要急着打开 IDE先在命令行里确认四个位置的内容。根目录的 settings.gradle 和 build.gradle 决定工程模块结构和插件版本app/build.gradle 里是 compileSdk、minSdk、targetSdk 以及依赖列表AndroidManifest.xml 重点看权限声明和 application 节点配置源码里负责网络请求的类通常叫 Api、HttpUtil 或 XXXService里面能找到写死的服务器地址和 API key。检查对象看什么发现问题时的对策settings.gradlepluginManagement 与模块列表缺文件就用新建空工程的模板补齐app/build.gradlecompileSdk、minSdk、targetSdk按 2.3 的对照表调整 AGP 与 SDKAndroidManifest.xmlINTERNET 权限、usesCleartextTraffic补权限明文流量配置见 6.2网络请求入口类域名与 key 写死的位置替换为 3.2 选定的数据源这四类信息直接决定导入后第一批报错的性质。权限声明缺失会导致请求直接抛 SecurityException明文流量未开启会让 http 接口在 Android 9 及以上直接失败SDK 版本与本机安装的 Android SDK 不匹配会在同步阶段就报错。先把这四个位置看清再打开 Android Studio报错量能少一半以上。2.3 AGP 版本与 Gradle、JDK 的兼容对照新版 Android Studio 安装包会自带对应的 Android SDK Platform 和 JDK但源码包里的 Gradle 插件AGP版本是写死的能不能编译取决于三者是否对齐。下面是常用的兼容对照AGP 版本最低 Gradle推荐 JDK典型场景8.18.017近两年的新工程或新迁移工程7.47.511 或 172022 年前后的工程存量最大7.07.011已迁移 AndroidX 的中期工程4.26.7.18 或 11老课程设计和培训 Demo 重灾区升级 AGP 不是改个版本号那么简单。AGP 从 4.x 跳到 7.x 时support 库与 AndroidX 不能共存Manifest 里手动注册的组件要做迁移从 7.x 跳到 8.x 时构建脚本里很多 deprecated API 被直接移除。我的建议是能跑就不升优先让本机 JDK 与 Gradle 版本去匹配旧的 AGP。只有旧 AGP 与当前 Android Studio 完全不兼容时才按表升一档并且升级后立刻跑一遍编译验证。如果 SDK Manager 里部分 Platform 组件无法勾选或下载失败先检查磁盘剩余空间和安装路径的写入权限这比反复重装 Android Studio 高效得多。2.4 不打开 IDE 也能完成的首次编译用工程自带的 gradlew 做首次编译比 IDE 同步界面信息更完整。命令行进入工程根目录后执行# 使用工程自带的 ./gradlew而非系统全局 gradle保证版本一致 cd weather-project ./gradlew :app:assembleDebug --stacktraceWindows 下使用gradlew.bat :app:assembleDebug --stacktracePowerShell 执行脚本前可能需要调整执行策略。--stacktrace的作用是打印完整调用栈老工程的报错经常被 Gradle 吞掉关键行加上它才能看到真正原因。编译成功后 APK 输出在app/build/outputs/apk/debug/app-debug.apk可以先拿这个包做安装冒烟测试不必等 IDE。编译失败时优先看两类日志。Could not find ...说明某个依赖版本在仓库里不存在把 build.gradle 中出错的版本改成当前可用版本即可Unsupported class file major version说明 JDK 版本不匹配按 2.3 的表格调整。这里最常见的坑是同时改了 AGP 和 Gradle导致排查时分不清是哪一层出的问题正确做法是只动一项验证通过再动下一项。3. 更换天气数据源API 选型、城市编码与 JSON 模型3.1 先定位源码里写死的请求地址与密钥绝大多数天气源码会把接口域名和 key 写死在某个常量类里替换数据源的第一步就是把这些硬编码找出来。在工程目录下执行# 递归搜索 java 目录忽略大小写并显示行号找出所有网络地址与密钥 grep -rniE http|appkey|api[_-]?key|weather app/src/main/java/ | head -40-r递归搜索-i忽略大小写-n显示行号head -40只取前 40 行避免刷屏。找到的每一处 http 字符串都要核对哪些是天气接口域名哪些是图标拼接地址哪些只是注释里的示例。图标地址容易被遗漏换数据源后图标 URL 风格不同会导致列表图片大面积加载失败这一点第 6 章还会专门讲。3.2 免费天气接口怎么选天气数据源决定整个项目的城市编码体系和返回字段结构选型需要提前想清楚。国内城市为主的项目和风天气QWeather最省事城市 ID 与国内行政区划一致开发版免费额度对个人项目够用返回标准 UTF-8 JSON有海外城市需求或用户量可能增长OpenWeatherMap 的免费层约每天 1000 次调用支持城市名和经纬度两种定位如果后续要做地图功能也可以直接使用地图平台自带的天气能力。数据源城市定位方式免费额度返回格式主要限制和风天气CityID / 经纬度开发版每日有限额UTF-8 JSON部分高级接口需要企业认证OpenWeatherMap城市名 / 经纬度约 1000 次/日UTF-8 JSON免费档没有分钟级预警原中国天气网接口城市 ID无 key 要求编码不固定无 SLA字段多次变动额度数字各平台调整频繁接入前以官方最新公告为准。更值得关注的是编码问题原中国天气网这类历史接口返回的可能不是 UTF-8而 Android 默认按 UTF-8 解析3.5 会给出兼容写法。如果源码原本就用它换到和风天气后不止要改域名和 key城市 ID 表也要整套替换。3.3 城市编码表放 assets不写死在代码里天气接口无一例外需要城市标识国内接口习惯用城市 ID海外接口习惯用经纬度或城市名。把城市编码表维护成 assets 下的 JSON 文件比写死在 Java 类里好维护得多。文件格式保持扁平数组按拼音排序方便搜索[ {name: 北京, id: 101010100, pinyin: beijing}, {name: 上海, id: 101020100, pinyin: shanghai}, {name: 广州, id: 101280101, pinyin: guangzhou} ]加载代码用 Gson 解析成对象列表// CityRepository.java从 assets 目录读取城市列表 public static ListCity loadCities(Context context) { String json null; try (InputStream is context.getAssets().open(cities.json)) { byte[] buf new byte[is.available()]; is.read(buf); json new String(buf, UTF-8); } catch (IOException e) { return new ArrayList(); } return new Gson().fromJson(json, new TypeTokenListCity(){}.getType()); }这段代码有两个细节。is.available()在 assets 文件上通常能拿到准确大小但严格写法应该用固定缓冲区循环读取这里为简洁作了省略。TypeToken是必须的否则泛型 List 会被 Gson 解析成 LinkedTreeMap运行时强转直接崩溃。城市文件放 assets 而不是 res/raw是因为 assets 支持子目录与任意嵌套后续按省分组时不用改读取逻辑。3.4 Retrofit 接口定义与超时参数老源码里最常见的网络写法是 HttpURLConnection 加子线程代码长且难复用。既然要换数据源顺手把请求层换成 Retrofit 是性价比最高的改造。接口定义如下// WeatherApi.java定义接口与请求参数 public interface WeatherApi { GET(v7/weather/now) CallWeatherNowBean getNow(Query(location) String cityId, Query(key) String apiKey); }// RetrofitClient.java单例构建 Retrofit Retrofit retrofit new Retrofit.Builder() .baseUrl(https://devapi.qweather.com/) .addConverterFactory(GsonConverterFactory.create()) .client(new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build()) .build();GET的路径与baseUrl拼成完整地址location接收城市 IDkey是开发者密钥。超时时间按移动网络真实水平设置10 秒是 Android 项目的通用值设太短在人流密集区域会频繁超时设太长会让失败提示滞后。OkHttpClient 单独构建是为了后续加拦截器打日志不要在 onBindViewHolder 里重复创建 Retrofit 实例。请求回调切回主线程更新 UIRetrofit 的 enqueue 默认实现了这一点如果用 execute 则需要自己处理线程切换。3.5 老接口 GBK 编码的兼容写法国内老接口有一个历史包袱返回内容可能是 GBK而 Retrofit 的字符串转换器默认 UTF-8。遇到乱码时不要改全局转换器精准的处理方式是拿原始字节手动解码// 对 CallResponseBody 的响应做手动解码再交给 Gson String raw new String(response.body().bytes(), GBK); WeatherNowBean bean new Gson().fromJson(raw, WeatherNowBean.class);先取原始字节再指定 GBK 字符集最后交给 Gson 解析三步缺一不可。判断是否需要这一步很简单中文城市名显示为问号或乱码而数字日期正常基本就是编码问题。换到现代接口后这段代码可以整体删掉它的价值在于当数据源无法更换时这是最小侵入的修复。4. 核心交互重写进度条、列表渲染与多城市切换4.1 进度条与错误状态的三态管理天气 App 的主页面本质上是一个状态机加载中、成功、失败三种状态下界面控件的可见性完全不同。很多老源码只处理了加载中和成功失败时页面白屏用户完全不知道发生了什么。推荐用一个方法统一切换// MainActivity 中统一切换界面状态 private void renderState(boolean loading, String error) { progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); mRecyclerView.setVisibility(!loading error null ? View.VISIBLE : View.GONE); mErrorView.setVisibility(error ! null ? View.VISIBLE : View.GONE); mErrorView.setText(error ! null ? error : ); }状态progressBarRecyclerView错误文案加载中显示隐藏隐藏请求成功隐藏显示隐藏请求失败隐藏隐藏显示原因android 进度条在 XML 里推荐用确定尺寸的 ProgressBar 配居中布局嵌在加载容器里而不是直接铺满屏幕。失败文案要区分「网络不可用」和「数据解析失败」前者提示用户开网络后者说明 key 失效或接口结构变化。这里容易被忽视的是回调线程Retrofit 的 enqueue 已经在主线程回调但使用 execute 或自建线程池时必须用 runOnUiThread 包裹 UI 操作。4.2 用 RecyclerView 适配逐日预报列表主列表用 RecyclerView 承载未来几天的预报数据适配器不处理业务逻辑只把 bean 渲染到 item 控件上// DailyAdapter.java逐日预报适配器 public class DailyAdapter extends RecyclerView.AdapterDailyAdapter.VH { private final ListDailyBean data; public DailyAdapter(ListDailyBean data) { this.data data; } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_daily, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { DailyBean d data.get(position); holder.tvDate.setText(d.getDate()); holder.tvTemp.setText(d.getTempMin() °C ~ d.getTempMax() °C); } Override public int getItemCount() { return data.size(); } static class VH extends RecyclerView.ViewHolder { TextView tvDate, tvTemp; VH(View v) { super(v); tvDate v.findViewById(R.id.tv_date); tvTemp v.findViewById(R.id.tv_temp); } } }item 布局在 Android Studio 里右键 res/layout 新建时选 Item 类型即可。item 根布局宽度必须设成 match_parent设成 wrap_content 会导致 item 只占内容宽度看起来像列表错位。onCreateViewHolder 只在需要新 View 时调用onBindViewHolder 负责每次复用时的数据填充所以不要在 onCreateViewHolder 里绑定数据。天气这类小数据量列表直接用notifyDataSetChanged()即可不必引入 DiffUtil。4.3 多城市管理用 SharedPreferences 存城市列表完整形态需要支持添加、切换、删除城市城市列表用一个 JSON 字符串存进 SharedPreferences 是最轻量的方案不必为它引入数据库// CityStorage.java城市列表的读写 private static final String PREFS app_config; private static final String KEY_CITIES city_list; public static ListString loadCities(Context ctx) { String json ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_CITIES, []); return new Gson().fromJson(json, new TypeTokenListString(){}.getType()); } public static void addCity(Context ctx, String cityId) { ListString list loadCities(ctx); if (!list.contains(cityId)) { list.add(0, cityId); // 新城市插到首位作为默认选中 String json new Gson().toJson(list); ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .edit().putString(KEY_CITIES, json).apply(); } }apply()是异步写盘不阻塞主线程进程被杀时会丢最后一次修改对天气这种低频写入可以接受高频写入才需要 commit 或改用 DataStore。列表里存的是城市 ID 而不是城市名城市名只用于展示避免名称相同而 ID 不同的边缘情况。切换城市就是改一个 int 下标再触发一次网络请求。4.4 SwipeRefreshLayout 下拉刷新下拉刷新是天气 App 的标配交互用 androidx 的 SwipeRefreshLayout 包住 RecyclerView两步接入// 设置下拉刷新回调 SwipeRefreshLayout refresh findViewById(R.id.refresh); refresh.setOnRefreshListener(() - fetchWeather(currentCityId)); refresh.setColorSchemeResources(R.color.blue);!-- activity_main.xml 外层套 SwipeRefreshLayout -- androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:idid/refresh android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/recycler android:layout_widthmatch_parent android:layout_heightmatch_parent/ /androidx.swiperefreshlayout.widget.SwipeRefreshLayout请求完成后必须调用refresh.setRefreshing(false)关闭转圈放在回调成功的 finally 里执行否则圆圈会一直转。Vertical 滑动方向与 RecyclerView 不冲突但 SwipeRefreshLayout 内再套 NestedScrollView 时刷新回调可能被吞遇到这种情况检查内层控件是否消费了触摸事件。5. 离线缓存与刷新策略天气数据在弱网下的可用性5.1 天气缓存存哪SharedPreferences 与文件缓存的取舍网络请求成功后把完整响应 JSON 缓存一份是天气 App 成本最低的体验提升。缓存位置有两种选择效果差异很大。SharedPreferences 适合存小对象比如城市列表、上次选中的城市下标完整天气响应包含多日数组体积可能几十 KB塞进 SharedPreferences 会出现解析开销大、调试困难的问题。更合理的是文件缓存存到getCacheDir()或getFilesDir()下的 JSON 文件读取一次磁盘 IO 完成。存储方式适用数据清除时机注意点SharedPreferences城市列表、配置项卸载时大 JSON 解析卡顿cacheDir 文件单城市天气缓存系统空间不足自动清可能被系统提前清理filesDir 文件需要持久保留的数据卸载时体积不可控SQLite / Room多表关联、历史数据按业务对天气项目属于过度设计天气预报是时效性数据缓存丢了不心疼优先 cacheDir如果希望用户关网后仍能看到上次数据改用 filesDir 更稳妥。要不要引入 Room取决于源码本身是否已有数据库依赖没有的话为单个 JSON 建表不值得。5.2 过期时间与「先显示缓存再拉新」缓存不是无限期的天气数据超过一小时参考价值就很低。加载策略采用经典的「先缓存后网络」界面先立刻显示缓存后台静默请求新数据成功就刷新列表并更新缓存失败则保留缓存并提示网络异常。// CacheManager.java带过期时间的缓存读写 private static final long EXPIRES 30 * 60 * 1000L; // 30 分钟 public static String get(Context ctx, String cityId) { SharedPreferences sp ctx.getSharedPreferences(weather_cache, Context.MODE_PRIVATE); long ts sp.getLong(cityId _ts, 0); if (System.currentTimeMillis() - ts EXPIRES) return null; return sp.getString(cityId, null); } public static void put(Context ctx, String cityId, String json) { SharedPreferences sp ctx.getSharedPreferences(weather_cache, Context.MODE_PRIVATE); sp.edit().putString(cityId, json) .putLong(cityId _ts, System.currentTimeMillis()) .apply(); }这里用 SharedPreferences 承载缓存是因为单城市 JSON 体积可接受配合时间戳足够5.1 提到的文件方案适合更复杂的响应结构两种可以共存。过期判断放在读取时而不是写入时省掉一个定时任务。EXPIRES按场景调整提醒类天气应用可以压到 5 分钟普通展示类 30 分钟是体验和数据成本之间的平衡点。5.3 冷启动渲染顺序与多城市缓存命中冷启动时如果先等网络再画界面弱网下用户盯着进度条两三秒体验很差。合理顺序是读取持久化的城市列表对每个城市先取缓存渲染再逐个发起网络刷新。多城市场景下刷新请求要串行执行而不是并发发出去避免瞬间占用大量连接实现上用 for 循环依次 enqueue每个回调里更新对应位置的列表项。判定缓存是否命中有一个标准验证动作完成一次在线加载后切到飞行模式再冷启动。界面显示上一次数据说明缓存读写和时序是对的白屏则检查 get 方法是否在 UI 线程调用、过期时间戳是否写成了存入时的时间。这个验证方法在第 6 章还会配合 logcat 再做一遍用来确认请求确实没有发出去。6. 发布前的最后检查混淆规则、权限裁剪与缓存验证6.1 为 Gson 模型保留字段开启混淆后Gson 依赖反射读取字段名字段被重命名成 a、b、c 会导致解析结果全为 null。规则是在 proguard-rules.pro 里保留模型的包路径# 天气数据模型全部保留字段避免混淆后 JSON 解析为空 -keep class com.example.weather.model.** { *; } -keepattributes Signature -keepattributes *Annotation*如果源码是 Kotlin还要保留 kotlin 反射相关的默认规则。更精细的做法是只对带 SerializedName 的字段做 keep但模型类数量少整包 keep 最简单可靠。验证方式是打 release 包安装用下面的 logcat 命令观察是否有 Gson 解析异常。6.2 权限裁剪与明文流量配置天气 App 实际需要的权限只有网络和定位老源码里经常混入相机、读取联系人这类无关权限发布前全部删掉。权限越少审核越顺利也减少安装时的心理负担uses-permission android:nameandroid.permission.INTERNET/ uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE/ uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ application android:usesCleartextTraffictrue很多免费天气接口是 http 明文Android 9 及以上默认禁止usesCleartextTraffictrue是全局放行。对数据敏感的应用不建议全局放行推荐用 networkSecurityConfig 在域名维度只放行特定 http 主机。ACCESS_COARSE_LOCATION 用于按当前城市定位功能不做定位就一并删掉相关的运行时权限申请代码也能整体移除。6.3 用 logcat 验证请求与缓存行为发布前的验证不依赖界面用 adb 看日志最可靠# 过滤 OkHttp 日志确认请求是否发出、响应码与耗时 adb logcat -s okhttp.OkHttpClient:D 21 | grep weather # 冷启动 App观察主线程渲染与生命周期日志 adb logcat -s ActivityManager:I MainActivity:V以缓存验证为例操作序列是完成一次在线加载打开飞行模式冷启动 App。logcat 里看不到任何 weather 请求日志、界面显示上一次数据即为缓存生效。缓存没生效时先确认 get 方法是否被调用再看时间戳比较逻辑多数问题出在把System.currentTimeMillis() - ts EXPIRES写成了ts EXPIRES。用同样的日志还能发现重复发起的请求比如每次 onResume 都触发网络请求导致流量翻倍这是老源码里常见的隐藏问题grep 请求路径统计次数即可确认。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门