案例2:天气查询App¶
难度:⭐⭐⭐ 初级到中级 预计完成时间:5-7天 技术栈:Kotlin + Compose + MVVM + Retrofit + Location Services
📱 应用预览¶
功能特性¶
- ✅ 定位当前城市
- ✅ 显示实时天气
- ✅ 未来7天天气预报
- ✅ 城市搜索
- ✅ 天气图标动画
- ✅ 数据缓存
界面预览¶
Text Only
┌─────────────────────────────────┐
│ 天气查询 ⚙️ │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 🔍 搜索城市... │ │
│ └─────────────────────────────┘ │
├─────────────────────────────────┤
│ │
│ ☀️ │
│ │
│ 26°C │
│ 晴朗 │
│ │
│ 北京市 │
│ 更新于 14:30 │
│ │
├─────────────────────────────────┤
│ 湿度: 45% 风速: 3级 │
│ 气压: 1013hPa 能见度: 10km │
├─────────────────────────────────┤
│ 未来7天预报 │
│ 周一 ☀️ 26°/18° │
│ 周二 ⛅ 24°/17° │
│ 周三 🌧️ 22°/16° │
│ ... │
└─────────────────────────────────┘
🏗️ 架构设计¶
项目结构¶
Text Only
weather-app/
├── app/
│ ├── src/main/java/com/example/weather/
│ │ ├── data/
│ │ │ ├── remote/
│ │ │ │ ├── WeatherApi.kt
│ │ │ │ ├── WeatherResponse.kt
│ │ │ │ └── RetrofitClient.kt
│ │ │ ├── local/
│ │ │ │ ├── WeatherDao.kt
│ │ │ │ └── WeatherDatabase.kt
│ │ │ └── repository/
│ │ │ └── WeatherRepository.kt
│ │ ├── domain/
│ │ │ ├── model/
│ │ │ │ └── Weather.kt
│ │ │ └── usecase/
│ │ │ ├── GetCurrentWeatherUseCase.kt
│ │ │ └── GetForecastUseCase.kt
│ │ ├── presentation/
│ │ │ ├── home/
│ │ │ │ ├── HomeScreen.kt
│ │ │ │ ├── HomeViewModel.kt
│ │ │ │ └── components/
│ │ │ └── search/
│ │ │ ├── SearchScreen.kt
│ │ │ └── SearchViewModel.kt
│ │ └── location/
│ │ └── LocationManager.kt
│ └── AndroidManifest.xml
└── build.gradle.kts
🚀 快速开始¶
1. 创建项目¶
2. 配置依赖¶
Kotlin
dependencies {
// Compose UI框架
implementation(platform("androidx.compose:compose-bom:2024.02.01"))
implementation("androidx.compose.material3:material3") // Material3组件库
// ViewModel:管理UI状态,支持屏幕旋转数据保留
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
// Retrofit:类型安全的HTTP客户端,用于调用天气API
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0") // JSON自动解析
// Location:Google定位服务,获取用户当前位置
implementation("com.google.android.gms:play-services-location:21.0.1")
// Coil:Compose专用图片加载库(加载天气图标)
implementation("io.coil-kt:coil-compose:2.5.0")
// Room:本地数据库,用于缓存天气数据实现离线访问
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1") // 支持协程
ksp("androidx.room:room-compiler:2.6.1") // 编译期代码生成
// Hilt:依赖注入框架,自动管理对象创建和生命周期
implementation("com.google.dagger:hilt-android:2.48")
ksp("com.google.dagger:hilt-compiler:2.48")
}
3. 添加权限¶
XML
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
📦 核心代码实现¶
1. 数据模型¶
Kotlin
// domain/model/Weather.kt
// 当前天气数据模型
data class Weather(
val cityName: String, // 城市名称
val temperature: Int, // 当前温度(℃)
val description: String, // 天气描述(如"晴朗"、"多云")
val iconUrl: String, // 天气图标URL
val humidity: Int, // 湿度百分比
val windSpeed: Int, // 风速(m/s)
val pressure: Int, // 气压(hPa)
val visibility: Int, // 能见度(km)
val updateTime: Long // 数据更新时间戳
)
// 未来天气预报数据模型
data class Forecast(
val date: String, // 日期
val dayOfWeek: String, // 星期几
val highTemp: Int, // 最高温度
val lowTemp: Int, // 最低温度
val iconUrl: String, // 天气图标URL
val description: String // 天气描述
)
2. API接口¶
Kotlin
// data/remote/WeatherApi.kt
// Retrofit接口定义:声明天气API的请求方法
interface WeatherApi {
// 获取指定城市的当前天气
@GET("v1/current.json")
suspend fun getCurrentWeather(
@Query("key") apiKey: String, // API密钥
@Query("q") query: String, // 查询参数(城市名或经纬度)
@Query("lang") language: String = "zh" // 返回中文描述
): CurrentWeatherResponse
// 获取指定城市的未来多天天气预报
@GET("v1/forecast.json")
suspend fun getForecast(
@Query("key") apiKey: String,
@Query("q") query: String,
@Query("days") days: Int = 7, // 预报天数,默认7天
@Query("lang") language: String = "zh"
): ForecastResponse
}
3. 位置管理¶
Kotlin
// location/LocationManager.kt
// 位置管理器:封装Google定位服务,获取用户当前GPS位置
class LocationManager @Inject constructor(
private val context: Context
) {
// 融合定位客户端(综合GPS、WiFi、基站定位)
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// 获取最近一次已知位置(使用协程挂起等待回调结果)
@SuppressLint("MissingPermission")
suspend fun getCurrentLocation(): Location? = suspendCancellableCoroutine { continuation ->
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
continuation.resume(location) // 定位成功,恢复协程并返回位置
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception) // 定位失败,抛出异常
}
}
}
4. ViewModel¶
Kotlin
// presentation/home/HomeViewModel.kt
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getCurrentWeatherUseCase: GetCurrentWeatherUseCase,
private val getForecastUseCase: GetForecastUseCase,
private val locationManager: LocationManager
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
// 根据用户当前位置加载天气数据
fun loadWeatherByLocation() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) } // 显示加载状态
try {
// 获取GPS位置,若失败则使用默认城市
val location = locationManager.getCurrentLocation()
val query = location?.let { "${it.latitude},${it.longitude}" } ?: "Beijing"
// 请求当前天气和未来预报
val weather = getCurrentWeatherUseCase(query)
val forecast = getForecastUseCase(query)
// 更新UI状态:天气数据加载完成
_uiState.update {
it.copy(
weather = weather,
forecast = forecast,
isLoading = false
)
}
} catch (e: Exception) {
// 请求失败,显示错误信息
_uiState.update { it.copy(error = e.message, isLoading = false) }
}
}
}
}
5. UI界面¶
Kotlin
// presentation/home/HomeScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onSearchClick: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("天气查询") },
actions = {
IconButton(onClick = onSearchClick) {
Icon(Icons.Default.Search, contentDescription = "搜索")
}
}
)
}
) { padding ->
// 根据UI状态显示不同内容
when {
uiState.isLoading -> LoadingIndicator() // 加载中:显示进度指示器
uiState.error != null -> ErrorMessage(uiState.error!!) // 错误:显示错误信息
uiState.weather != null -> WeatherContent( // 成功:显示天气内容
weather = uiState.weather!!,
forecast = uiState.forecast,
modifier = Modifier.padding(padding)
)
}
}
}
// 天气内容主体:垂直滚动布局,依次展示各天气信息卡片
@Composable
fun WeatherContent(
weather: Weather,
forecast: List<Forecast>,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()) // 支持垂直滚动
) {
CurrentWeatherCard(weather) // 当前天气卡片(温度、图标、城市名)
WeatherDetails(weather) // 详细信息(湿度、风速、气压等)
ForecastList(forecast) // 未来7天天气预报列表
}
}
🧪 测试¶
Kotlin
class WeatherViewModelTest {
@Test
fun `load weather by location updates ui state`() = runTest {
// 测试代码
}
}
🎓 学习要点¶
通过本案例,你将掌握:
- ✅ 定位服务集成
- ✅ 网络请求(Retrofit)
- ✅ 数据缓存(Room)
- ✅ Compose UI开发
- ✅ 权限处理
下一步: 案例3:新闻阅读App
案例完成时间:预计5-7天