Menu
CoddyTech

Ứ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 đó.

challenge icon

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ùng loadWeather(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 Row căn giữa gồm ba TextButton: Lisbon, Tokyo, Oslo
  • khi nhấn vào một nút, cập nhật city và một _weather mới trong setState
  • bên dưới hàng nút, đặt FutureBuilder trong một Expanded, 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,
    );
  }
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch Flutter online