news 2026/10/14 23:10:50

Android天气预报App开发实战:从架构到发布全流程详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android天气预报App开发实战:从架构到发布全流程详解

做Android天气预报App,听起来是个入门级练手项目,但真要把它做到"能稳定运行、界面不糊、耗电可控、数据不崩",牵扯到的知识点远比想象中多。网络请求、JSON解析、定位权限、数据库缓存、后台任务调度、多彩布局适配,几乎把Android日常开发的核心技术栈全串起来了。这篇文章我想把做这个APP的完整思路和实操细节写出来,从选型到最后上架前要检查的东西,一次说透。适合刚学完基础、准备做第一个完整项目的开发者,也适合想用一个小项目验证自己工程化能力的同学参考。

1. 整体架构设计与技术选型

很多初学者拿到"天气预报"这个需求,第一反应就是"找个天气API,返回什么就显示什么",但这样写出来的代码通常只能跑两天,第三天换个需求就崩了。我建议先花半天时间把架构想清楚,后面所有功能都是往这个骨架里填肉。

1.1 功能需求先拆解

天气预报APP的核心功能,按优先级排列:

  • 实时天气展示:温度、天气现象、体感温度、风力风向、湿度、气压、能见度
  • 多日天气预报:未来7天的最高温、最低温、白昼天气、夜间天气
  • 城市管理与切换:支持搜索城市、定位当前城市、保存常用城市
  • 下拉刷新与定时刷新:手动刷新、后台自动更新数据
  • 离线兜底:没网的时候展示上次缓存的数据

每一项拆开都不难,但合在一起就考验架构能力。我见过最失败的写法是把所有逻辑塞进MainActivity里,Activity堆了两千行,后来加一个城市管理功能,改一个字段要牵连三个地方。所以第一步要是把数据层、界面层、业务层分开。

1.2 技术选型的核心思路

技术栈的选择,我不建议全都用最新的,但也不建议全用最原始的原生API。选型的逻辑很简单:团队维护成本低、社区案例多、出问题能快速搜到答案。

模块技术方案选型理由
网络请求Retrofit + OkHttp生态成熟,拦截器、超时配置、协程支持都齐
JSON解析Gson / kotlinx.serialization网络和模型对接简便,debug直观
异步处理Kotlin协程避免回调地狱,代码可读性高
本地存储Room数据库类型安全,SQLite的官方封装,支持Flow观察数据变化
界面XML + Material Components兼容性强,自定义控件方便,对低版本机型友好
图片图标本地矢量图 + 自绘天气图标不依赖服务端图片URL,省流量、加载快

有人会问,现在Google都推Jetpack Compose了,为什么不用Compose?我个人的看法是:如果你做的是全新项目且团队有足够时间踩坑,Compose完全可用;但天气预报这种需要大量列表、自适应布局、深浅色切换的场景,XML方案依然是当前多数存量项目的主流。而且用XML写,后续接外包、接手老项目也更容易,学习成本对新手更友好。

1.3 整体数据流设计

整个APP的数据流我习惯画成一条单向链路:

界面层(Activity/Fragment) → ViewModel → Repository → 数据源(网络API / 本地数据库)

界面层只做两件事:展示状态、转发用户操作。ViewModel负责持有UI状态、调用Repository。Repository对上层暴露一个统一的入口,内部自己判断"是走网络还是走缓存",上层完全不关心数据从哪来。

这样设计的最大好处是,哪天你想把天气API服务商换掉,只需要改Repository里对应的方法实现,界面层一行都不用动。我后来接手过一个历史项目,就是没做这层隔离,API字段一变,全部页面集体改签名,那个酸爽不想体验第二次。

2. 数据接入与解析:最容易被坑的一环

天气APP的核心是数据,但数据恰恰是最容易出问题的环节。第三方天气API返回的JSON结构五花八门,字段单位不统一,有的给华氏度,有的给摄氏度,有的风速给km/h,有的给m/s。这里必须把所有细节吃透。

