Menu
CoddyTech
flag Ar iconالعربيةdown icon

تطبيق الطقس: حاول مرة أخرى

جزء من قسم البيانات والشبكة في رحلة Flutter على Coddy. الدرس 26 من 27.

تُفتح تطبيقات الطقس في القطارات والأنفاق وعلى الجبال. عندما لا تتوفر إشارة، ينبغي أن يوضح التطبيق ذلك ببساطة، وأن يتيح طريقة للعودة، لا أن يعرض مؤشر تحميل لا ينتهي أو سطرًا من نص الخطأ.

اجعل للخطأ شاشة صغيرة خاصة به: أيقونة، وجملة يفهمها المستخدم، والتفاصيل تحتها، وزر حاول مرة أخرى ينشئ مستقبلًا جديدًا في setState. ضع ذلك في دالة خاصة به، buildError، بجوار buildWeather، حتى تظل build قصيرة.

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

يؤدي التحقق من ConnectionState.waiting أولًا إلى إظهار مؤشر التحميل أثناء كل عملية تحميل، بما في ذلك المحاولة الثانية. بعد اكتمال المستقبل، ستكون هناك إما رسالة خطأ أو بيانات، لذا يمكن للسطر الأخير استخدام snapshot.data!.

لمشاهدة ذلك في المعاينة، اكتب offline = true; في السطر الأول من main واضغط على Run. أزل هذا السطر قبل الضغط على Check.

challenge icon

التحدي

متوسط

أضف إلى التطبيق شاشة خطأ:

  • طريقة buildError(Object error): Icons.cloud_off (الحجم 64)، Could not load the weather (الحجم 20)، نص الخطأ، وزر حاول مرة أخرى
  • طريقة tryAgain تضبط loadWeather() جديدًا داخل setState
  • في المُنشئ، تحقّق أولًا من 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 عبر الإنترنت