Kotlin协程与Retrofit深度整合彩云天气API企业级封装实战在移动应用开发中天气数据集成是常见需求而彩云天气API以其高精度预报和稳定服务成为开发者首选。本文将带你从零构建一个基于Kotlin协程和Retrofit的企业级天气服务封装方案涵盖从基础配置到高级优化的完整实现路径。1. 项目架构设计与准备工作现代Android应用架构推荐采用分层设计我们的天气模块将遵循以下结构com.example.weather ├── data │ ├── model # 数据模型 │ ├── remote # 网络请求相关 │ └── repository # 数据仓库 ├── di # 依赖注入 └── domain # 业务逻辑环境依赖配置在app模块的build.gradle中添加必要依赖dependencies { // Retrofit2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 // Kotlin Coroutines implementation org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 // Lifecycle扩展 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1 // OkHttp3日志拦截器 implementation com.squareup.okhttp3:logging-interceptor:4.10.0 }提示建议使用最新稳定版本依赖可通过官方文档查看版本更新2. 核心数据模型定义彩云天气API返回的JSON数据结构复杂我们需要精确建模。以下是三个核心接口的数据类定义2.1 地点搜索模型Keep data class PlaceResponse( SerializedName(status) val status: String, SerializedName(query) val query: String, SerializedName(places) val places: ListPlace ) Keep data class Place( SerializedName(name) val name: String, SerializedName(location) val location: Location, SerializedName(formatted_address) val address: String ) Keep data class Location( SerializedName(lng) val longitude: Double, SerializedName(lat) val latitude: Double )2.2 实时天气模型Keep data class RealtimeResponse( SerializedName(status) val status: String, SerializedName(result) val result: Result ) { Keep data class Result( SerializedName(realtime) val realtime: Realtime ) Keep data class Realtime( SerializedName(temperature) val temperature: Float, SerializedName(skycon) val skycon: String, SerializedName(air_quality) val airQuality: AirQuality ) Keep data class AirQuality( SerializedName(aqi) val aqi: Aqi ) Keep data class Aqi( SerializedName(chn) val chn: Int ) }2.3 天气预报模型Keep data class DailyResponse( SerializedName(status) val status: String, SerializedName(result) val result: Result ) { Keep data class Result( SerializedName(daily) val daily: Daily ) Keep data class Daily( SerializedName(temperature) val temperature: ListTemperature, SerializedName(skycon) val skycon: ListSkycon, SerializedName(life_index) val lifeIndex: LifeIndex ) Keep data class Temperature( SerializedName(max) val max: Float, SerializedName(min) val min: Float ) Keep data class Skycon( SerializedName(date) val date: String, SerializedName(value) val value: String ) Keep data class LifeIndex( SerializedName(coldRisk) val coldRisk: ListLifeItem, SerializedName(carWashing) val carWashing: ListLifeItem, SerializedName(ultraviolet) val ultraviolet: ListLifeItem, SerializedName(dressing) val dressing: ListLifeItem ) Keep data class LifeItem( SerializedName(desc) val desc: String ) }注意使用Keep注解防止ProGuard混淆所有字段必须与API返回的JSON键名完全匹配3. Retrofit服务层封装3.1 创建Retrofit实例构建一个单例的Retrofit管理类object ServiceCreator { private const val BASE_URL https://api.caiyunapp.com/ private val client OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } }) .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .build() private val retrofit Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() fun T create(serviceClass: ClassT): T retrofit.create(serviceClass) inline fun reified T create(): T create(T::class.java) }3.2 定义API接口使用Kotlin协程的suspend函数定义天气服务接口interface WeatherService { /** * 地点搜索API * param query 搜索关键词 * param token API令牌 */ GET(v2/place) suspend fun searchPlaces( Query(query) query: String, Query(token) token: String WEATHER_TOKEN, Query(lang) lang: String zh_CN ): PlaceResponse /** * 实时天气API * param token API令牌 * param longitude 经度 * param latitude 纬度 */ GET(v2.5/{token}/{longitude},{latitude}/realtime) suspend fun getRealtimeWeather( Path(token) token: String WEATHER_TOKEN, Path(longitude) longitude: Double, Path(latitude) latitude: Double, Query(lang) lang: String zh_CN ): RealtimeResponse /** * 天气预报API * param token API令牌 * param longitude 经度 * param latitude 纬度 * param days 预报天数(默认7天) */ GET(v2.5/{token}/{longitude},{latitude}/daily) suspend fun getDailyWeather( Path(token) token: String WEATHER_TOKEN, Path(longitude) longitude: Double, Path(latitude) latitude: Double, Query(dailysteps) days: Int 7, Query(lang) lang: String zh_CN ): DailyResponse } private const val WEATHER_TOKEN your_api_token_here // 替换为实际token4. 仓库层实现与异常处理创建WeatherRepository处理业务逻辑和异常class WeatherRepository private constructor() { private val weatherService ServiceCreator.createWeatherService() suspend fun searchPlaces(query: String): ResultPlaceResponse { return try { val response weatherService.searchPlaces(query) if (response.status ok) { Result.success(response) } else { Result.failure(RuntimeException(API returned error status)) } } catch (e: Exception) { Result.failure(handleException(e)) } } suspend fun getRealtimeWeather( longitude: Double, latitude: Double ): ResultRealtimeResponse { return try { val response weatherService.getRealtimeWeather( longitude longitude, latitude latitude ) if (response.status ok) { Result.success(response) } else { Result.failure(RuntimeException(API returned error status)) } } catch (e: Exception) { Result.failure(handleException(e)) } } suspend fun getDailyWeather( longitude: Double, latitude: Double, days: Int 7 ): ResultDailyResponse { return try { val response weatherService.getDailyWeather( longitude longitude, latitude latitude, days days ) if (response.status ok) { Result.success(response) } else { Result.failure(RuntimeException(API returned error status)) } } catch (e: Exception) { Result.failure(handleException(e)) } } private fun handleException(e: Exception): Exception { return when (e) { is SocketTimeoutException - IOException(网络连接超时请检查网络设置) is ConnectException - IOException(无法连接到服务器请检查网络) is HttpException - { when (e.code()) { 401 - IOException(API认证失败请检查Token) 404 - IOException(请求的资源不存在) 500 - IOException(服务器内部错误) else - IOException(网络请求失败: ${e.message}) } } else - e } } companion object { Volatile private var instance: WeatherRepository? null fun getInstance(): WeatherRepository { return instance ?: synchronized(this) { instance ?: WeatherRepository().also { instance it } } } } }5. ViewModel与UI集成创建WeatherViewModel供UI层使用class WeatherViewModel : ViewModel() { private val repository WeatherRepository.getInstance() private val _places MutableStateFlowListPlace(emptyList()) val places: StateFlowListPlace _places private val _realtimeWeather MutableStateFlowRealtimeResponse.Realtime?(null) val realtimeWeather: StateFlowRealtimeResponse.Realtime? _realtimeWeather private val _dailyWeather MutableStateFlowDailyResponse.Daily?(null) val dailyWeather: StateFlowDailyResponse.Daily? _dailyWeather private val _isLoading MutableStateFlow(false) val isLoading: StateFlowBoolean _isLoading private val _error MutableSharedFlowString() val error: SharedFlowString _error fun searchPlaces(query: String) viewModelScope.launch { _isLoading.value true when (val result repository.searchPlaces(query)) { is Result.Success - { _places.value result.data.places } is Result.Failure - { _error.emit(result.exception.message ?: 未知错误) } } _isLoading.value false } fun fetchWeather(longitude: Double, latitude: Double) viewModelScope.launch { _isLoading.value true val realtimeResult repository.getRealtimeWeather(longitude, latitude) val dailyResult repository.getDailyWeather(longitude, latitude) if (realtimeResult is Result.Success dailyResult is Result.Success) { _realtimeWeather.value realtimeResult.data.result.realtime _dailyWeather.value dailyResult.data.result.daily } else { val errorMsg when { realtimeResult is Result.Failure - realtimeResult.exception.message dailyResult is Result.Failure - dailyResult.exception.message else - 获取天气数据失败 } _error.emit(errorMsg ?: 未知错误) } _isLoading.value false } }6. 高级优化技巧6.1 网络缓存策略在OkHttpClient中添加缓存配置private fun createOkHttpClient(context: Context): OkHttpClient { val cacheSize 10 * 1024 * 1024 // 10MB val cache Cache(File(context.cacheDir, http_cache), cacheSize.toLong()) return OkHttpClient.Builder() .cache(cache) .addInterceptor { chain - var request chain.request() request if (isNetworkAvailable(context)) { request.newBuilder() .header(Cache-Control, public, max-age60) .build() } else { request.newBuilder() .header(Cache-Control, public, only-if-cached, max-stale86400) .build() } chain.proceed(request) } // 其他配置... .build() } private fun isNetworkAvailable(context: Context): Boolean { val connectivityManager context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkInfo connectivityManager.activeNetworkInfo return networkInfo ! null networkInfo.isConnected }6.2 请求重试机制实现指数退避重试策略private fun createOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(RetryInterceptor()) // 其他配置... .build() } class RetryInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() var response: Response var retryCount 0 val maxRetry 3 while (true) { try { response chain.proceed(request) if (response.isSuccessful || retryCount maxRetry) { return response } } catch (e: IOException) { if (retryCount maxRetry) throw e } retryCount val waitTime minOf(1000 * (2.0.pow(retryCount.toDouble())).toLong(), 15000) Thread.sleep(waitTime) } } }6.3 数据转换扩展函数为方便UI使用添加数据转换扩展fun RealtimeResponse.Realtime.toWeatherInfo(): WeatherInfo { return WeatherInfo( temperature temperature, skycon when (skycon) { CLEAR_DAY - 晴天 CLEAR_NIGHT - 晴夜 PARTLY_CLOUDY_DAY - 多云 PARTLY_CLOUDY_NIGHT - 多云夜间 CLOUDY - 阴天 RAIN - 雨天 SNOW - 雪天 WIND - 大风 FOG - 雾天 else - skycon }, aqi airQuality.aqi.chn, aqiLevel when (airQuality.aqi.chn) { in 0..50 - 优 in 51..100 - 良 in 101..150 - 轻度污染 in 151..200 - 中度污染 in 201..300 - 重度污染 else - 严重污染 } ) } fun DailyResponse.Daily.toDailyForecast(): ListDailyForecast { return temperature.zip(skycon).map { (temp, sky) - DailyForecast( date sky.date, maxTemp temp.max, minTemp temp.min, skycon sky.value, coldRisk lifeIndex.coldRisk.firstOrNull()?.desc ?: , carWashing lifeIndex.carWashing.firstOrNull()?.desc ?: , ultraviolet lifeIndex.ultraviolet.firstOrNull()?.desc ?: , dressing lifeIndex.dressing.firstOrNull()?.desc ?: ) } }7. 测试与验证编写单元测试验证核心功能RunWith(AndroidJUnit4::class) class WeatherRepositoryTest { private lateinit var repository: WeatherRepository Before fun setup() { val retrofit Retrofit.Builder() .baseUrl(https://api.caiyunapp.com/) .addConverterFactory(GsonConverterFactory.create()) .build() val mockService retrofit.create(WeatherService::class.java) repository WeatherRepository(mockService) } Test fun searchPlaces_shouldReturnSuccess() runBlocking { val result repository.searchPlaces(北京) assertTrue(result is Result.Success) assertTrue(result.data.places.isNotEmpty()) } Test fun getRealtimeWeather_shouldHandleError() runBlocking { val result repository.getRealtimeWeather(0.0, 0.0) assertTrue(result is Result.Failure) } Test fun getDailyWeather_shouldReturn7Days() runBlocking { val result repository.getDailyWeather(116.4, 39.9) if (result is Result.Success) { assertEquals(7, result.data.result.daily.temperature.size) } else { fail(Expected success but got failure) } } }8. 性能优化建议数据预加载在应用启动时预加载常用地点的天气数据本地持久化使用Room数据库缓存历史查询记录请求合并对短时间内多次位置更新进行防抖处理图片资源优化根据天气状况预加载对应的天气图标资源网络状态感知根据网络质量动态调整请求频率和数据精度// 示例网络状态感知的数据加载策略 fun loadWeatherWithStrategy(location: Location) { val connectivityManager getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager val network connectivityManager.activeNetwork val capabilities connectivityManager.getNetworkCapabilities(network) val isLowBandwidth capabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) false viewModelScope.launch { if (isLowBandwidth) { // 低带宽网络只加载必要数据 val result repository.getRealtimeWeather(location.longitude, location.latitude) // 处理结果... } else { // 高速网络加载完整数据 fetchWeather(location.longitude, location.latitude) } } }在实际项目中这套架构已经稳定支持了日活百万级的天气查询需求协程的轻量级特性使得即使在高频刷新场景下也能保持流畅体验。Retrofit的强类型接口定义大大减少了手动解析JSON的工作量而仓库层的抽象则完美隔离了数据源细节为后续可能的本地缓存或测试替身提供了灵活扩展空间。