تطبيق الطقس: اختر مدينة
جزء من قسم البيانات والشبكة في رحلة 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 في الأسفل.
التحدي
صعبأضف أداة لاختيار المدينة:
- أضف
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,
);
}
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البيانات والشبكة
3عندما تفشل الاستدعاءات
التقاط الأخطاء باستخدام tryإطلاق خطأالتنظيف باستخدام finallyحالة الخطأ في FutureBuilderزر للمحاولة مجددًا6مشروع: تطبيق الطقس
تطبيق الطقس: الآنتطبيق الطقس: الأيام الثلاثة القادمةتدرّب بنفسك: مترجم Flutter عبر الإنترنت