跳转至

案例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. 创建项目

Bash
# 使用Android Studio创建新项目
# 包名: com.example.weather
# 最低SDK: API 24

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 {
        // 测试代码
    }
}

🎓 学习要点

通过本案例,你将掌握:

  1. ✅ 定位服务集成
  2. ✅ 网络请求(Retrofit)
  3. ✅ 数据缓存(Room)
  4. ✅ Compose UI开发
  5. ✅ 权限处理

下一步: 案例3:新闻阅读App


案例完成时间:预计5-7天