You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

首次开发:Jetpack Compose项目集成weatherapi.com API全流程指导

完整分步指导:从WeatherAPI获取数据到LazyColumn展示

1. 获取WeatherAPI密钥

  • 注册weatherapi.com免费账号,登录后进入后台
  • 在"My API Keys"页面复制专属API密钥,注意免费版有请求次数限制,开发时控制请求频率

2. 完善项目依赖(若未完全配置)

确保build.gradle(Module level)包含以下核心依赖:

// Retrofit核心库
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Gson JSON解析转换器
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// OkHttp网络客户端(含日志拦截器,用于调试)
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
// Jetpack Compose组件(用于LazyColumn)
implementation platform('androidx.compose:compose-bom:2023.08.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3'
// ViewModel与协程(处理异步请求)
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// Coil图片加载库(用于天气图标)
implementation 'io.coil-kt:coil-compose:2.4.0'

3. 定义API接口与数据模型

3.1 编写Retrofit API接口

根据WeatherAPI官方文档,定义核心请求接口:

interface WeatherApiService {
    // 获取当前天气
    @GET("current.json")
    suspend fun getCurrentWeather(
        @Query("key") apiKey: String,
        @Query("q") location: String,
        @Query("aqi") aqi: String = "no"
    ): CurrentWeatherResponse

    // 获取未来N天预报(默认3天)
    @GET("forecast.json")
    suspend fun getForecastWeather(
        @Query("key") apiKey: String,
        @Query("q") location: String,
        @Query("days") days: Int = 3,
        @Query("aqi") aqi: String = "no"
    ): ForecastWeatherResponse
}

3.2 构建数据模型(Data Class)

匹配WeatherAPI返回的JSON结构,创建精简版数据类(可按需扩展字段):

// 当前天气响应体
data class CurrentWeatherResponse(
    val location: Location,
    val current: Current
)

// 预报响应体
data class ForecastWeatherResponse(
    val location: Location,
    val current: Current,
    val forecast: Forecast
)

data class Location(
    val name: String,
    val country: String,
    val localtime: String
)

data class Current(
    val temp_c: Double,
    val condition: Condition,
    val humidity: Int,
    val wind_kph: Double
)

data class Condition(
    val text: String,
    val icon: String
)

data class Forecast(
    val forecastday: List<ForecastDay>
)

data class ForecastDay(
    val date: String,
    val day: Day
)

data class Day(
    val maxtemp_c: Double,
    val mintemp_c: Double,
    val condition: Condition
)

4. 创建Retrofit实例

用单例模式封装Retrofit初始化,添加日志拦截器便于调试:

object RetrofitInstance {
    private const val BASE_URL = "https://api.weatherapi.com/v1/"

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .build()

    val api: WeatherApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(WeatherApiService::class.java)
    }
}

5. 实现数据请求逻辑(ViewModel)

用ViewModel管理请求状态,处理异步调用与异常:

class WeatherViewModel : ViewModel() {
    private val _weatherState = MutableStateFlow<WeatherState>(WeatherState.Loading)
    val weatherState: StateFlow<WeatherState> = _weatherState.asStateFlow()

    // 建议从local.properties读取API密钥,不要硬编码
    private val API_KEY = "你的API密钥"

    fun fetchWeather(location: String) {
        viewModelScope.launch {
            try {
                _weatherState.value = WeatherState.Loading
                val forecastData = RetrofitInstance.api.getForecastWeather(API_KEY, location)
                _weatherState.value = WeatherState.Success(forecastData)
            } catch (e: Exception) {
                _weatherState.value = WeatherState.Error(e.message ?: "请求失败")
            }
        }
    }

    sealed class WeatherState {
        object Loading : WeatherState()
        data class Success(val data: ForecastWeatherResponse) : WeatherState()
        data class Error(val message: String) : WeatherState()
    }
}

6. 用LazyColumn展示数据(Compose UI)

编写Compose界面,根据状态切换加载、成功、错误视图:

@Composable
fun WeatherScreen(viewModel: WeatherViewModel = viewModel()) {
    val weatherState by viewModel.weatherState.collectAsState()
    var inputLocation by remember { mutableStateOf("Beijing") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // 城市搜索栏
        TextField(
            value = inputLocation,
            onValueChange = { inputLocation = it },
            label = { Text("输入城市名") },
            modifier = Modifier.fillMaxWidth(),
            trailingIcon = {
                IconButton(onClick = { viewModel.fetchWeather(inputLocation) }) {
                    Icon(Icons.Filled.Search, contentDescription = "查询天气")
                }
            }
        )

        Spacer(modifier = Modifier.height(16.dp))

        when (weatherState) {
            is WeatherViewModel.WeatherState.Loading -> {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
            }
            is WeatherViewModel.WeatherState.Success -> {
                val data = (weatherState as WeatherViewModel.WeatherState.Success).data
                // 当前天气卡片
                CurrentWeatherCard(data.location, data.current)

                Spacer(modifier = Modifier.height(16.dp))

                // 未来预报列表(LazyColumn)
                Text(text = "未来3天预报", style = MaterialTheme.typography.titleMedium)
                LazyColumn(modifier = Modifier.fillMaxWidth()) {
                    items(data.forecast.forecastday) { day ->
                        ForecastItem(day)
                    }
                }
            }
            is WeatherViewModel.WeatherState.Error -> {
                Text(
                    text = (weatherState as WeatherViewModel.WeatherState.Error).message,
                    color = MaterialTheme.colorScheme.error,
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }
        }
    }
}

@Composable
fun CurrentWeatherCard(location: Location, current: Current) {
    Card(modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "${location.name}, ${location.country}", style = MaterialTheme.typography.titleLarge)
            Text(text = "本地时间: ${location.localtime}")
            Spacer(modifier = Modifier.height(8.dp))
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(text = "${current.temp_c}°C", style = MaterialTheme.typography.displayMedium)
                Spacer(modifier = Modifier.width(16.dp))
                Image(
                    painter = rememberAsyncImagePainter(model = "https:${current.condition.icon}"),
                    contentDescription = current.condition.text,
                    modifier = Modifier.size(64.dp)
                )
                Text(text = current.condition.text, style = MaterialTheme.typography.titleMedium)
            }
            Spacer(modifier = Modifier.height(8.dp))
            Text(text = "湿度: ${current.humidity}%")
            Text(text = "风速: ${current.wind_kph} km/h")
        }
    }
}

@Composable
fun ForecastItem(day: ForecastDay) {
    Card(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), elevation = CardDefaults.cardElevation()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = day.date, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(8.dp))
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(text = "最高: ${day.day.maxtemp_c}°C")
                Spacer(modifier = Modifier.width(16.dp))
                Text(text = "最低: ${day.day.mintemp_c}°C")
                Spacer(modifier = Modifier.width(16.dp))
                Image(
                    painter = rememberAsyncImagePainter(model = "https:${day.day.condition.icon}"),
                    contentDescription = day.day.condition.text,
                    modifier = Modifier.size(48.dp)
                )
                Text(text = day.day.condition.text)
            }
        }
    }
}

额外注意事项

  • 在AndroidManifest.xml添加网络权限:<uses-permission android:name="android.permission.INTERNET" />
  • 上线前关闭OkHttp的日志拦截器,避免泄露敏感信息
  • 补充无网络状态的判断与提示,提升用户体验

内容的提问来源于stack exchange,提问作者SYNTHXX

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 11:23:24