Ứng dụng thời tiết: chọn thành phố
Thuộc phần Dữ liệu và mạng trong hành trình Flutter của Coddy. Bài học 27 trên 27.
Tính năng cuối cùng: một hàng thành phố ở trên cùng gồm Lisbon, Tokyo và Oslo. Chạm vào một thành phố sẽ tải thời tiết của nơi đó.
Chọn một thành phố cũng giống như tải lại với một đường dẫn khác. Giữ thành phố đã chọn trong trạng thái bằng biến city, truyền tham số cho loadWeather và tạo đường dẫn từ đó:
Future<Weather> loadWeather(String city) async {
final body = await fetch('/weather/${city.toLowerCase()}');
return Weather.fromJson(jsonDecode(body));
}Các nút ghi Tokyo, nhưng đường dẫn của máy chủ là /weather/tokyo. toLowerCase() chuyển từ dạng này sang dạng kia. Đường dẫn trên hầu hết máy chủ có phân biệt chữ hoa chữ thường, nên /weather/Tokyo sẽ trả về Not found.
Đặt hàng nút bên ngoài FutureBuilder để hàng nút vẫn hiện trên màn hình trong khi tải dữ liệu của một thành phố. Một Column với hàng nút ở trên và FutureBuilder nằm trong một Expanded bên dưới sẽ làm được điều đó.
Thử thách
KhóThêm bộ chọn thành phố:
- khai báo
String city = 'Lisbon';trong trạng thái, và dùngloadWeather(String city)để tải dữ liệu từ/weather/cùng với tên thành phố viết thường - ở đầu phần thân, đặt một
Rowcăn giữa gồm baTextButton:Lisbon,Tokyo,Oslo - khi nhấn vào một nút, cập nhật
cityvà một_weathermới trongsetState - bên dưới hàng nút, đặt
FutureBuildertrong mộtExpanded, và nút Thử lại tải lại dữ liệu cho thành phố đã chọn
Tự làm thử
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,
);
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Dữ liệu và mạng
3Khi lời gọi hàm thất bại
Bắt lỗi bằng tryNém lỗiDọn dẹp với finallyTrạng thái lỗi của FutureBuilderNút thử lại6Dự án: ứng dụng thời tiết
Ứng dụng thời tiết: hiện tạiỨng dụng thời tiết: ba ngày tớiTự luyện tập: Trình biên dịch Flutter online