diff --git a/README.md b/README.md index 7f13d07..37835de 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,15 @@ # Weather App for Friflex -Тестовое приложение для полуения текущей погоды и прогноза погоды на 3 дня. -Код приложения покрыт тестами. +Тестовое приложение для полуения текущей погоды и прогноза погоды на 3 дня. + +Код приложения покрыт юнит-тестами. В случае проблем после клонирования выполнить flutter pub run build_runner build --delete-conflicting-outputs В приложении реализованы: -- работа с двумя запросами API погоды: - - запрос текущей погоды - - запрос прогноза погоды на 3 дня с интревалом 3 часа -- отображение экрана текущей погоды с: - - иконкой соответствующего состояния погоды - - текстового описания состояния погоды - - температуры - - влажности - - сокрости ветра -- отображение экрана списка прогнозов погоды на 3 дня с интервалом 3 часа с сортировкой прогнозов погоды по температуре, начиная с самого холодного прогноза и отображающих: - - иконку соответствующего состояния погоды - - текстовое описания состояния погоды - - температуру - - влажность - - сокрость ветра +- работа с двумя запросами API погоды: запрос текущей погоды, запрос прогноза погоды на 3 дня с интревалом 3 часа +- отображение экрана текущей погоды с: иконкой соответствующего состояния погоды, текстового описания состояния погоды, температуры, влажности, скорости ветра +- отображение экрана списка прогнозов погоды на 3 дня с интервалом 3 часа с сортировкой прогнозов погоды по температуре, начиная с самого холодного прогноза и отображающих иконку соответствующего состояния погоды, текстовое описания состояния погоды, температуру, влажность, сокрость ветра - поле ввода валидирует вводимый текст по длине символов и при подтверждении пустого поля ввода отображается текст ошибки - использована библиотека для получения состояния интернет-соединения connectivity_plus и при всех интернет запросах проверяется состояние сети - последнее введенное значение города сохраняется в системных настройках и при следующем входе открвается сразу экран текущей погоды diff --git a/lib/app/app_strings.dart b/lib/app/app_strings.dart new file mode 100644 index 0000000..f8c3d2c --- /dev/null +++ b/lib/app/app_strings.dart @@ -0,0 +1,26 @@ +//строковые константы приложения +abstract class AppStrings { + AppStrings._(); + + static const errorConnection = 'Ошибка. Проверьте доступ к сети интернет.'; + static const errorNoCityName = 'Необходимо ввести название города'; + static const errorShortName = 'Слишком короткое название'; + static const errorUnexpected = 'Неожиданная ошибка'; + + static const backButtonText = 'Назад'; + static const refreshButtonText = 'Обновить'; + + static const labelTemperature = 'Температура:'; + static const labelHumidity = 'Влажность:'; + static const labelWindSpeed = 'Скорость ветра:'; + static const labelActualForecast = 'Прогноз актуален на:'; + static const labelGetWeather = 'Узнать погоду в городе:'; + static const labelConfirmButton = 'Подтвердить'; + static const labelChooseAnother = 'Выбрать другой'; + static const labelCurrentWeather = 'Текущая погода'; + static const labelCity = 'Город'; + static const labelForecastFull = 'Прогноз погоды'; + static const labelForecastShort = 'Прогноз'; + + static const labelWindUnits = 'м/с'; +} diff --git a/lib/app/app_theme.dart b/lib/app/app_theme.dart new file mode 100644 index 0000000..9587cc2 --- /dev/null +++ b/lib/app/app_theme.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +//класс для управления темой приложения +abstract class AppTheme { + AppTheme._(); + + static final lightTheme = ThemeData( + primarySwatch: Colors.purple, + snackBarTheme: const SnackBarThemeData( + shape: StadiumBorder(), + backgroundColor: Colors.purple, + behavior: SnackBarBehavior.floating, + actionTextColor: Colors.white, + ), + ); +} diff --git a/lib/app/app_utils.dart b/lib/app/app_utils.dart new file mode 100644 index 0000000..bb01584 --- /dev/null +++ b/lib/app/app_utils.dart @@ -0,0 +1,10 @@ +import 'package:intl/intl.dart'; + +//утилиты для использования на всех страницах +String formatDateTime(DateTime? dateTime) { + if (dateTime != null) { + return DateFormat("dd-MM-yyyy HH:mm").format(dateTime); + } else { + return '-'; + } +} diff --git a/lib/data/dto/current_weather/current_weather_dto.dart b/lib/data/dto/current_weather/current_weather_dto.dart index c764b6f..19f858c 100644 --- a/lib/data/dto/current_weather/current_weather_dto.dart +++ b/lib/data/dto/current_weather/current_weather_dto.dart @@ -34,8 +34,15 @@ class CurrentWeatherDTO with _$CurrentWeatherDTO { _$CurrentWeatherDTOFromJson(json); } +//маппер данных +//преобразование форматов времени extension CurrentWeatherMapper on CurrentWeatherDTO { CurrentWeatherEntity toEntity() { + final time = dt?.toInt(); + DateTime? convertedTime; + if (time != null) { + convertedTime = DateTime.fromMillisecondsSinceEpoch(time * 1000); + } return CurrentWeatherEntity( coord: coord?.toEntity(), weather: weather?.map((e) => e.toEntity()).toList(), @@ -44,7 +51,7 @@ extension CurrentWeatherMapper on CurrentWeatherDTO { visibility: visibility, wind: wind?.toEntity(), clouds: clouds?.toEntity(), - dt: DateTime.fromMillisecondsSinceEpoch(dt?.toInt() ?? 0), + dt: convertedTime, sys: sys?.toEntity(), timezone: timezone, id: id, diff --git a/lib/data/dto/forecast_weather/forecast_part_dto.dart b/lib/data/dto/forecast_weather/forecast_part_dto.dart index 5fb043b..73a2b0f 100644 --- a/lib/data/dto/forecast_weather/forecast_part_dto.dart +++ b/lib/data/dto/forecast_weather/forecast_part_dto.dart @@ -30,11 +30,18 @@ class ForecastPartDTO with _$ForecastPartDTO { _$ForecastPartDTOFromJson(json); } +//маппер данных +//реализация преобразования даты extension ForecastPartMapper on ForecastPartDTO { ForecastPartEntity toEntity() { + final time = dt?.toInt(); + DateTime? convertedTime; + if (time != null) { + convertedTime = DateTime.fromMillisecondsSinceEpoch(time * 1000); + } return ForecastPartEntity( clouds: clouds?.toEntity(), - dt: DateTime.fromMillisecondsSinceEpoch(dt?.toInt() ?? 0), + dt: convertedTime, dtTxt: dtTxt, main: main?.toEntity(), pop: pop, diff --git a/lib/domain/bloc/current_weather/current_weather_cubit.dart b/lib/domain/bloc/current_weather/current_weather_cubit.dart index 77bc551..4a3cd50 100644 --- a/lib/domain/bloc/current_weather/current_weather_cubit.dart +++ b/lib/domain/bloc/current_weather/current_weather_cubit.dart @@ -26,15 +26,14 @@ class CurrentWeatherCubit extends Cubit { //и формирование соответсвующего текста ошибки //если ошибка какая-то другая - текст ошибки устанавливается другим //оба случая вырасывают новое состояние с параметром текста ошибки - await currentWeatherRepository.getCurrentWeather(cityName: cityName).then( - (weatherEntity) => - emit(CurrentWeatherState(weatherEntity: weatherEntity))) - // .catchError((error) { - // emit(CurrentWeatherState(errorMessage: 'Такой город не найден')); - // }, test: (error) => error is CityNotFoundException).catchError((error) { - // print(error.toString()); - // emit(CurrentWeatherState(errorMessage: 'Ошибка получения данных')); - // }) - ; + await currentWeatherRepository + .getCurrentWeather(cityName: cityName) + .then((weatherEntity) => + emit(CurrentWeatherState(weatherEntity: weatherEntity))) + .catchError((error) { + emit(CurrentWeatherState(errorMessage: 'Такой город не найден')); + }, test: (error) => error is CityNotFoundException).catchError((error) { + emit(CurrentWeatherState(errorMessage: 'Ошибка получения данных')); + }); } } diff --git a/lib/domain/bloc/forecast_weather/forecast_weather_cubit.dart b/lib/domain/bloc/forecast_weather/forecast_weather_cubit.dart index 93fabc8..b04b62a 100644 --- a/lib/domain/bloc/forecast_weather/forecast_weather_cubit.dart +++ b/lib/domain/bloc/forecast_weather/forecast_weather_cubit.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:bloc/bloc.dart'; import 'package:friflex_weather_app/app/app_exception.dart'; import 'package:friflex_weather_app/domain/entities/forecast_weather/forecast_part_entity.dart'; diff --git a/lib/main.dart b/lib/main.dart index 6373fba..0861add 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:friflex_weather_app/app/app_bloc_observer.dart'; import 'package:friflex_weather_app/app/app_const.dart'; +import 'package:friflex_weather_app/app/app_theme.dart'; import 'package:friflex_weather_app/di.dart'; import 'package:friflex_weather_app/domain/bloc/app_settings/app_settings_cubit.dart'; import 'package:friflex_weather_app/domain/bloc/current_weather/current_weather_cubit.dart'; @@ -53,6 +54,7 @@ class FriflexWeatherApp extends StatelessWidget { const FriflexWeatherApp({Key? key, required this.initialRoute}) : super(key: key); + //поле для передачи значения стартового маршрута final String initialRoute; @override @@ -68,9 +70,7 @@ class FriflexWeatherApp extends StatelessWidget { child: MaterialApp( debugShowCheckedModeBanner: false, title: AppConst.appName, - theme: ThemeData( - primarySwatch: Colors.purple, - ), + theme: AppTheme.lightTheme, //установка initialRoute, полученного в начале initialRoute: initialRoute, //именованные маршруты для навигации diff --git a/lib/presentation/screens/city_input_page.dart b/lib/presentation/screens/city_input_page.dart index 089814a..0b02039 100644 --- a/lib/presentation/screens/city_input_page.dart +++ b/lib/presentation/screens/city_input_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:friflex_weather_app/app/app_const.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; import 'package:friflex_weather_app/app/app_text_styles.dart'; import 'package:friflex_weather_app/domain/bloc/app_settings/app_settings_cubit.dart'; import 'package:friflex_weather_app/domain/bloc/internet_connection/connected_bloc.dart'; @@ -34,6 +35,7 @@ class CityInputTextField extends StatefulWidget { class _CityInputTextFieldState extends State { //контроллер для доступа к текстовому полю final TextEditingController cityNameController = TextEditingController(); + //текст ошибки поля для ввода String errorText = ''; @@ -51,9 +53,9 @@ class _CityInputTextFieldState extends State { if (value.length > 1) { errorText = ''; } else if (value.length == 1) { - errorText = 'Слишком короткое название'; + errorText = AppStrings.errorShortName; } else { - errorText = 'Необходимо ввести название города'; + errorText = AppStrings.errorNoCityName; } //обновление состояния setState(() {}); @@ -72,11 +74,12 @@ class _CityInputTextFieldState extends State { } else { //демонстрация уведомления в случае отсутствия интернета ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Ошибка. Проверьте доступ к сети интернет.'))); + content: Text(AppStrings.errorConnection), + )); } } else { //установка текста ошибки - errorText = 'Необходимо ввести название города'; + errorText = AppStrings.errorNoCityName; //обновление состояния setState(() {}); } @@ -91,7 +94,7 @@ class _CityInputTextFieldState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - 'Узнать погоду в городе:', + AppStrings.labelGetWeather, style: AppTextStyle.titleTextTextStyle, ), //отступ со всех сторон @@ -128,7 +131,7 @@ class _CityInputTextFieldState extends State { //кнопка для перехода на следующий экран OutlinedButton( onPressed: () => navigateToWeatherScreen(context), - child: const Text('Подтвердить'), + child: const Text(AppStrings.labelConfirmButton), ) ], )); diff --git a/lib/presentation/screens/current_weather_page.dart b/lib/presentation/screens/current_weather_page.dart index 1c44c0b..740c4f6 100644 --- a/lib/presentation/screens/current_weather_page.dart +++ b/lib/presentation/screens/current_weather_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:friflex_weather_app/app/app_const.dart'; -import 'package:friflex_weather_app/app/app_text_styles.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; import 'package:friflex_weather_app/domain/bloc/app_settings/app_settings_cubit.dart'; import 'package:friflex_weather_app/domain/bloc/current_weather/current_weather_cubit.dart'; -import 'package:friflex_weather_app/domain/entities/current_weather/current_weather_entity.dart'; +import 'package:friflex_weather_app/presentation/widgets/current_weather_card.dart'; import 'package:friflex_weather_app/presentation/widgets/error_page.dart'; +import 'package:friflex_weather_app/presentation/widgets/forecast_button.dart'; import 'package:friflex_weather_app/presentation/widgets/weather_progress_indicator.dart'; import '../../domain/bloc/internet_connection/connected_bloc.dart'; @@ -45,18 +45,19 @@ class _CurrentWeatherPageState extends State { leadingWidth: 80, //кнопка возврата к экрану выбора названия города leading: IconButton( - icon: const Text('Выбрать другой', textAlign: TextAlign.center), + icon: const Text(AppStrings.labelChooseAnother, + textAlign: TextAlign.center), //вызов навигатора для выбрасывания текущего экрана из стека onPressed: () => Navigator.pop(context), ), //составной заголовок аппбара title: Column( children: [ - const Text('Текущая погода'), + const Text(AppStrings.labelCurrentWeather), //для адаптации размера текста FittedBox( child: Text( - 'Город $cityName', + '${AppStrings.labelForecastFull} $cityName', ), ), ], @@ -98,125 +99,18 @@ class _CurrentWeatherPageState extends State { if (context.read().state is ConnectedSuccessState) { //демонстрация уведомления о проблемах с получением данных ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(state.errorMessage ?? 'Неожиданная ошибка'), + content: Text(state.errorMessage ?? AppStrings.errorUnexpected), action: SnackBarAction( - label: 'Назад', + label: AppStrings.backButtonText, onPressed: () { Navigator.of(context).pop(); }, ), )); - } else { - //демонстрация уведомления в случае отсутствия интернета - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Ошибка. Проверьте доступ к сети интернет.'))); - } - } -} - -//кнопка для открытия страницы с прогнозом -class ForecastButton extends StatelessWidget { - const ForecastButton({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return IconButton( - constraints: const BoxConstraints.expand(width: 80), - icon: const Text('Прогноз', textAlign: TextAlign.center), - onPressed: () => navigateToForecast(context), - ); - } - - //проверка текущего соединения и демонстрауия уведомления - void navigateToForecast(BuildContext context) { - //проверка состояния интернет-соединения - if (context.read().state is ConnectedSuccessState) { - //переход на страницу с прогнозом погоды - Navigator.pushNamed(context, AppConst.forecastWeatherRoute); } else { //демонстрация уведомления в случае отсутствия интернета ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Ошибка. Проверьте доступ к сети интернет.', - ), - ), - ); + const SnackBar(content: Text(AppStrings.errorConnection))); } } } - -class CurrentWeatherCard extends StatelessWidget { - const CurrentWeatherCard({ - Key? key, - required this.weather, - }) : super(key: key); - - final CurrentWeatherEntity? weather; - - @override - Widget build(BuildContext context) { - //переменная с кодом состояния погоды - final String condition = weather?.weather?.first.icon ?? '_unknown'; - return Center( - //адаптивный к размерам экрана контейнер - child: FractionallySizedBox( - //процент по ширине - widthFactor: 0.8, - //процен по высоте - heightFactor: 0.6, - child: Container( - //добавление скруленной фиолетовой рамки - decoration: BoxDecoration( - border: Border.all(color: Colors.purple), - borderRadius: const BorderRadius.all(Radius.circular(20))), - child: Center( - child: Column( - //размещение объектов колонке с равным расстоянием между и половинрй - //расстояния сверху и снизу - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - children: [ - //контейнер с закругленной рамкой и изображением - Container( - width: 100, - decoration: BoxDecoration( - border: Border.all(color: Colors.purple), - borderRadius: - const BorderRadius.all(Radius.circular(20))), - //отображение картинки с состоянием погоды - child: Image.asset( - 'assets/conditions/cond$condition.png')), - //текстовое описание состояния погоды - Text( - weather?.weather?.first.description ?? 'неизвестно', - style: AppTextStyle.parametersTextStyle, - ), - ], - ), - //отображение температуры - Text( - 'Температура: ${weather?.main?.temp} °C', - style: AppTextStyle.parametersTextStyle, - ), - //отображение влажности - Text( - 'Влажность: ${weather?.main?.humidity} %', - style: AppTextStyle.parametersTextStyle, - ), - //отображение скорости ветра - Text( - 'Скорость ветра: ${weather?.wind?.speed} м/с', - style: AppTextStyle.parametersTextStyle, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/presentation/screens/forecast_page.dart b/lib/presentation/screens/forecast_page.dart index 61a2534..3543c91 100644 --- a/lib/presentation/screens/forecast_page.dart +++ b/lib/presentation/screens/forecast_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; import 'package:friflex_weather_app/domain/bloc/forecast_weather/forecast_weather_cubit.dart'; -import 'package:friflex_weather_app/domain/entities/forecast_weather/forecast_part_entity.dart'; import 'package:friflex_weather_app/presentation/widgets/error_page.dart'; +import 'package:friflex_weather_app/presentation/widgets/forecast_card_list.dart'; import 'package:friflex_weather_app/presentation/widgets/weather_progress_indicator.dart'; import '../../domain/bloc/app_settings/app_settings_cubit.dart'; @@ -34,9 +35,16 @@ class _ForecastPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + leadingWidth: 80, + leading: IconButton( + icon: const Text(AppStrings.backButtonText, + textAlign: TextAlign.center), + //вызов навигатора для выбрасывания текущего экрана из стека + onPressed: () => Navigator.pop(context), + ), title: Column( children: [ - const Text('Прогноз погоды'), + const Text(AppStrings.labelCity), FittedBox( child: Text( 'Город $cityName', @@ -48,7 +56,8 @@ class _ForecastPageState extends State { actions: [ IconButton( constraints: const BoxConstraints.expand(width: 90), - icon: const Text('Обновить', textAlign: TextAlign.center), + icon: const Text(AppStrings.refreshButtonText, + textAlign: TextAlign.center), //обращение к кубиту прогноза погоды с вызовом функции // получения объекта прогноза погоды onPressed: () => context @@ -84,92 +93,3 @@ class _ForecastPageState extends State { ); } } - -class ForecastCardList extends StatelessWidget { - const ForecastCardList({Key? key, required this.forecastParts}) - : super(key: key); - final List? forecastParts; - - @override - Widget build(BuildContext context) { - return ListView.separated( - //отступы - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), - //общее количество - itemCount: forecastParts?.length ?? 0, - //разделение между элементами списка - separatorBuilder: (context, index) { - return const SizedBox( - height: 10, - ); - }, - itemBuilder: (context, index) { - //сохранение элемента списка - final item = forecastParts?.elementAt(index); - //построение карточки с прогнозом погоды - return ForecastCardItem( - //иконка погоды - conditionIcon: item?.weather?.first.icon ?? '_unknown', - //температура - temp: item?.main?.temp.toString() ?? '-', - //влажность - humidity: item?.main?.humidity.toString() ?? '-', - //скорость ветра - windSpeed: item?.wind?.speed.toString() ?? '-', - //время прогноза - forecastTime: item?.dtTxt ?? '-', - ); - }); - } -} - -class ForecastCardItem extends StatelessWidget { - const ForecastCardItem({ - Key? key, - required this.conditionIcon, - required this.temp, - required this.humidity, - required this.windSpeed, - required this.forecastTime, - }) : super(key: key); - - //иконка погоды - final String conditionIcon; - //температура - final String temp; - //влажность - final String humidity; - //скорость ветра - final String windSpeed; - //время прогноза - final String forecastTime; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.purple), - borderRadius: const BorderRadius.all(Radius.circular(20))), - child: Row( - children: [ - //загрузка картинки по коду - Image.asset('assets/conditions/cond$conditionIcon.png'), - //колнка занимает все оставшееся место - Expanded( - child: Column( - children: [ - Text('Температура: $temp °C'), - Text('Влажность: $humidity %'), - Text('Скорость ветра: $windSpeed м/с'), - Text( - 'Прогноз актуален на: $forecastTime', - textAlign: TextAlign.center, - ) - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/presentation/widgets/current_weather_card.dart b/lib/presentation/widgets/current_weather_card.dart new file mode 100644 index 0000000..792321b --- /dev/null +++ b/lib/presentation/widgets/current_weather_card.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; +import 'package:friflex_weather_app/app/app_text_styles.dart'; +import 'package:friflex_weather_app/domain/entities/current_weather/current_weather_entity.dart'; + +import '../../app/app_utils.dart'; + +class CurrentWeatherCard extends StatelessWidget { + const CurrentWeatherCard({ + Key? key, + required this.weather, + }) : super(key: key); + + final CurrentWeatherEntity? weather; + + @override + Widget build(BuildContext context) { + //переменная с кодом состояния погоды + final String condition = weather?.weather?.first.icon ?? '_unknown'; + return Center( + //адаптивный к размерам экрана контейнер + child: FractionallySizedBox( + //процент по ширине + widthFactor: 0.8, + //процен по высоте + heightFactor: 0.6, + child: Container( + //добавление скруленной фиолетовой рамки + decoration: BoxDecoration( + border: Border.all(color: Colors.purple), + borderRadius: const BorderRadius.all(Radius.circular(20))), + child: Center( + child: Column( + //размещение объектов колонке с равным расстоянием между и половинрй + //расстояния сверху и снизу + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + //контейнер с закругленной рамкой и изображением + Container( + width: 100, + decoration: BoxDecoration( + border: Border.all(color: Colors.purple), + borderRadius: + const BorderRadius.all(Radius.circular(20)), + ), + //отображение картинки с состоянием погоды + child: + Image.asset('assets/conditions/cond$condition.png'), + ), + //текстовое описание состояния погоды + Text( + weather?.weather?.first.description ?? 'неизвестно', + style: AppTextStyle.parametersTextStyle, + ), + ], + ), + //отображение температуры + Text( + '${AppStrings.labelTemperature} ${weather?.main?.temp} °C', + style: AppTextStyle.parametersTextStyle, + ), + //отображение влажности + Text( + '${AppStrings.labelHumidity} ${weather?.main?.humidity} %', + style: AppTextStyle.parametersTextStyle, + ), + //отображение скорости ветра + Text( + '${AppStrings.labelWindSpeed} ${weather?.wind?.speed} ${AppStrings.labelWindUnits}', + style: AppTextStyle.parametersTextStyle, + ), + //отображение времени актуальности данных погоды + Text( + '${AppStrings.labelActualForecast} ${formatDateTime(weather?.dt)}', + style: AppTextStyle.parametersTextStyle, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/forecast_button.dart b/lib/presentation/widgets/forecast_button.dart new file mode 100644 index 0000000..80d28c4 --- /dev/null +++ b/lib/presentation/widgets/forecast_button.dart @@ -0,0 +1,38 @@ +//кнопка для открытия страницы с прогнозом +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:friflex_weather_app/app/app_const.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; +import 'package:friflex_weather_app/domain/bloc/internet_connection/connected_bloc.dart'; + +class ForecastButton extends StatelessWidget { + const ForecastButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + constraints: const BoxConstraints.expand(width: 80), + icon: const Text(AppStrings.labelForecastShort, + textAlign: TextAlign.center), + onPressed: () => navigateToForecast(context), + ); + } + + //проверка текущего соединения и демонстрауия уведомления + void navigateToForecast(BuildContext context) { + //проверка состояния интернет-соединения + if (context.read().state is ConnectedSuccessState) { + //переход на страницу с прогнозом погоды + Navigator.pushNamed(context, AppConst.forecastWeatherRoute); + } else { + //демонстрация уведомления в случае отсутствия интернета + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text(AppStrings.errorConnection), + ), + ); + } + } +} diff --git a/lib/presentation/widgets/forecast_card_item.dart b/lib/presentation/widgets/forecast_card_item.dart new file mode 100644 index 0000000..9be5f07 --- /dev/null +++ b/lib/presentation/widgets/forecast_card_item.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:friflex_weather_app/app/app_strings.dart'; + +class ForecastCardItem extends StatelessWidget { + const ForecastCardItem({ + Key? key, + required this.conditionIcon, + required this.temp, + required this.humidity, + required this.windSpeed, + required this.forecastTime, + }) : super(key: key); + + //иконка погоды + final String conditionIcon; + + //температура + final String temp; + + //влажность + final String humidity; + + //скорость ветра + final String windSpeed; + + //время прогноза + final String forecastTime; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.purple), + borderRadius: const BorderRadius.all(Radius.circular(20))), + child: Row( + children: [ + //загрузка картинки по коду + Image.asset('assets/conditions/cond$conditionIcon.png'), + //колнка занимает все оставшееся место + Expanded( + child: Column( + children: [ + Text('${AppStrings.labelTemperature} $temp °C'), + Text('${AppStrings.labelHumidity} $humidity %'), + Text( + '${AppStrings.labelWindSpeed} $windSpeed ${AppStrings.labelWindUnits}'), + Text( + '${AppStrings.labelActualForecast} $forecastTime', + textAlign: TextAlign.center, + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/forecast_card_list.dart b/lib/presentation/widgets/forecast_card_list.dart new file mode 100644 index 0000000..f1cb087 --- /dev/null +++ b/lib/presentation/widgets/forecast_card_list.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:friflex_weather_app/app/app_utils.dart'; +import 'package:friflex_weather_app/domain/entities/forecast_weather/forecast_part_entity.dart'; +import 'package:friflex_weather_app/presentation/widgets/forecast_card_item.dart'; + +class ForecastCardList extends StatelessWidget { + const ForecastCardList({Key? key, required this.forecastParts}) + : super(key: key); + final List? forecastParts; + + @override + Widget build(BuildContext context) { + return ListView.separated( + //отступы + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), + //общее количество + itemCount: forecastParts?.length ?? 0, + //разделение между элементами списка + separatorBuilder: (context, index) { + return const SizedBox( + height: 10, + ); + }, + itemBuilder: (context, index) { + //сохранение элемента списка + final item = forecastParts?.elementAt(index); + //построение карточки с прогнозом погоды + return ForecastCardItem( + //иконка погоды + conditionIcon: item?.weather?.first.icon ?? '_unknown', + //температура + temp: item?.main?.temp.toString() ?? '-', + //влажность + humidity: item?.main?.humidity.toString() ?? '-', + //скорость ветра + windSpeed: item?.wind?.speed.toString() ?? '-', + //время прогноза + forecastTime: formatDateTime(item?.dt), + ); + }); + } +} diff --git a/lib/presentation/widgets/weather_progress_indicator.dart b/lib/presentation/widgets/weather_progress_indicator.dart index 83437ad..c352127 100644 --- a/lib/presentation/widgets/weather_progress_indicator.dart +++ b/lib/presentation/widgets/weather_progress_indicator.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; //Виджет индикатора прогресса с цветом и позицией +//используя во всем приложении можно заменить на любой свой и не искать class WeatherProgressIndicator extends StatelessWidget { const WeatherProgressIndicator({ Key? key, diff --git a/pubspec.lock b/pubspec.lock index 1f5ba88..0c7c36c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -366,6 +366,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 59f81c4..70e916d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: shared_preferences: ^2.0.15 mocktail: ^0.3.0 bloc_test: ^9.0.3 + intl: ^0.17.0 dev_dependencies: flutter_test: diff --git a/test/presentation/city_input_screen_test.dart b/test/presentation/city_input_screen_test.dart index 5ebe420..0240b38 100644 --- a/test/presentation/city_input_screen_test.dart +++ b/test/presentation/city_input_screen_test.dart @@ -4,11 +4,7 @@ // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:friflex_weather_app/main.dart'; import 'package:friflex_weather_app/presentation/screens/city_input_page.dart'; import '../helpers/make_testable_widget.dart';