Menu
CoddyTech

Ứng dụng thời tiết: thử lại

Thuộc phần Dữ liệu và mạng trong hành trình Flutter của Coddy. Bài học 26 trên 27.

Người dùng mở ứng dụng thời tiết trên tàu, trong đường hầm và trên núi. Khi không có tín hiệu, ứng dụng nên nói rõ điều đó và đưa ra cách quay lại, thay vì hiển thị vòng xoay không bao giờ dừng hoặc một dòng văn bản báo lỗi.

Hãy dành một màn hình nhỏ riêng cho lỗi: một biểu tượng, một câu dễ hiểu, thông tin chi tiết bên dưới và nút Thử lại tạo một future mới trong setState. Đặt màn hình này trong phương thức riêng, buildError, cạnh buildWeather, để build vẫn ngắn gọn.

if (snapshot.connectionState == ConnectionState.waiting) {
  return const CircularProgressIndicator();
}
if (snapshot.hasError) {
  return buildError(snapshot.error!);
}
return buildWeather(snapshot.data!);

Kiểm tra ConnectionState.waiting trước sẽ hiển thị vòng xoay trong mỗi lần tải, kể cả lần thử thứ hai. Khi future hoàn tất, sẽ có lỗi hoặc dữ liệu, nên dòng cuối có thể dùng snapshot.data!.

Để xem trong bản xem trước, hãy viết offline = true; ở dòng đầu tiên của main rồi nhấn Chạy. Xóa dòng đó trước khi nhấn Kiểm tra.

challenge icon

Thử thách

Trung bình

Thêm màn hình lỗi cho ứng dụng:

  • một phương thức (method) buildError(Object error): Icons.cloud_off (kích thước 64), Could not load the weather (kích thước 20), văn bản lỗi và nút Thử lại
  • một phương thức tryAgain đặt một loadWeather() mới trong setState
  • trong builder, kiểm tra ConnectionState.waiting trước, rồi kiểm tra lỗi, sau đó hiển thị thời tiết

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));
  }

  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.hasError) {
              return Text('Could not load: ${snapshot.error}');
            }
            if (snapshot.hasData) {
              return buildWeather(snapshot.data!);
            }
            return const CircularProgressIndicator();
          },
        ),
      ),
    );
  }

  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)),
              ],
            ),
          ),
      ],
    );
  }
}

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