天气应用:选择城市
本课属于 Coddy Flutter 学习旅程的“数据与网络”单元。第 27 课,共 27 课。
最后一个功能:顶部显示一排城市,分别是 Lisbon、Tokyo 和 Oslo。点击其中一个,就会加载对应城市的天气。
选择城市时,会通过不同的路径重新加载数据。将选中的 city 保存在状态中,为 loadWeather 添加一个参数,并根据它构建路径:
Future<Weather> loadWeather(String city) async {
final body = await fetch('/weather/${city.toLowerCase()}');
return Weather.fromJson(jsonDecode(body));
}按钮上显示的是 Tokyo,但服务器的路径是 /weather/tokyo。toLowerCase() 会将前者转换为后者。大多数服务器区分路径中的大小写,因此 /weather/Tokyo 会返回 Not found。
将按钮行放在 FutureBuilder 外面,这样加载城市时它也会留在屏幕上。在顶部放置按钮行,并在下方的 Expanded 中放置 FutureBuilder,组成一个 Column,就能实现这一点。
挑战
困难添加一个城市选择器:
- 在状态中添加一个
String city = 'Lisbon';,并让loadWeather(String city)获取/weather/和小写形式的城市名 - 在 body 顶部放置一行居中的
Row,其中有三个TextButton:Lisbon、Tokyo、Oslo - 点击其中一个按钮后,在
setState中设置city和新的_weather - 在这一行下方,将
FutureBuilder放在Expanded中;点击再试一次会重新加载所选城市的天气
亲手试试
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'api.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: WeatherScreen(),
);
}
}
class WeatherScreen extends StatefulWidget {
const WeatherScreen({super.key});
@override
State<WeatherScreen> createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
late Future<Weather> _weather;
@override
void initState() {
super.initState();
_weather = loadWeather();
}
Future<Weather> loadWeather() async {
final body = await fetch('/weather/lisbon');
return Weather.fromJson(jsonDecode(body));
}
void tryAgain() {
setState(() {
_weather = loadWeather();
});
}
IconData iconFor(String condition) {
return switch (condition) {
'Sunny' => Icons.wb_sunny,
'Cloudy' => Icons.cloud,
'Rainy' => Icons.umbrella,
'Snowy' => Icons.ac_unit,
_ => Icons.help_outline,
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Weather')),
body: Center(
child: FutureBuilder<Weather>(
future: _weather,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return buildError(snapshot.error!);
}
return buildWeather(snapshot.data!);
},
),
),
);
}
Widget buildWeather(Weather weather) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(weather.city, style: const TextStyle(fontSize: 32)),
Icon(iconFor(weather.condition), size: 96, color: Colors.orange),
Text(
'${weather.temperature}°',
style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
Text(weather.condition, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 32),
for (final day in weather.forecast)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 8),
child: Row(
children: [
Icon(iconFor(day.condition)),
const SizedBox(width: 16),
Expanded(
child: Text(
DateFormat('EEEE').format(day.date),
style: const TextStyle(fontSize: 18),
),
),
Text('${day.high}° / ${day.low}°', style: const TextStyle(fontSize: 18)),
],
),
),
],
);
}
Widget buildError(Object error) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off, size: 64),
const SizedBox(height: 16),
const Text('Could not load the weather', style: TextStyle(fontSize: 20)),
Text('$error'),
const SizedBox(height: 16),
ElevatedButton(onPressed: tryAgain, child: const Text('Try again')),
],
);
}
}
class Weather {
Weather({
required this.city,
required this.temperature,
required this.condition,
required this.forecast,
});
final String city;
final int temperature;
final String condition;
final List<DayForecast> forecast;
factory Weather.fromJson(Map<String, dynamic> json) {
final days = json['forecast'] as List<dynamic>;
return Weather(
city: json['city'] as String,
temperature: json['temperature'] as int,
condition: json['condition'] as String,
forecast: days
.map((day) => DayForecast.fromJson(day as Map<String, dynamic>))
.toList(),
);
}
}
class DayForecast {
DayForecast({
required this.date,
required this.condition,
required this.high,
required this.low,
});
final DateTime date;
final String condition;
final int high;
final int low;
factory DayForecast.fromJson(Map<String, dynamic> json) {
return DayForecast(
date: DateTime.parse(json['date'] as String),
condition: json['condition'] as String,
high: json['high'] as int,
low: json['low'] as int,
);
}
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
数据与网络中的所有课
自己练习: Flutter 在线编译器