@[TOC](Android实战项目② 天气预报App)# Android实战项目②: Retrofit + Hilt开发天气预报App --- 完整源码详解> 系列第2篇。学习Retrofit网络请求、Kotlin Serialization JSON解析、Hilt依赖注入、sealed class状态管理和Coil图片加载。---## 一、项目概览| 项目信息 | 说明 ||---------|------|| 难度 | ★★☆☆☆ || 技术栈 | Retrofit + OkHttp + Kotlin Serialization + Hilt + Coil + Compose || 功能 | 城市搜索、当前天气、预报列表、天气图标 || 文件数 | 12个Kotlin文件 || API | OpenWeatherMap (需自行注册免费Key) |---## 二、项目结构02-weather/app/src/main/java/com/example/weather/├── data/│ ├── remote/│ │ ├── WeatherApi.kt # Retrofit接口│ │ └── dto/│ │ └── WeatherResponse.kt # 数据传输对象│ └── repository/│ └── WeatherRepository.kt # 数据仓库├── di/│ └── NetworkModule.kt # Hilt网络模块├── domain/model/│ └── Weather.kt # 领域模型├── viewmodel/│ └── WeatherViewModel.kt # 状态管理├── ui/│ ├── screen/HomeScreen.kt # 主页面│ ├── component/│ │ ├── WeatherCard.kt # 天气卡片│ │ └── ForecastItem.kt # 预报列表项│ └── theme/Theme.kt├── MainApplication.kt└── MainActivity.kt---## 三、完整源码 + 详解### 3.1 Retrofit API接口 WeatherApi.kt> 知识点 : Retrofit用注解(@GET/@POST/@Query/@Path)声明式定义HTTP API。suspend函数让Retrofit自动在后台线程执行网络请求。kotlinpackage com.example.weather.data.remoteimport com.example.weather.data.remote.dto.ForecastResponseimport com.example.weather.data.remote.dto.WeatherResponseimport retrofit2.http.GETimport retrofit2.http.Query/** * OpenWeatherMap API 接口定义 * 文档: https://openweathermap.org/api * * 知识点: * - Retrofit @GET 注解: 声明 HTTP GET 请求 * - @Query: URL 查询参数 * - suspend: 协程挂起函数,Retrofit 自动在后台线程执行 */interface WeatherApi { /** 获取当前天气 */ @GET("weather") suspend fun getCurrentWeather( @Query("q") city: String, @Query("appid") apiKey: String = API_KEY, @Query("units") units: String = "metric", // 摄氏度 @Query("lang") lang: String = "zh_cn" ): WeatherResponse /** 获取 5 天预报 (每 3 小时) */ @GET("forecast") suspend fun getForecast( @Query("q") city: String, @Query("appid") apiKey: String = API_KEY, @Query("units") units: String = "metric", @Query("lang") lang: String = "zh_cn" ): ForecastResponse companion object { const val BASE_URL = "https://api.openweathermap.org/data/2.5/" // 请替换为你自己的 API Key (免费注册: https://openweathermap.org/api) const val API_KEY = "YOUR_API_KEY" }}---### 3.2 数据传输对象 WeatherResponse.kt> 知识点 : @Serializable是Kotlin Serialization的注解,编译时生成序列化/反序列化代码(比Gson/Moshi的运行时反射更快)。@SerialName映射JSON字段名。kotlinpackage com.example.weather.data.remote.dtoimport kotlinx.serialization.SerialNameimport kotlinx.serialization.Serializable/** 当前天气 API 响应 */@Serializabledata class WeatherResponse( val name: String, // 城市名 val main: MainInfo, val weather: List<WeatherInfo>, val wind: WindInfo, val sys: SysInfo)@Serializabledata class MainInfo( val temp: Double, // 当前温度 @SerialName("feels_like") val feelsLike: Double, @SerialName("temp_min") val tempMin: Double, @SerialName("temp_max") val tempMax: Double, val humidity: Int // 湿度 %)@Serializabledata class WeatherInfo( val id: Int, val main: String, // 天气类型: Clear, Clouds, Rain... val description: String, // 中文描述 val icon: String // 图标代码)@Serializabledata class WindInfo(val speed: Double)@Serializabledata class SysInfo(val country: String)/** 预报 API 响应 */@Serializabledata class ForecastResponse( val list: List<ForecastItem>, val city: CityInfo)@Serializabledata class ForecastItem( val dt: Long, // Unix 时间戳 val main: MainInfo, val weather: List<WeatherInfo>, @SerialName("dt_txt") val dtTxt: String // "2024-01-21 12:00:00")@Serializabledata class CityInfo(val name: String, val country: String)---### 3.3 领域模型 Weather.kt> 知识点 : 领域模型是业务层的数据类,和API返回的DTO解耦。这样API格式变化不影响UI层。kotlinpackage com.example.weather.domain.model/** 领域模型 --- 当前天气 */data class Weather( val cityName: String, val country: String, val temperature: Double, val feelsLike: Double, val tempMin: Double, val tempMax: Double, val humidity: Int, val windSpeed: Double, val description: String, val iconUrl: String)/** 领域模型 --- 预报项 */data class Forecast( val dateTime: String, val temperature: Double, val tempMin: Double, val tempMax: Double, val description: String, val iconUrl: String)---### 3.4 数据仓库 WeatherRepository.kt> 知识点 : Repository模式封装数据来源(网络/本地),ViewModel只和Repository交互。@Inject constructor让Hilt自动注入依赖。kotlinpackage com.example.weather.data.repositoryimport com.example.weather.data.remote.WeatherApiimport com.example.weather.domain.model.Forecastimport com.example.weather.domain.model.Weatherimport javax.inject.Injectimport javax.inject.Singleton/** * 天气仓库 --- 封装 API 调用,将 DTO 转换为领域模型 * * 知识点: * - @Inject constructor: Hilt 自动注入依赖 * - @Singleton: 单例作用域 * - DTO → Domain 转换: 解耦网络响应和业务模型 */@Singletonclass WeatherRepository @Inject constructor( private val api: WeatherApi) { /** 获取当前天气 */ suspend fun getCurrentWeather(city: String): Weather { val response = api.getCurrentWeather(city) val weather = response.weather.firstOrNull() return Weather( cityName = response.name, country = response.sys.country, temperature = response.main.temp, feelsLike = response.main.feelsLike, tempMin = response.main.tempMin, tempMax = response.main.tempMax, humidity = response.main.humidity, windSpeed = response.wind.speed, description = weather?.description ?: "未知", iconUrl = "https://openweathermap.org/img/wn/${weather?.icon ?: "01d"}@2x.png" ) } /** 获取预报 */ suspend fun getForecast(city: String): List<Forecast> { val response = api.getForecast(city) return response.list.map { item -> val weather = item.weather.firstOrNull() Forecast( dateTime = item.dtTxt, temperature = item.main.temp, tempMin = item.main.tempMin, tempMax = item.main.tempMax, description = weather?.description ?: "未知", iconUrl = "https://openweathermap.org/img/wn/${weather?.icon ?: "01d"}@2x.png" ) } }}---### 3.5 Hilt依赖注入 NetworkModule.kt> 知识点 : @Module声明Hilt模块,@Provides告诉Hilt如何创建实例,@Singleton全局单例。依赖链: Module提供OkHttp → 提供Retrofit → 提供API接口。kotlinpackage com.example.weather.diimport com.example.weather.data.remote.WeatherApiimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.components.SingletonComponentimport kotlinx.serialization.json.Jsonimport okhttp3.MediaType.Companion.toMediaTypeimport okhttp3.OkHttpClientimport okhttp3.logging.HttpLoggingInterceptorimport retrofit2.Retrofitimport retrofit2.converter.kotlinx.serialization.asConverterFactoryimport javax.inject.Singleton/** * Hilt 网络模块 --- 提供 OkHttp, Retrofit, API 实例 * * 知识点: * - @Module + @InstallIn: 告诉 Hilt 这个模块安装在哪个组件 * - @Provides: 告诉 Hilt 如何创建实例 * - @Singleton: 全局单例 */@Module@InstallIn(SingletonComponent::class)object NetworkModule { @Provides @Singleton fun provideJson(): Json = Json { ignoreUnknownKeys = true // 忽略 API 返回的未知字段 isLenient = true } @Provides @Singleton fun provideOkHttpClient(): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build() } @Provides @Singleton fun provideRetrofit(client: OkHttpClient, json: Json): Retrofit { return Retrofit.Builder() .baseUrl(WeatherApi.BASE_URL) .client(client) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() } @Provides @Singleton fun provideWeatherApi(retrofit: Retrofit): WeatherApi { return retrofit.create(WeatherApi::class.java) }}---### 3.6 ViewModel WeatherViewModel.kt> 知识点 : @HiltViewModel让Hilt管理ViewModel创建和注入。sealed class统一管理Loading/Success/Error三种UI状态。kotlinpackage com.example.weather.viewmodelimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.example.weather.data.repository.WeatherRepositoryimport com.example.weather.domain.model.Forecastimport com.example.weather.domain.model.Weatherimport dagger.hilt.android.lifecycle.HiltViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport javax.inject.Inject/** * 天气 ViewModel --- 管理 UI 状态 * * 知识点: * - @HiltViewModel: Hilt 自动管理 ViewModel 的创建和注入 * - sealed class UiState: 统一管理 Loading/Success/Error 状态 * - viewModelScope: 自动在 ViewModel 销毁时取消协程 */@HiltViewModelclass WeatherViewModel @Inject constructor( private val repository: WeatherRepository) : ViewModel() { private val _weatherState = MutableStateFlow<WeatherUiState>(WeatherUiState.Idle) val weatherState: StateFlow<WeatherUiState> = _weatherState.asStateFlow() private val _forecastState = MutableStateFlow<ForecastUiState>(ForecastUiState.Idle) val forecastState: StateFlow<ForecastUiState> = _forecastState.asStateFlow() private val _searchCity = MutableStateFlow("Beijing") val searchCity: StateFlow<String> = _searchCity.asStateFlow() init { searchWeather("Beijing") } fun onCityChanged(city: String) { _searchCity.value = city } fun searchWeather(city: String = _searchCity.value) { if (city.isBlank()) return viewModelScope.launch { _weatherState.value = WeatherUiState.Loading _forecastState.value = ForecastUiState.Loading try { val weather = repository.getCurrentWeather(city) _weatherState.value = WeatherUiState.Success(weather) val forecast = repository.getForecast(city) _forecastState.value = ForecastUiState.Success(forecast) } catch (e: Exception) { _weatherState.value = WeatherUiState.Error(e.message ?: "获取天气失败") _forecastState.value = ForecastUiState.Error(e.message ?: "获取预报失败") } } }}sealed class WeatherUiState { data object Idle : WeatherUiState() data object Loading : WeatherUiState() data class Success(val weather: Weather) : WeatherUiState() data class Error(val message: String) : WeatherUiState()}sealed class ForecastUiState { data object Idle : ForecastUiState() data object Loading : ForecastUiState() data class Success(val forecasts: List<Forecast>) : ForecastUiState() data class Error(val message: String) : ForecastUiState()}---### 3.7 主页面 HomeScreen.kt> 知识点 : hiltViewModel()在Compose中获取Hilt管理的ViewModel。when(state)配合sealed class实现状态UI映射。kotlinpackage com.example.weather.ui.screenimport androidx.compose.animation.AnimatedVisibilityimport androidx.compose.foundation.layout.*import androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.foundation.text.KeyboardActionsimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Searchimport androidx.compose.material3.*import androidx.compose.runtime.*import androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.hilt.navigation.compose.hiltViewModelimport com.example.weather.ui.component.ForecastItemimport com.example.weather.ui.component.WeatherCardimport com.example.weather.viewmodel.*/** * 天气主页 --- 搜索城市、显示当前天气和预报 */@OptIn(ExperimentalMaterial3Api::class)@Composablefun HomeScreen(viewModel: WeatherViewModel = hiltViewModel()) { val weatherState by viewModel.weatherState.collectAsState() val forecastState by viewModel.forecastState.collectAsState() val city by viewModel.searchCity.collectAsState() Scaffold(topBar = { TopAppBar(title = { Text("天气预报") }) }) { padding -> LazyColumn( modifier = Modifier.fillMaxSize().padding(padding), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { // 搜索栏 item { OutlinedTextField( value = city, onValueChange = { viewModel.onCityChanged(it) }, label = { Text("输入城市名 (英文)") }, trailingIcon = { IconButton(onClick = { viewModel.searchWeather() }) { Icon(Icons.Default.Search, "搜索") } }, keyboardActions = KeyboardActions(onDone = { viewModel.searchWeather() }), singleLine = true, modifier = Modifier.fillMaxWidth() ) } // 当前天气 item { when (val state = weatherState) { is WeatherUiState.Loading -> Box(Modifier.fillMaxWidth(), Alignment.Center) { CircularProgressIndicator() } is WeatherUiState.Success -> WeatherCard(state.weather) is WeatherUiState.Error -> Text("错误: ${state.message}", color = MaterialTheme.colorScheme.error) is WeatherUiState.Idle -> {} } } // 预报标题 item { AnimatedVisibility(forecastState is ForecastUiState.Success) { Text("未来预报", style = MaterialTheme.typography.titleMedium) } } // 预报列表 if (forecastState is ForecastUiState.Success) { val forecasts = (forecastState as ForecastUiState.Success).forecasts items(forecasts.take(16)) { forecast -> ForecastItem(forecast) } } } }}---### 3.8 天气卡片 WeatherCard.kt> 知识点 : Coil的AsyncImage异步加载网络图片。Material3的Card组件自带圆角和阴影。kotlinpackage com.example.weather.ui.componentimport androidx.compose.foundation.layout.*import androidx.compose.material3.*import androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport coil3.compose.AsyncImageimport com.example.weather.domain.model.Weather/** 当前天气卡片 --- 显示温度、描述、湿度、风速 */@Composablefun WeatherCard(weather: Weather, modifier: Modifier = Modifier) { Card(modifier = modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(4.dp)) { Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text("${weather.cityName}, ${weather.country}", style = MaterialTheme.typography.titleLarge) Spacer(Modifier.height(8.dp)) AsyncImage(model = weather.iconUrl, contentDescription = weather.description, modifier = Modifier.size(80.dp)) Text("${weather.temperature.toInt()}°C", fontSize = 48.sp, fontWeight = FontWeight.Bold) Text(weather.description, style = MaterialTheme.typography.bodyLarge) Spacer(Modifier.height(12.dp)) Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("体感", style = MaterialTheme.typography.labelSmall) Text("${weather.feelsLike.toInt()}°C") } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("湿度", style = MaterialTheme.typography.labelSmall) Text("${weather.humidity}%") } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("风速", style = MaterialTheme.typography.labelSmall) Text("${weather.windSpeed} m/s") } } } }}---## 四、知识点总结| 知识点 | 文件 | 说明 ||--------|-----|------|| Retrofit @GET/@Query | WeatherApi.kt | 声明式HTTP接口 || @Serializable | WeatherResponse.kt | 编译时JSON序列化 || @SerialName | WeatherResponse.kt | JSON字段名映射 || Repository模式 | WeatherRepository.kt | 封装数据来源 || @Module @Provides | NetworkModule.kt | Hilt依赖提供 || @HiltViewModel | WeatherViewModel.kt | Hilt管理ViewModel || sealed class | WeatherViewModel.kt | 状态模式 || Coil AsyncImage | WeatherCard.kt | 异步图片加载 |## 五、运行准备1. 注册OpenWeatherMap免费API Key: https://openweathermap.org/api2. 替换WeatherApi.kt中的YOUR_API_KEY3. Android Studio打开projects/02-weather,Run运行> 系列导航 : 第2/6篇。下一篇: 项目③待办事项App --- 学习Room数据库和Clean Architecture。
Android实战项目② Retrofit+Hilt开发天气预报App 完整源码详解
明天就是Friday2026-04-22 8:31
相关推荐
哑巴湖小水怪1 天前
Android的架构是四层还是五层2501_916008891 天前
深入解析iOS应用启动性能优化策略与实践美狐美颜SDK开放平台1 天前
短视频/直播双场景美颜SDK开发方案:接入、功能、架构详解untE EADO1 天前
在 MySQL 中使用 `REPLACE` 函数iblade1 天前
Android CLI And Skills 3x faster阿巴斯甜1 天前
SharedUnPeekLiveData和UnPeekBus的区别:阿巴斯甜1 天前
UnPeek-LiveData的使用:我就是马云飞1 天前
我废了!大厂10年的我面了20家公司,面试官让我回去等通知!limuyang21 天前
在 Android 上用上原生的 xxHash,性能直接拉满Fate_I_C1 天前
ViewModel 的生命周期与数据保持