← Назад к вопросам

Реализовать приложение прогноза погоды

2.3 Middle🔥 201 комментариев
#Flutter виджеты#Нативная интеграция#Работа с сетью

Условие

Создайте приложение для отображения прогноза погоды.

Требования

  1. Ввод названия города
  2. Отображение текущей погоды (температура, описание, иконка)
  3. Использование публичного API погоды (OpenWeatherMap или аналог)
  4. Обработка ошибок (город не найден, нет интернета)
  5. Красивый 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 ключ — проверка авторизации

Этот подход обеспечивает масштабируемость, тестируемость и удобство поддержки кода.