基于Android的天气预报App源码解析:从数据接入到APK打包全流程
发布时间:2026/9/14 5:52:42 作者:尧图编辑部 阅读量:1,286

简介面向Android毕业设计与课程设计的天气预报APP完整源码适合有Java/Android基础、需快速搭建或参考功能实现的本科生。项目以城市天气查询为核心左侧城市列表支持点击切换展示实时天气、温度、星期与城市名片并简要呈现未来6天天气趋势。源码涵盖页面布局、数据解析、列表交互等核心模块可帮助理解安卓应用从界面到逻辑的完整开发流程。压缩包共24个文件约5.02MB包含png图片资源、xml布局与配置文件、jar依赖包、md说明文档以及可直接安装的apk文件图片与xml支撑界面显示jar用于天气数据处理md便于项目阅读。目前已有1597人学习下载适合毕业设计选题、天气类应用练习或二次改造研读代码可掌握城市列表联动、天气信息展示及多日预报实现思路。1. 拿到“基于Android的天气预报APP系统源码.zip”后第一件事你下载了一个名为“基于Android的天气预报APP系统源码.zip”的压缩包解压后看到的是一堆gradle文件、app目录和若干截图。绝大多数人第一反应是双击项目直接Sync然后被SDK路径、JDK版本、依赖下载失败三连击拦住。这个标题背后是一个本科毕业设计级别的Android工程从天气数据接口到城市搜索再到七日预报展示覆盖一个App所需的最小闭环。它适合两类人一类是需要交付课程设计的学生一类是想拿天气模板做内部工具的开发者。要把这套源码真正跑起来顺序不是先看代码而是先确认三件事天气数据从哪来、接口失效了怎么办、要不要做离线缓存。这三个问题直接决定了网络层、数据层和界面的整体结构。2. 天气预报App的技术栈选型与天气数据接口接入2.1 为什么天气数据来源决定整体架构天气App在业务上就是一个“请求—解析—缓存—展示”的四层结构。很多人拿到源码就把注意力放在RecyclerView的布局和圆角卡片上但真正决定架构的是数据从哪来。直接请求第三方开放接口最常见比如和风天气、心知天气、聚合数据这类平台申请key后按文档拼URL返回JSON这些接口通常有免费额度足够演示。数据来源也影响包体结构和权限配置如果接口走的是httpsAndroidManifest里什么都不用加如果图省事用了http就得考虑高版本Android的明文流量限制这个坑在第五章单独讲。请求失败时展示什么决定是否引入数据库。只做当天天气一个SharedPreferences就够做七日预报和城市收藏就要上Room或SQLite。最后“城市切换是内置还是搜索”决定要不要维护一份城市列表文件。把这三个问题在动手前定下来代码边界就清楚了。2.2 常见组件选型与各自职责一个能跑又能讲清楚的天气Demo通常不需要引入重型依赖。下表是课程设计源码里最稳的组合组件职责在本项目中的用法Retrofit OkHttp网络请求拉取实时天气和七日预报GsonJSON解析把响应解析成Kotlin数据类ViewModel LiveData状态管理持有加载状态、城市数据Room本地缓存缓存最近一次天气、城市SharedPreferences轻量存储记住上次选中的城市RecyclerView列表展示七日预报、城市搜索结果Retrofit比自带HttpURLConnection的优势不是执行速度而是把接口描述统一收敛成注解拦截器可以统一处理日志、重试和错误码。我一般会把网络层单独抽成一个包这样即便接口域名变了也只需要改一个文件。如果你拿到的zip是Java写的组件选型不变只是Kotlin协程那部分要改成Callback回调。2.3 最小可用的Gradle依赖配置打开源码zip的根目录一般会有build.gradle和settings.gradle。先确认Gradle插件版本和本机Android Studio版本是否兼容以一份常见模板为例依赖最小集合如下plugins { id com.android.application version 8.1.4 apply false id org.jetbrains.kotlin.android version 1.9.22 apply false } android { namespace com.example.weather compileSdk 34 defaultConfig { applicationId com.example.weather minSdk 23 targetSdk 34 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 implementation androidx.room:room-runtime:2.6.1 kapt androidx.room:room-compiler:2.6.1 }这一段有三个参数需要特别说明。minSdk设23是因为这套代码要使用运行时权限和通知渠道再低就得写大量兼容分支没有意义compileSdk和targetSdk设34能覆盖大部分新机但targetSdk 34会启用后台位置限制和更严格的导出规则Manifest里的Activity必须显式配android:exported否则安装时直接解析失败。如果zip里的代码是纯Java插件部分换成java-library配置即可依赖不变。2.4 用Retrofit定义天气查询接口并跑通第一个请求网络层要做的事就两件定义接口、创建客户端。接口定义写法interface WeatherService { GET(weather/city) suspend fun fetchWeather( Query(city) city: String, Query(key) apiKey: String ): WeatherResponse }suspend关键字让这个函数可以在协程里直接调用不会阻塞主线程Query注解把city和apiKey拼到URL尾部返回类型直接指定为数据类Gson转换器会自动做JSON到对象的映射。客户端单例按通用做法写object WeatherClient { private val okHttpClient OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BASIC }) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build() val service: WeatherService by lazy { Retrofit.Builder() .baseUrl(https://api.example.com/) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(WeatherService::class.java) } }connectTimeout和readTimeout都设10秒是天气类接口的通用经验值。太短一到弱网就抛SocketTimeoutException太长界面会一直转圈。日志级别用Level.BASIC只能看到URL和响应码既够排查问题又不会被完整JSON刷屏。baseUrl必须以斜杠结尾这是Retrofit的硬性要求否则启动时会抛IllegalArgumentException。首次跑请求建议先从日志确认URL和key拼接正确再去看返回JSON的字段名。3. 天气数据解析、城市切换与本地缓存落地3.1 天气数据模型设计所有字段都可空第三方天气接口返回的JSON字段不稳定可能这个城市缺湿度那个城市没有风向。如果数据类里把字段定义成非空IntGson解析时遇到缺字段直接抛异常整个天气列表都渲染不出来。所以设计模型时遵循一条原则展示层能容忍空值就用可空类型并给默认值。data class Daily( SerializedName(date) val date: String? null, SerializedName(tempMax) val tempMax: String? null, SerializedName(tempMin) val tempMin: String? null, SerializedName(textDay) val textDay: String? null, SerializedName(windDir) val windDir: String? null, SerializedName(humidity) val humidity: String? null ) data class WeatherResponse( SerializedName(city) val city: String? null, SerializedName(daily) val daily: ListDaily? null, SerializedName(status) val status: Int -1 )温度和湿度用String而不是Int是因为展示层要拼字符串“12℃~18℃”String直接插值最省事也避免空安全转换的样板代码。SerializedName必须和接口返回字段精确一致两边大小写差一个字母就可能拿到null。还有一个容易忽略的点daily字段本身可能缺失或字段名不叫daily比如有些接口叫“data”这时候要么改注解要么在解析后用扩展函数做一次二次映射。3.2 城市数据内置与模糊搜索不搭后端的情况下城市数据最稳妥的存在位置是assets/city.json。做法是启动时读一次解析成List 放内存搜索时直接filter。常见模板里这个文件通常只覆盖省会城市演示时输入一个县级市就搜不到。拿到源码zip后先检查city.json的大小如果只有几十行就要补充。搜索逻辑data class City( val name: String, val province: String, val pinyin: String ) fun searchCities(cities: ListCity, keyword: String): ListCity { val kw keyword.trim().lowercase() return cities.filter { it.name.contains(kw, ignoreCase true) || it.pinyin.contains(kw) || it.province.contains(kw, ignoreCase true) } }pinyin字段放的是“beijing”这种全拼搜索时把用户输入的拼音转成小写再比。contains在几百条数据上只是毫秒级操作不需要倒排索引也无需引入Room的FTS。搜索结果直接用RecyclerView展示点击之后把城市名回传到ViewModel触发重新请求这一层不需要存储中间状态。3.3 Room与SharedPreferences的取舍离线缓存有两种写法。只缓存当前城市的当天天气用SharedPreferences存一个JSON字符串半小时内读取直接生效成本和复杂度最低。要想离线还能看七日预报建议Room落库。两者的边界可以这样看维度SharedPreferencesRoom数据类型简单键值对结构化数据七日预报缓存需要整体序列化一个String逻辑混乱按城市存行查询灵活依赖与编译无额外依赖引入room-runtime和kapt编译器适合场景只存上次选中的城市缓存多城市多日天气一张表就能解决问题Entity(tableName weather_cache) data class WeatherCache( PrimaryKey val city: String, ColumnInfo(name daily_json) val dailyJson: String, ColumnInfo(name update_time) val updateTime: Long ) Dao interface WeatherDao { Query(SELECT * FROM weather_cache WHERE city :city) suspend fun getByCity(city: String): WeatherCache? Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(cache: WeatherCache) }把整个daily列表用Gson转成JSON字符串存在一列读取时再反序列化比建主从表少很多样板。城市名当主键同一个城市重复写入时会走REPLACE策略直接覆盖。updateTime字段记录写入时间读取时判断是否超过30分钟超过就重新拉接口。这个逻辑在ViewModel里实现viewModelScope.launch { val cache dao.getByCity(city) if (cache ! null System.currentTimeMillis() - cache.updateTime 30 * 60 * 1000) { _weatherLiveData.value parseCache(cache.dailyJson) } else { val fresh weatherService.fetchWeather(city, BuildConfig.API_KEY) _weatherLiveData.value fresh dao.insert(WeatherCache(city, Gson().toJson(fresh.daily), System.currentTimeMillis())) } }这里体现的是“先读缓存再请求”的双层策略界面得到数据的速度更快即使这次请求失败上一次数据也还在。30分钟是针对免费天气接口更新频率的经验值免费key通常每小时更新一次半小时缓存不会让演示显得数据陈旧。3.4 RecyclerView绑定七日预报列表界面层最常见的问题是Adapter里做了解析、判断、模板拼接一团乱麻。Adapter的职责只有一个把数据放进ViewHolder。稳妥写法class DailyAdapter : RecyclerView.AdapterDailyAdapter.VH() { private val items mutableListOfDaily() fun submit(list: ListDaily?) { items.clear() list?.let { items.addAll(it) } notifyDataSetChanged() } override fun onBindViewHolder(holder: VH, position: Int) { val item items[position] holder.tvDate.text item.date ?: -- holder.tvTemp.text ${item.tempMin ?: --}℃ ~ ${item.tempMax ?: --}℃ holder.tvText.text item.textDay ?: 未知 } }submit方法接收可空列表空值就当空列表处理避免调用方写if判空。onBindViewHolder里用?:给每个字段兜底date为空显示“--”天气描述为空显示“未知”。这样接口返回的数据即使缺几个字段UI依然能渲染出一行完整数据不会崩溃。ViewModel侧的顺序是先从缓存构造一次LiveData再去网络请求成功后再覆盖。Activity只做两件事观察LiveData变化、把列表传给adapter.submit()。4. 从系统源码zip到可安装APK导入、签名与打包4.1 解压后的清理与工程导入拿到zip解压后先做两件事删除构建缓存、检查工程根目录。要删的目录很固定.gradle、.idea、build、app/build、local.properties。这些目录记录的是别人电脑上的SDK路径和缓存留着只会让Android Studio在同步时把错误当成自己的配置。删除之后打开Android Studio选择Open定位到包含settings.gradle的目录开始加载。# Windows 下清理示例 cd weather del /s /q .gradle .idea build app\build local.properties如果删除后同步仍报“SDK location not found”就手动写一个local.properties指定本机SDK路径sdk.dirD:\\android-sdk注意写两个反斜杠反斜杠在properties文件里是转义字符单个写会导致路径解析失败。第二个常出问题的环节是JDK版本。AGP 8.x要求JDK 17而有些模板是从旧版本升级来的源码里把Java版本配置在8就会出现“Unsupported class file major version 61”。解决方式是在File → Project Structure里把Gradle JDK切到17同时把编译targetCompat也调成17。判断是JDK问题还是SDK问题的方法很简单看Sync日志里有没有“Unsupported class file”有就是JDK没有就是SDK。提示删除local.properties不会影响工程逻辑Android Studio会在下次打开时自动生成真正需要保留的是gradle/wrapper里的gradle-wrapper.jar和.properties文件它们是Gradle版本的唯一来源。4.2 release签名配置与签名文件管理源码zip里普遍没有签名配置跑应用时用的都是debug签名。debug签名文件位于用户目录的.android/debug.keystore只适合本机调试拷到别人机器后签不了同一个包。要生成一个可安装的release包最常见做法是生成jks签名文件并在build.gradle里引用android { signingConfigs { release { storeFile file(../keystore/release.jks) storePassword weather123456 keyAlias weather keyPassword weather123456 } } buildTypes { release { minifyEnabled false signingConfig signingConfigs.release } } }storeFile是jks文件路径用相对路径可以让工程整体迁移keyAlias和密码在生成jks时设置保持一致即可。这里有一组比较重要的取舍minifyEnabled在课程设计里保持false因为混淆会根据调用关系删除类和方法Gson依赖反射读取字段一旦混淆规则不齐全JSON无法映射成对象运行时会突然出现一堆null。如果确实需要缩减包体再额外添加consumer rules而不是在模板里直接开。4.3 buildTypes、productFlavors与版本号参数构建类型的常见差异是接口地址和签名不同。用buildConfigField在构建期注入变量比在代码里写if判断更规范buildTypes { debug { buildConfigField String, API_KEY, \test_key\ buildConfigField String, BASE_URL, \https://test.example.com/\ } release { buildConfigField String, API_KEY, \prod_key\ buildConfigField String, BASE_URL, \https://api.example.com/\ } }代码里直接引用BuildConfig.API_KEY和BuildConfig.BASE_URLAndroid Studio在启用BuildConfig功能后会自动生成这个类。换环境时只改gradle配置不用动Java代码。这一节里还有几个参数经常被搞混名称作用常见取值buildTypes同一下载形态的不同构建模式debug / releaseproductFlavors同一套代码的不同渠道版本free / paidversionCode整数版本号系统判定升级用1, 2, 3versionName展示给用户的版本号1.0.0versionCode每次发布必须递增否则检测不到新版本versionName只是给人看的重复不影响安装。productFlavors用于多渠道场景比如百度渠道、应用宝渠道每个渠道要不同的应用名或统计ID可以和buildType交叉组合。4.4 打包成干净的zip并验证可复现交付前把工程压成zip不少人是直接右键压缩整个文件夹然后体积一两百MB里面全是build目录和gradle缓存。干净的做法是分两步先执行clean再生成release验证能出包。gradlew clean gradlew assembleRelease第一条清理所有中间产物第二条生成release APK路径在app/build/outputs/apk/release/app-release.apk。确认能稳定出包后再把整个工程压成zip。压之前删掉.gradle、.idea、build、local.properties四个目录保留gradle/wrapper目录这样接收方解压后用gradlew命令能自动下载对应Gradle版本而非依赖本机全局配置。验证zip完不完整用命令行unzip -t weather.zip-t会逐个文件测试CRC校验最后输出“No errors detected in compressed data of weather.zip”才说明压缩包没有截断。zip这个格式没有内建加密里面的工程源码会被直接读走签名的jks文件如果也放进去等于把密钥公开所以交付压缩包前记得把keystore目录排除单独走其他途径发给负责人。5. 高版本Android适配与答辩演示的三个实战技巧5.1 明文HTTP请求的两种豁免方式演示环境的接口地址经常是http://开头Android 9开始默认禁止明文流量一跑release包就报“Cleartext HTTP traffic to xxx not permitted”。最省事的改法是给Manifest配usesCleartextTrafficapplication android:usesCleartextTraffictrue但如果想只放行测试域名就建network_security_config.xml在debug目录和release目录放不同配置正式包依然要求https。模板代码如果只盯着MainActivity调接口很容易忽略这个细节但它决定演示机能否联网。5.2 定位权限避免国产ROM返回空值定位自动获取当前城市在答辩环节容易出问题。模拟器、真机的定位权限弹窗、ROM的“仅在使用中允许”这些系统差异都可能导致Location为空。源码里如果直接用getLastKnownLocation返回值可能在部分机型上直接为null所以拿到的位置要做空判断返回null就默认展示北京或者上次选中的城市。5.3 断网兜底离线数据和启动验证演示当天最怕网络抖动接口突然超时。一个简单的兜底是在assets目录放一份fallback_weather.json字段结构和Daily数据类保持一致几行就够了{ daily: [ { date: 2024-06-01, tempMax: 28, tempMin: 20, textDay: 晴 } ] }ViewModel的网络异常分支里只解析这份文件不触发重试界面照常更新LiveData用户看到的就是一份完整天气数据不会察觉异常。答辩前用ADB预启动应用比手动点图标更稳定adb shell am start -n com.example.weather/.MainActivity配合打包时的命令可以做到“构建、安装、启动”一步到位gradlew assembleRelease adb install -r app/build/outputs/apk/release/app-release.apk adb shell am start -n com.example.weather/.MainActivity-r表示覆盖安装并保留本地数据确保前一步成功才执行后一步。把zip里写死的测试key、过期接口地址在交付前替换掉再执行一次这条命令确认整条链路这套源码才算真正能落地。本文还有配套的精品资源点击获取