Menu
CoddyTech

날씨 앱: 다시 시도하기

Coddy Flutter 여정의 데이터와 네트워크 섹션에 포함된 레슨. 27개 중 26번째.

날씨 앱은 기차 안이나 터널, 산에서 열립니다. 신호가 없으면 앱은 그 사실을 분명히 알리고 돌아갈 방법을 제공해야 합니다. 끝없이 돌아가는 로딩 표시나 오류 문구만 보여 줘서는 안 됩니다.

오류 전용 화면을 작게 구성하세요. 아이콘, 누구나 이해할 수 있는 문장, 그 아래에 표시되는 자세한 내용, 그리고 다시 시도 버튼을 넣으세요. 이 버튼은 setState에서 새로운 미래를 만듭니다. buildWeather 옆에 buildError라는 별도의 메서드를 만들어 화면을 구성하면 build를 짧게 유지할 수 있습니다.

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

ConnectionState.waiting을 먼저 확인하면 두 번째 시도를 포함해 로드할 때마다 로딩 표시가 나타납니다. 미래가 완료되면 오류나 데이터 중 하나가 있으므로 마지막 줄에서 snapshot.data!를 사용할 수 있습니다.

미리 보기에서 확인하려면 main의 첫 번째 줄에 offline = true;를 작성하고 Run을 누르세요. Check를 누르기 전에 이 줄을 제거하세요.

challenge icon

챌린지

중급

앱에 오류 화면을 추가하세요:

  • buildError(Object error) 메서드: Icons.cloud_off(크기 64), Could not load the weather(크기 20), 오류 텍스트, 그리고 Try again 버튼
  • setState에서 새로운 loadWeather()를 설정하는 tryAgain 메서드
  • 빌더에서 먼저 ConnectionState.waiting을 확인하고, 그다음 오류를 확인한 뒤, 날씨를 표시하세요

직접 해보기

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 icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

데이터와 네트워크의 모든 레슨

직접 연습해 보세요: 온라인 Flutter 컴파일러