26、实战项目二:天气 App(网络请求 + 状态管理 + Canvas 绘图 + 动画)
说实话,学到这里,你手里应该已经攒了不少 Compose 的零碎技能了。但怎么把它们串起来,做成一个真正能用的 App?
我个人觉得,最好的方式就是动手做一个完整项目。今天这个天气 App,我会把网络请求、状态管理、Canvas 绘图和动画全部揉进去。做完它,你基本就能独立开发一个中等复杂度的 Compose 应用了。
项目概览:我们要做什么?
一个天气 App,核心功能很简单:输入城市名,显示当前天气和未来几天的预报。但我会在细节上做文章——用 Canvas 画一个动态的天气背景,比如晴天有太阳光晕,雨天有雨滴落下。这些动画效果,会让你的 App 看起来比那些「Hello World」demo 高级得多。
- Retrofit + Kotlin 协程做网络请求
- ViewModel + StateFlow 管理 UI 状态
- Canvas 绘制自定义天气图形
- Animatable 实现平滑动画过渡
第一步:搭建网络请求层
我习惯先把数据层搞定。毕竟没有数据,UI 画得再漂亮也是空壳。
这里用 Retrofit 请求和风天气的 API。你想想看,如果每次都手动解析 JSON,那得多痛苦。Retrofit 帮我们省了这一步。
// 定义 API 接口
interface WeatherApi {
@GET("v7/weather/now")
suspend fun getCurrentWeather(
@Query("location") location: String,
@Query("key") apiKey: String
): WeatherResponse
@GET("v7/weather/3d")
suspend fun getForecast(
@Query("location") location: String,
@Query("key") apiKey: String
): ForecastResponse
}
// 创建 Retrofit 实例
object RetrofitClient {
private const val BASE_URL = "https://devapi.qweather.com/"
val instance: WeatherApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherApi::class.java)
}
}
第二步:ViewModel 状态管理
网络请求拿到数据后,怎么通知 UI 更新?用 StateFlow。它比 LiveData 更灵活,而且完全支持协程。
我一般会在 ViewModel 里定义三种状态:加载中、成功、失败。这样 UI 层只需要观察一个 StateFlow,就能处理所有情况。
class WeatherViewModel : ViewModel() {
private val weatherApi = RetrofitClient.instance
// 密封类定义状态
sealed class WeatherState {
object Loading : WeatherState()
data class Success(val data: WeatherResponse) : WeatherState()
data class Error(val message: String) : WeatherState()
}
private val _weatherState = MutableStateFlow<WeatherState>(WeatherState.Loading)
val weatherState: StateFlow<WeatherState> = _weatherState.asStateFlow()
fun fetchWeather(city: String) {
viewModelScope.launch {
_weatherState.value = WeatherState.Loading
try {
val response = weatherApi.getCurrentWeather(city, API_KEY)
_weatherState.value = WeatherState.Success(response)
} catch (e: Exception) {
_weatherState.value = WeatherState.Error(e.message ?: "未知错误")
}
}
}
}
第三步:Canvas 绘制天气背景
这部分是我最喜欢的。用 Canvas 画天气图形,比用图片灵活得多。你可以根据天气数据动态调整颜色、大小、运动轨迹。
举个例子,画一个太阳光晕效果:
@Composable
fun SunAnimation(modifier: Modifier = Modifier) {
val alpha = remember { Animatable(0.3f) }
LaunchedEffect(Unit) {
// 无限循环的光晕呼吸效果
while (true) {
alpha.animateTo(0.8f, animationSpec = tween(1500))
alpha.animateTo(0.3f, animationSpec = tween(1500))
}
}
Canvas(modifier = modifier.fillMaxSize()) {
val centerX = size.width / 2
val centerY = size.height / 2
val radius = size.minDimension / 4
// 外圈光晕
drawCircle(
color = Color.Yellow.copy(alpha = alpha.value),
radius = radius * 1.5f
)
// 内圈太阳
drawCircle(
color = Color.Yellow,
radius = radius
)
}
}
你看,用 Animatable 控制透明度,就能做出光晕呼吸的效果。如果是雨天,我会画很多小线段,让它们从顶部下落,配合随机偏移,看起来就像真的雨滴。
第四步:动画让界面活起来
天气数据切换时,如果直接跳变,用户体验会很生硬。用 animateXxxAsState 系列函数,可以让数值平滑过渡。
比如温度数字的变化:
@Composable
fun AnimatedTemperature(temp: Int) {
val animatedTemp by animateIntAsState(
targetValue = temp,
animationSpec = tween(durationMillis = 800)
)
Text(
text = "$animatedTemp°C",
fontSize = 48.sp,
fontWeight = FontWeight.Bold
)
}
当温度从 25 变成 30 时,数字会从 25 慢慢跳到 30,而不是瞬间变化。这种细节,用户可能说不出来,但能感觉到「这个 App 用着很舒服」。
第五步:组装起来
最后,把上面所有东西拼到 Composable 里:
@Composable
fun WeatherScreen(viewModel: WeatherViewModel = viewModel()) {
val state by viewModel.weatherState.collectAsState()
Box(modifier = Modifier.fillMaxSize()) {
// 背景动画层
SunAnimation(modifier = Modifier.fillMaxSize())
// 内容层
when (state) {
is WeatherState.Loading -> CircularProgressIndicator()
is WeatherState.Success -> {
val data = (state as WeatherState.Success).data
Column {
AnimatedTemperature(data.temp)
Text("天气:${data.weather}")
Text("湿度:${data.humidity}%")
}
}
is WeatherState.Error -> {
Text("出错了:${(state as WeatherState.Error).message}")
}
}
}
}
总结一下
这个天气 App 虽然不大,但涵盖了实际项目中的核心流程:数据从网络来,经过 ViewModel 管理,最终展示在 Compose UI 上,还加了 Canvas 绘图和动画调味。
你做完之后,可以试着扩展一下:加个城市搜索历史、用 Room 缓存数据、或者画个更复杂的暴风雨动画。这些都是在现有框架上「加料」而已。
嗯,动手试试吧。代码写出来跑通的那一刻,你会觉得前面学的所有东西都值了。