2.1 天气数据源的接入方式

我用的是市面上可以直接访问的第三方天气数据服务商,申请一个API Key就能调用。接入时主要用三个接口:实时天气、未来7天预报、城市搜索/地理位置反查。

以实时天气接口为例,请求参数通常是:

@GET("v3/weather/now.json") suspend fun fetchWeatherNow( @Query("location") location: String, // 经纬度或城市ID @Query("key") apiKey: String ): WeatherNowResponse

location参数支持经纬度也支持城市ID。定位成功后把经纬度传进去,返回的就是对应位置的实时天气。这里有个细节:直接传经纬度的精度会比传城市ID高,尤其在城市边缘地带,同一个城市中心到郊区温度可能差好几度。

2.2 数据模型的层级设计

天气API返回的JSON是嵌套结构,数据模型必须严格对应。拿实时天气举例,核心结构大概是:

data class WeatherNowResponse( val code: String, // 状态码 val updateTime: String, // 数据更新时间 val now: NowWeather // 实时天气核心数据 ) data class NowWeather( val temp: String, // 温度,单位:℃ val feelsLike: String, // 体感温度 val icon: String, // 天气图标编码 val text: String, // 天气现象描述:晴、多云、小雨等 val windDir: String, // 风向 val windScale: String, // 风力等级 val humidity: String, // 相对湿度,百分比数值 val pressure: String, // 气压,单位:hPa val vis: String, // 能见度,单位:km val cloud: String // 云量,百分比数值 )

每个字段我都建议用String而不是Int或Float来接收,因为某些天气服务商在字段缺失时返回的是空字符串,用数值类型接收直接抛JSON解析异常,用String接收至少不会崩溃,后续再做安全转换。

2.3 解析细节与单位换算

解析JSON看似简单,但坑藏在单位换算和编码映射里。这里我踩过几次,值得单独写一段。

温度和体感温度:默认是摄氏度,但部分接口会提供华氏度参数。如果你面向的是全球用户,务必在设置里做单位切换。换算公式很简单:℃ = (℉ - 32) × 5 / 9。我建议在数据层统一转成摄氏度存储,界面展示时再按设置项转换。

风力等级换算:有些服务商给的是风速数值,比如"12.0",单位是km/h或m/s,需要自行换算成风力等级。实际用到的换算逻辑:

fun windSpeedToLevel(speedKmh: Double): Int { return when { speedKmh < 1 -> 0 speedKmh < 6 -> 1 speedKmh < 12 -> 2 speedKmh < 20 -> 3 speedKmh < 29 -> 4 speedKmh < 39 -> 5 speedKmh < 50 -> 6 speedKmh < 62 -> 7 speedKmh < 75 -> 8 speedKmh < 89 -> 9 speedKmh < 103 -> 10 else -> 11 } }

天气图标编码:天气服务商通常会给一串数字或字符串编码,比如"100"代表晴,"101"代表多云,"300"代表小雨。这些编码不同服务商各不相同。我强烈建议拿到编码后在本地映射到自绘矢量图,而不是直接用服务商返回的图片URL。原因有两个:一是图片URL有时效性,二是有可能被刷流量,而且本地矢量图的加载速度远快于网络图片。

2.4 异常重试与缓存兜底

网络请求一定要做超时和重试。我用OkHttp拦截器统一设置:

val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build()

但要注意,retryOnConnectionFailure只处理连接失败,不处理HTTP 500之类的异常。所以Repository层还要再包一层重试逻辑:

suspend fun <T> retryIO(block: suspend () -> T, maxRetry: Int = 3): T { var retryCount = 0 while (true) { try { return block() } catch (e: Exception) { retryCount++ if (retryCount > maxRetry) throw e delay(1000L * retryCount) // 指数退避 } } }

提示:重试不是越多越好。天气数据实时性高,超过3次重试意义不大。另外,用户手动下拉刷新时也不要无限重试,要友好提示"刷新失败,当前展示的是X分钟前的数据"。

3. 界面开发:从"能用"到"好看"

天气预报APP的界面设计有一个天然优势:数据维度多,布局可以做得层次分明。但层次分明不等于信息堆砌,如何把实时温度、体感、风、湿度、未来几天预报合理地组织在屏幕上,需要认真设计。

3.1 主界面的信息层级划分

我推荐的主界面布局分成三个信息层级:

第一层级:当前温度和天气现象,作为视觉焦点,字号最大,通常用64sp以上的超大字重。这一层要让人一眼看到"今天多少度"。

第二层级:体感温度、天气描述、最高最低温。字号中等,作为第一层的补充说明。

第三层级:风力、湿度、气压、能见度,用两列或四列的宫格展示,小字号+图标。

第四层级:未来7天预报,用垂直列表展示,每天一行,包含日期、白天/夜间天气图标、最低最高温区间。这个列表我建议用RecyclerView嵌套在主界面的NestedScrollView里,再配合头部大卡片一起滚动。

3.2 卡片化设计与圆角层次

Material Design的卡片设计特别适合天气类应用。我用的参数组合:

  • 外层背景:浅灰色或主题背景色
  • 卡片背景:纯白色或半透明白,圆角16dp,海拔2dp
  • 卡片间距:12~16dp

深浅色模式切换时,卡片颜色要跟着变。系统深色模式下,卡片用深灰色而不是纯黑,能保留层次感。

用MaterialCardView实现卡片:

<com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="16dp" app:cardElevation="2dp"> <!-- 卡片内容 --> </com.google.android.material.card.MaterialCardView>

3.3 多类型列表适配器

未来7天预报这一块,如果是固定模式,可以直接用LinearLayout一行行加;但如果要混排"24小时预报"和"7天预报",建议用RecyclerView的多type实现。

class WeatherListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { const val TYPE_HEADER = 0 const val TYPE_HOURLY = 1 const val TYPE_DAILY = 2 const val TYPE_INDEX = 3 // 生活指数,比如穿衣、紫外线 } override fun getItemViewType(position: Int): Int { return dataList[position].type } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { TYPE_HEADER -> HeaderViewHolder(...) TYPE_HOURLY -> HourlyViewHolder(...) TYPE_DAILY -> DailyViewHolder(...) else -> IndexViewHolder(...) } } }

多type的关键点在getItemViewType要稳定返回,不能依赖数据内容动态变化,否则RecyclerView复用时会错乱。

3.4 下拉刷新与状态管理

下拉刷新我用SwipeRefreshLayout包着NestedScrollView。刷新时要做三件事:

  1. 请求最新数据
  2. 数据成功返回后更新UI
  3. 关闭刷新动画

在ViewModel里维护一个isRefreshing状态:

sealed class WeatherUiState { object Loading : WeatherUiState() data class Success(val data: WeatherData) : WeatherUiState() data class Error(val message: String) : WeatherUiState() }

界面层观察这个状态,根据状态决定显示加载动画、数据内容还是错误提示。这比直接用try-catch包裹网络请求再塞给Adapter要清晰得多,也方便做单元测试。

3.5 天气图标的矢量实现

天气图标不要用PNG图片集合,占包体积不说,还容易糊。我推荐用Android矢量图(VectorDrawable),每个图标一个xml文件。比如晴天是一个圆+几条放射线,用pathData绘制。网上有很多免费的天气矢量图标集,找一套风格统一的,转换成xml格式就行。

矢量图的好处:任意缩放不模糊,颜色可以跟随主题动态变化,包体积几乎是零成本。

4. 定位、数据库缓存与后台数据刷新

天气预报APP的"灵魂"是自动定位。用户打开APP就想看到当前位置的天气,如果每次都要手动搜索城市,体验就废了。但定位链路牵扯到权限、LocationManager、地理编码、后台任务调度,也是崩溃高发区。

4.1 定位服务的正确姿势

定位我分两条链路处理:GPS定位和网络定位。优先用GPS,GPS没有信号时降级到网络定位。

val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager val location = if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) } else { locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) }

但getLastKnownLocation有个问题:返回的可能是很久之前的缓存位置。如果用户从A城市飞到B城市,拿到的还是A城市的坐标。所以更可靠的方式是主动请求定位更新:

locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 1000L, // 最小时间间隔,单位毫秒 0f, // 最小距离间隔,单位米 locationListener )

拿到经纬度后,要用Geocoder反向地理编码获取城市名:

val geocoder = Geocoder(context, Locale.CHINA) val addresses = geocoder.getFromLocation(latitude, longitude, 1) val cityName = addresses.firstOrNull()?.locality

注意:反向地理编码是IO操作,必须放到子线程或协程的Dispatchers.IO里执行,放到主线程会出现"Application Not Responding"闪退。

权限处理:Android 10(API 29)需要ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION两个权限。Android 12开始还要求声明ACCESS_BACKGROUND_LOCATION才能后台定位,但天气预报这类应用完全没必要申请后台定位权限,只要前台定位就可以。权限申请要动态请求,用户拒绝后要给出引导弹窗,再拒绝就回退到默认城市。

4.2 数据库缓存设计

离线缓存我用Room实现。两张表:

  • 城市表:存用户添加的常用城市、当前选中的城市
  • 天气缓存表:存每个城市的天气JSON数据,加上缓存时间戳
@Entity(tableName = "city") data class CityEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val name: String, val latitude: Double, val longitude: Double, val isSelected: Boolean = false ) @Entity(tableName = "weather_cache") data class WeatherCacheEntity( @PrimaryKey val cityKey: String, val weatherDataJson: String, val cachedAt: Long )

天气缓存表为什么不拆成多个字段而是直接存JSON?因为天气数据结构复杂、字段多,拆字段存进数据库反而难维护。存整个JSON字符串,反序列化还是一条逻辑,简单可靠。缓存时间戳用于判断数据是否过期,一般天气数据的合理缓存时间是30分钟,超过就触发网络刷新。

4.3 定时刷新:别用AlarmManager,用WorkManager

很多同学第一反应是AlarmManager+Service做定时刷新,但这是个坑。现在的Android系统对后台限制越来越严格,AlarmManager的精确闹钟会被Doze模式延迟,而且频繁唤醒CPU非常耗电。

我推荐用WorkManager,它是Google官方推荐的异步任务调度框架,系统会智能地将任务合并批量执行,省电且稳定。

class WeatherWorker(context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result { return try { val repository = WeatherRepository.getInstance(context) val refreshResult = runBlocking { repository.refreshSelectedCityWeather() } if (refreshResult) { Result.success() } else { Result.retry() } } catch (e: Exception) { Result.retry() } } }

然后创建定时任务:

val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = PeriodicWorkRequestBuilder<WeatherWorker>(30, TimeUnit.MINUTES) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "weather_refresh", ExistingPeriodicWorkPolicy.KEEP, request )

WorkManager最快执行周期是15分钟,如果APP想每30分钟刷新一次,这刚好满足。数据刷新成功后,如果要通知用户,前台发一条通知即可。注意Android 13开始要动态申请POST_NOTIFICATIONS权限,不申请的通知会直接不显示。

4.4 城市搜索功能

城市搜索我建议本地维护一份城市列表,用SQLite或直接Assets里放JSON,搜索时做模糊匹配。这样比每次搜索都请求服务端接口更快,还没网也能用。

搜索逻辑:

fun searchCities(keyword: String): List<CityEntity> { return cityDao.search("%$keyword%") }

Room里的查询:

@Query("SELECT * FROM city WHERE name LIKE :keyword") suspend fun search(keyword: String): List<CityEntity>

这个方案简单粗暴,但用户体验很直接,输入一个字就有候选列表。

5. 真机调试与发布前检查

说实话,前面写了那么多设计,真要让APP在几十种Android机型上都能正常跑,调试和发布前的排查才是真正耗时间的环节。这里把我实际踩过的坑和总结的检查清单分享出来。

5.1 常见的崩溃与显示异常问题

专业易懂但不算难点的问题

JSON字段缺失导致崩溃:天气服务商偶尔会返回异常的JSON结构,比如某个字段直接没有了。用Gson解析时如果目标字段是非空String,解析会抛异常。我的对策是:数据模型中可空字段用nullable类型,然后在界面展示处做空值兜底。

val tempText = weather.temp ?: "--"

华氏度/摄氏度混乱:某次测试发现某个城市体感温度异常,排查后发现是服务商返回的字段单位变了。后来我在Repository层统一做一次单位转换,所有数据进入界面层之前都规整成统一单位,这个问题就再没出现过。

图标看不清:某个天气现象编码我在矢量图里没做映射,导致界面显示空白。解决方式是建立一张完整的编码映射表,并且给一个默认兜底图标,映射不到的统统显示为"多云"图标。

夜间模式适配崩坏:很多新手在XML里写死了背景色和文字颜色,深浅色模式切换时文字和背景融为一体。我的建议是全部用主题属性,比如?attr/colorSurface、?attr/colorOnSurface,不要写死#FFFFFF。

5.2 API Key的安全问题

这是发布前必须处理的问题。如果直接把API Key写在代码里,对方一抓包就能把Key抽走,一天就能把你的免费调用额度耗光。规避方案我推荐两种:

一是把Key放进local.properties或环境变量,通过BuildConfig注入。但这样只是不给"直接看代码的人"偷走,严格来说Key还是打包在APK里了。

二是搭一个轻量后端代理,APP请求自己的服务器,服务器再转发天气API请求,Key只保存在服务端。这样最安全,但需要额外维护服务器。对于个人练手项目,用第一种方案就够,但记得在API服务商后台设置配额限制,万一Key泄露损失也可控。

5.3 混淆与包体积优化

发布release包前要配置R8混淆。混淆时要注意数据模型类不能被混淆,否则Gson反射找不到字段。

在proguard-rules.pro里添加:

-keep class com.example.weather.data.model.** { *; }

这个规则的意思是:data.model包下的类及其所有成员都要保留。这是我调试Gson解析问题后总结出来的经验,忘了加这一行,release包里的列表数据会全部变成null。

包体积优化方面,我的做法:

  • 替换掉所有大图资源,能矢量就矢量
  • 去掉未引用的依赖,用./gradlew dependencies分析依赖树
  • 开启资源收缩android.enableResourceShrinking=true
  • 使用App Bundle替代APK上传应用市场,同一套代码能根据机型下发不同尺寸包

5.4 上线前检查清单

分享一份我每次发布前都会过的检查单:

检查项具体内容
权限是否只申请了必要权限,位置权限有没有合理拒绝引导
网络弱网、无网环境下是否走缓存,是否给出友好提示
数据温度单位、风力等级是否正确,异常数据是否兜底
界面深浅色模式是否正常,小屏和大屏设备布局是否错乱
性能启动时间是否过长,列表滚动是否卡顿
安全API Key是否移除硬编码,日志是否打印完整JSON
后台定时刷新是否省电,通知是否受权限限制
兼容Android 8到Android 14全版本实测,刘海屏安全区域是否避开

我见过太多APP没做兼容性测试就发布,结果在几款旧机型上直接闪退,用户差评一波带走。建议最少找5台不同品牌、不同Android版本的手机做一次真机测试,比在模拟器上跑一百遍都管用。

写在最后的一点体会

做天气APP这个项目,最大的收获其实不是"我会调API了",而是建立了一套完整的"发现问题—定位问题—解决问题"的方法。我做过一次最尴尬的事故:上线第二天,某个城市用户反馈温度显示异常,排查到半夜发现是该地区出现了一次极端天气,服务商返回的windScale字段为空,我的代码没做兜底。这种事故经历一次就长记性了,后面所有字段我都默认它可能不存在。

还有一个建议想分享给正在做这个项目的同学:开发阶段一定要做一个模拟数据开关。我在Repository里加了一个配置项,打开后不请求网络,而是从本地Asset读取固定JSON返回。这个开关在界面调试时特别好用——你可以快速切换不同天气数据来测试UI,比如模拟雨天UI、极端高温UI、数据字段缺失UI,不用频繁请求API、不用担心被限流、还能测试各种边界样式。

天气预报APP至少可以再扩展的方向很多:桌面小插件、语音播报天气、多城市天气对比、天气预警推送、穿衣出行指数。但我始终认为,把核心流程做扎实比追求功能堆砌重要得多。数据链路稳定了、界面交互顺了、异常情况兜住了,再往外扩功能,怎么加都不乱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 23:09:55

TikTok变现全攻略:从账号定位到直播带货的实战拆解

这几年做海外市场&#xff0c;被问得最频繁的一个问题就是&#xff1a;“TikTok到底怎么赚钱&#xff1f;”每次看到这种问题&#xff0c;我都挺感慨的&#xff0c;因为大部分人的注意力都停在“看视频乐呵”的层面&#xff0c;根本没意识到自己刷的每一条视频背后&#xff0c;…

作者头像 李华
网站建设 2026/10/14 23:09:06

全连接神经网络公式推导:正向传播与反向传播手把手详解

全连接神经网络的公式推导&#xff0c;说难不难&#xff0c;但真能从头到尾手推一遍的人&#xff0c;十个里面可能不到五个。我见过不少同学用框架用得飞起&#xff0c;一行model.fit()就把网络训起来了&#xff0c;可一旦要换损失函数、改网络结构、排查梯度异常&#xff0c;就…

作者头像 李华
网站建设 2026/10/14 23:08:32

从零开发QQ群聊天机器人LuckyTri:WebSocket架构与部署实战

这个机器人叫 LuckyTri&#xff0c;是我自己花了两三周时间做完并在几个群里跑了半年多的一个 QQ 群聊天机器人。可能有人问&#xff0c;现在群里已经有很多现成 Bot 了&#xff0c;为什么还要自己写一个&#xff1f;这个问题我一开始也问过自己。后来发现&#xff0c;现成的 B…

作者头像 李华
网站建设 2026/10/14 23:08:14

零代码智能客服:业务状态机驱动的降本增效实践

1. 这不是“又一个客服机器人”&#xff0c;而是一套能立刻落地的成本手术刀“客服成本降80%”——这个数字不是PPT里的乐观预测&#xff0c;而是我去年帮一家做母婴辅食的客户做完系统切换后&#xff0c;财务部发来的真实报表截图上的加粗红字。他们原来用的是外包呼叫中心人工…

作者头像 李华
网站建设 2026/10/14 23:07:40

易语言恒云雨驱动控制模块:内核态读写与IO控制码实战

简介&#xff1a;易语言恒云雨驱动控制模块是一份面向易语言开发者的系统级驱动交互源码资源&#xff0c;适合希望深入硬件驱动管理与控制的中高级学习者。模块围绕驱动全生命周期提供完整函数集&#xff0c;涵盖获取通讯、加载与启动驱动、停止与删除驱动、创建与销毁连接、连…

作者头像 李华
网站建设 2026/10/14 23:07:14

AgentScope实战:构建标准多智能体协作流程的核心设计

做多智能体开发&#xff0c;尤其是想在一块框架里同时解决原生适配大模型、标准化的智能体交互&#xff0c;AgentScope是值得认真看的一个开源工具。我最近用它在三个模拟业务场景里做了两轮多Agent协作的实验&#xff0c;整体感觉是&#xff1a;它确实把“一个Agent如何和另一…

作者头像 李华