← Назад к вопросам
Реализовать приложение прогноза погоды
2.3 Middle🔥 201 комментариев
#Flutter виджеты#Нативная интеграция#Работа с сетью
Условие
Создайте приложение для отображения прогноза погоды.
Требования
- Ввод названия города
- Отображение текущей погоды (температура, описание, иконка)
- Использование публичного API погоды (OpenWeatherMap или аналог)
- Обработка ошибок (город не найден, нет интернета)
- Красивый UI с погодной тематикой
Дополнительные баллы
- Определение геолокации и автоматическое получение погоды
- Прогноз на 5 дней
- Сохранение избранных городов
- Анимированные погодные эффекты (дождь, снег)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Приложение Прогноза Погоды на Flutter
Архитектура и структура проекта
Построим приложение по принципам Clean Architecture с разделением на слои:
lib/
├── data/
│ ├── models/ # Data models
│ ├── repositories/ # Реализация репозиториев
│ └── datasources/ # API и локальное хранилище
├── domain/
│ ├── entities/ # Бизнес-сущности
│ ├── repositories/ # Абстракции
│ └── usecases/ # Бизнес-логика
├── presentation/
│ ├── pages/
│ ├── widgets/
│ ├── controllers/ # State management
│ └── theme/
└── core/
└── utils/ # Утилиты
Модели данных
// domain/entities/weather.dart
class Weather {
final String city;
final double temperature;
final String description;
final String iconCode;
final double humidity;
final double windSpeed;
final DateTime timestamp;
Weather({
required this.city,
required this.temperature,
required this.description,
required this.iconCode,
required this.humidity,
required this.windSpeed,
required this.timestamp,
});
}
// data/models/weather_model.dart
class WeatherModel extends Weather {
WeatherModel({
required String city,
required double temperature,
required String description,
required String iconCode,
required double humidity,
required double windSpeed,
required DateTime timestamp,
}) : super(
city: city,
temperature: temperature,
description: description,
iconCode: iconCode,
humidity: humidity,
windSpeed: windSpeed,
timestamp: timestamp,
);
factory WeatherModel.fromJson(Map<String, dynamic> json) {
return WeatherModel(
city: json["name"] ?? "Unknown",
temperature: (json["main"]["temp"] as num).toDouble(),
description: json["weather"][0]["main"] ?? "Clear",
iconCode: json["weather"][0]["icon"] ?? "01d",
humidity: (json["main"]["humidity"] as num).toDouble(),
windSpeed: (json["wind"]["speed"] as num).toDouble(),
timestamp: DateTime.now(),
);
}
}
Управление состоянием с GetX
// presentation/controllers/weather_controller.dart
class WeatherController extends GetxController {
final WeatherRepository _repository;
final isLoading = false.obs;
final weather = Rxn<Weather>();
final error = Rxn<String>();
final favorites = <String>[].obs;
final searchController = TextEditingController();
WeatherController({required WeatherRepository repository})
: _repository = repository;
@override
void onInit() {
super.onInit();
_loadFavorites();
_getCurrentLocationWeather();
}
Future<void> searchCity(String cityName) async {
if (cityName.isEmpty) return;
isLoading.value = true;
error.value = null;
try {
final result = await _repository.getWeatherByCity(cityName);
weather.value = result;
searchController.clear();
} catch (e) {
error.value = _mapErrorMessage(e);
} finally {
isLoading.value = false;
}
}
Future<void> _getCurrentLocationWeather() async {
isLoading.value = true;
try {
final position = await _getLocation();
final result = await _repository.getWeatherByCoordinates(
position.latitude,
position.longitude,
);
weather.value = result;
} catch (e) {
error.value = "Не удалось определить локацию";
} finally {
isLoading.value = false;
}
}
void toggleFavorite(String city) {
if (favorites.contains(city)) {
favorites.remove(city);
} else {
favorites.add(city);
}
_saveFavorites();
}
String _mapErrorMessage(dynamic error) {
if (error is SocketException) {
return "Нет интернета. Проверьте соединение";
} else if (error.toString().contains("404")) {
return "Город не найден. Попробуйте другой";
}
return "Ошибка загрузки погоды";
}
Future<Position> _getLocation() async {
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
await Geolocator.requestPermission();
}
return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
}
void _loadFavorites() async {
// Загрузка из SharedPreferences
}
void _saveFavorites() async {
// Сохранение в SharedPreferences
}
}
UI слой с анимациями
// presentation/pages/home_page.dart
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
final controller = Get.find<WeatherController>();
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF667EEA),
Color(0xFF764BA2),
],
),
),
child: SafeArea(
child: Column(
children: [
_buildSearchBar(controller),
SizedBox(height: 20),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (controller.error.value != null) {
return Center(
child: Text(
controller.error.value!,
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
);
}
if (controller.weather.value == null) {
return Center(
child: Text(
"Введите город",
style: TextStyle(
color: Colors.white70,
fontSize: 18,
),
),
);
}
return _buildWeatherCard(controller.weather.value!);
}),
),
_buildFavoritesBar(controller),
],
),
),
),
);
}
Widget _buildSearchBar(WeatherController controller) {
return Padding(
padding: EdgeInsets.all(16),
child: TextField(
controller: controller.searchController,
style: TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: "Введите город",
hintStyle: TextStyle(color: Colors.white60),
prefixIcon: Icon(Icons.location_on, color: Colors.white),
suffixIcon: IconButton(
icon: Icon(Icons.search, color: Colors.white),
onPressed: () {
controller.searchCity(controller.searchController.text);
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.white30),
),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
),
onSubmitted: (value) => controller.searchCity(value),
),
);
}
Widget _buildWeatherCard(Weather weather) {
return FadeTransition(
opacity: Tween(begin: 0.0, end: 1.0).animate(_fadeController),
child: Center(
child: Card(
elevation: 20,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: EdgeInsets.all(40),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
Colors.white,
Colors.blue[50]!,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
weather.city,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 20),
_buildWeatherIcon(weather.iconCode),
SizedBox(height: 20),
Text(
"${weather.temperature.toStringAsFixed(1)}°C",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Color(0xFF667EEA),
),
),
SizedBox(height: 10),
Text(
weather.description,
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
),
SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildWeatherDetail(
Icons.water_drop,
"Влажность",
"${weather.humidity.toStringAsFixed(0)}%",
),
_buildWeatherDetail(
Icons.air,
"Ветер",
"${weather.windSpeed.toStringAsFixed(1)} м/с",
),
],
),
],
),
),
),
),
);
}
Widget _buildWeatherIcon(String iconCode) {
final iconUrl = "https://openweathermap.org/img/wn/$iconCode@4x.png";
return Image.network(
iconUrl,
width: 100,
height: 100,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.cloud, size: 100, color: Colors.blue);
},
);
}
Widget _buildWeatherDetail(IconData icon, String label, String value) {
return Column(
children: [
Icon(icon, color: Color(0xFF667EEA), size: 32),
SizedBox(height: 8),
Text(
label,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.black87,
),
),
],
);
}
Widget _buildFavoritesBar(WeatherController controller) {
return Padding(
padding: EdgeInsets.all(16),
child: Obx(() {
if (controller.favorites.isEmpty) {
return SizedBox.shrink();
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (String city in controller.favorites)
Padding(
padding: EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () => controller.searchCity(city),
child: Chip(
label: Text(city, style: TextStyle(color: Colors.white)),
backgroundColor: Colors.white.withOpacity(0.2),
deleteIcon: Icon(Icons.close, color: Colors.white),
onDeleted: () => controller.toggleFavorite(city),
),
),
),
],
),
);
}),
);
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
}
Зависимости в pubspec.yaml
dependencies:
flutter:
sdk: flutter
get: ^4.6.5
http: ^1.1.0
geolocator: ^9.0.2
shared_preferences: ^2.2.0
intl: ^0.19.0
Обработка ошибок
Приложение корректно обрабатывает:
- Отсутствие интернета — проверка SocketException
- Город не найден — обработка HTTP 404
- Отсутствие разрешения на геолокацию — запрос разрешения
- Неверный API ключ — проверка авторизации
Этот подход обеспечивает масштабируемость, тестируемость и удобство поддержки кода.