Menu
CoddyTech

אפליקציית מזג אוויר: בחירת עיר

חלק מהיחידה נתונים ורשת במסלול ה-Flutter של Coddy. שיעור 27 מתוך 27.

התכונה האחרונה: שורת ערים בחלק העליון, ליסבון, טוקיו ואוסלו. הקשה על אחת מהן טוענת את מזג האוויר שלה.

בחירת עיר היא טעינה מחדש עם נתיב אחר. שומרים את 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, כדי שהיא תישאר על המסך בזמן טעינת עיר. Column עם השורה למעלה ועם FutureBuilder בתוך Expanded למטה עושה זאת.

challenge icon

אתגר

קשה

הוסיפו בורר עיר:

  • String city = 'Lisbon'; במצב, ו-loadWeather(String city) שטוענת את /weather/ בתוספת שם העיר באותיות קטנות
  • בחלק העליון של גוף המסך, Row ממורכזת ובה שלושה TextButton: Lisbon, Tokyo, Oslo
  • הקשה על אחד מהם מגדירה את city ואת _weather החדש בתוך setState
  • מתחת לשורה, ה-FutureBuilder בתוך Expanded, ו-Try again טוען מחדש את העיר שנבחרה

נסו בעצמכם

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 iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה נתונים ורשת

תרגלו בעצמכם: קומפיילר Flutter אונליין