diff --git a/l10n/app_en.arb b/l10n/app_en.arb index d94cb06..2211e91 100644 --- a/l10n/app_en.arb +++ b/l10n/app_en.arb @@ -10,9 +10,12 @@ "addedToFavourite": "is added to favourites", "removedFromFavourite": "is removed from favourites", + "cardsLoadingFailed": "Server is unreachable", + "cardsLoadingFailedSnackBar": "Failed to load crypto data", "coinDataPriceChange": "for the last 24 hours", "settingsLanguage": "Language", + "settingsCurrency": "Currency", "navigationHome": "Home", "navigationFavourites": "Favourites", diff --git a/l10n/app_ru.arb b/l10n/app_ru.arb index a5fb29c..c45baf6 100644 --- a/l10n/app_ru.arb +++ b/l10n/app_ru.arb @@ -10,9 +10,12 @@ "addedToFavourite": "добавлен в избранное", "removedFromFavourite": "удален из избранного", + "cardsLoadingFailed": "Сервер недоступен", + "cardsLoadingFailedSnackBar": "Не удалось загрузить данные о криптовалюте", "coinDataPriceChange": "за последние 24 часа", "settingsLanguage": "Язык", + "settingsCurrency": "Валюта", "navigationHome": "Главная", "navigationFavourites": "Избранное", diff --git a/lib/components/locale/l10n/app_localizations.dart b/lib/components/locale/l10n/app_localizations.dart index 68a848d..51bccee 100644 --- a/lib/components/locale/l10n/app_localizations.dart +++ b/lib/components/locale/l10n/app_localizations.dart @@ -137,6 +137,18 @@ abstract class AppLocale { /// **'is removed from favourites'** String get removedFromFavourite; + /// No description provided for @cardsLoadingFailed. + /// + /// In en, this message translates to: + /// **'Server is unreachable'** + String get cardsLoadingFailed; + + /// No description provided for @cardsLoadingFailedSnackBar. + /// + /// In en, this message translates to: + /// **'Failed to load crypto data'** + String get cardsLoadingFailedSnackBar; + /// No description provided for @coinDataPriceChange. /// /// In en, this message translates to: @@ -149,6 +161,12 @@ abstract class AppLocale { /// **'Language'** String get settingsLanguage; + /// No description provided for @settingsCurrency. + /// + /// In en, this message translates to: + /// **'Currency'** + String get settingsCurrency; + /// No description provided for @navigationHome. /// /// In en, this message translates to: diff --git a/lib/components/locale/l10n/app_localizations_en.dart b/lib/components/locale/l10n/app_localizations_en.dart index 988620b..06aad57 100644 --- a/lib/components/locale/l10n/app_localizations_en.dart +++ b/lib/components/locale/l10n/app_localizations_en.dart @@ -27,12 +27,21 @@ class AppLocaleEn extends AppLocale { @override String get removedFromFavourite => 'is removed from favourites'; + @override + String get cardsLoadingFailed => 'Server is unreachable'; + + @override + String get cardsLoadingFailedSnackBar => 'Failed to load crypto data'; + @override String get coinDataPriceChange => 'for the last 24 hours'; @override String get settingsLanguage => 'Language'; + @override + String get settingsCurrency => 'Currency'; + @override String get navigationHome => 'Home'; diff --git a/lib/components/locale/l10n/app_localizations_ru.dart b/lib/components/locale/l10n/app_localizations_ru.dart index bf2878d..27b0e34 100644 --- a/lib/components/locale/l10n/app_localizations_ru.dart +++ b/lib/components/locale/l10n/app_localizations_ru.dart @@ -27,12 +27,21 @@ class AppLocaleRu extends AppLocale { @override String get removedFromFavourite => 'удален из избранного'; + @override + String get cardsLoadingFailed => 'Сервер недоступен'; + + @override + String get cardsLoadingFailedSnackBar => 'Не удалось загрузить данные о криптовалюте'; + @override String get coinDataPriceChange => 'за последние 24 часа'; @override String get settingsLanguage => 'Язык'; + @override + String get settingsCurrency => 'Валюта'; + @override String get navigationHome => 'Главная'; diff --git a/lib/data/mappers/crypto_mapper.dart b/lib/data/mappers/crypto_mapper.dart index 349ffcf..449809b 100644 --- a/lib/data/mappers/crypto_mapper.dart +++ b/lib/data/mappers/crypto_mapper.dart @@ -4,28 +4,26 @@ import 'package:flutter_android_app/domain/models/card.dart'; import 'package:flutter_android_app/domain/models/home.dart'; extension CoinDataDtoToModel on CoinDataDto { - CardData toDomain(AppLocale? locale) => CardData( + CardData toDomain(AppLocale? locale, String currencyId) => CardData( id: id ?? 'UNKNOWN', title: name ?? 'UNKNOWN', imageUrl: image, - currentPrice: _getLocalizedPrice(currentPrice, locale?.localeName), - priceChange: _getLocalizedPriceChange(priceChange24h, locale), + currentPrice: '$currentPrice ${_getCurrencySymbol(currencyId)}', + priceChange: _getLocalizedPriceChange(priceChange24h, locale, currencyId), ); - String _getLocalizedPrice(double? price, String? localeName) { - if (localeName == null) { - return '$price \$'; - } + String _getCurrencySymbol(String currencyId) { + return switch (currencyId) { + 'rub' => '₽', + 'usd' => '\$', - return switch (localeName) { - 'ru' => '$price ₽', - _ => '$price \$', + _ => '?', }; } - String _getLocalizedPriceChange(double? priceChange, AppLocale? locale) { + String _getLocalizedPriceChange(double? priceChange, AppLocale? locale, String currencyId) { if (priceChange == null) { - return '+${_getLocalizedPrice(0, locale?.localeName)}'; + return '+0 ${_getCurrencySymbol(currencyId)}'; } String retVal = ''; @@ -33,15 +31,15 @@ extension CoinDataDtoToModel on CoinDataDto { retVal += '+'; } - retVal += _getLocalizedPrice(priceChange, locale?.localeName); + retVal += '$priceChange ${_getCurrencySymbol(currencyId)}'; return '$retVal ${locale?.coinDataPriceChange}'; } } extension CoinsDtoToModel on CoinsDto { - HomeData toDomain(AppLocale? locale, int currentPage) => HomeData( - data: coins?.map((e) => e.toDomain(locale)).toList(), + HomeData toDomain(AppLocale? locale, String currencyId, int currentPage) => HomeData( + data: coins?.map((e) => e.toDomain(locale, currencyId)).toList(), nextPage: currentPage + 1, ); } diff --git a/lib/main.dart b/lib/main.dart index 91acad7..828f005 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_bloc.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_bloc.dart'; import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart'; import 'package:flutter_android_app/presentation/home_page/home_page.dart'; @@ -30,33 +31,37 @@ class _MyAppState extends State { return BlocProvider( lazy: false, create: (context) => FavouritesBloc(), - child: BlocProvider( + child: BlocProvider( lazy: false, - create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))), - child: BlocBuilder( - builder: (context, state) => MaterialApp( - title: 'Cryptocurrency Exchange App', - locale: state.currentLocale, - localizationsDelegates: AppLocale.localizationsDelegates, - supportedLocales: AppLocale.supportedLocales, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.indigoAccent, + create: (context) => CurrencyBloc(), + child: BlocProvider( + lazy: false, + create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))), + child: BlocBuilder( + builder: (context, state) => MaterialApp( + title: 'Cryptocurrency Exchange App', + locale: state.currentLocale, + localizationsDelegates: AppLocale.localizationsDelegates, + supportedLocales: AppLocale.supportedLocales, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigoAccent, + brightness: isDarkMode ? Brightness.dark : Brightness.light, + ), + useMaterial3: true, brightness: isDarkMode ? Brightness.dark : Brightness.light, ), - useMaterial3: true, - brightness: isDarkMode ? Brightness.dark : Brightness.light, - ), - debugShowCheckedModeBanner: false, - home: RepositoryProvider( - lazy: true, - create: (_) => CryptoRepository(), - child: BlocProvider( - lazy: false, - create: (context) => HomeBloc(context.read()), - child: MainScaffold( - toggleDarkMode: _toggleDarkMode, - isDarkModeSelected: isDarkMode, + debugShowCheckedModeBanner: false, + home: RepositoryProvider( + lazy: true, + create: (_) => CryptoRepository(), + child: BlocProvider( + lazy: false, + create: (context) => HomeBloc(context.read()), + child: MainScaffold( + toggleDarkMode: _toggleDarkMode, + isDarkModeSelected: isDarkMode, + ), ), ), ), diff --git a/lib/presentation/currency_bloc/currency_bloc.dart b/lib/presentation/currency_bloc/currency_bloc.dart new file mode 100644 index 0000000..7508cd0 --- /dev/null +++ b/lib/presentation/currency_bloc/currency_bloc.dart @@ -0,0 +1,61 @@ +import 'package:flutter_android_app/presentation/currency_bloc/currency_events.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class CurrencyBloc extends Bloc { + static const String _currencyPrefsKey = 'local_currency'; + static const List _availableCurrencyIds = ['usd', 'rub']; + static final String _defaultCurrencyId = _availableCurrencyIds[0]; + + CurrencyBloc() : super(const CurrencyState()) { + on(_onLoadLocalCurrency); + on(_onToggleLocalCurrency); + } + + Future _onLoadLocalCurrency( + LoadLocalCurrencyEvent event, Emitter emit + ) async { + emit(state.copyWith( + hasCurrencyLoaded: false, + )); + + final prefs = await SharedPreferences.getInstance(); + final data = prefs.getString(_currencyPrefsKey); + + String currencyId = ''; + if (data != null) { + currencyId = data; + } else { + currencyId = _defaultCurrencyId; + } + + emit(state.copyWith( + currencyId: currencyId, + hasCurrencyLoaded: true, + )); + } + + Future _onToggleLocalCurrency( + ToggleLocalCurrencyEvent event, Emitter emit + ) async { + if (state.currencyId == null) { + return; + } + + final int oldCurrencyIdIdx = _availableCurrencyIds.indexOf(state.currencyId!); + int newCurrencyIdIdx = oldCurrencyIdIdx + 1; + if (newCurrencyIdIdx >= _availableCurrencyIds.length){ + newCurrencyIdIdx = 0; + } + + final newCurrencyId = _availableCurrencyIds[newCurrencyIdIdx]; + + final prefs = await SharedPreferences.getInstance(); + prefs.setString(_currencyPrefsKey, newCurrencyId); + + emit(state.copyWith( + currencyId: newCurrencyId, + )); + } +} diff --git a/lib/presentation/currency_bloc/currency_events.dart b/lib/presentation/currency_bloc/currency_events.dart new file mode 100644 index 0000000..3289411 --- /dev/null +++ b/lib/presentation/currency_bloc/currency_events.dart @@ -0,0 +1,11 @@ +abstract class CurrencyEvent { + const CurrencyEvent(); +} + +class LoadLocalCurrencyEvent extends CurrencyEvent { + const LoadLocalCurrencyEvent(); +} + +class ToggleLocalCurrencyEvent extends CurrencyEvent { + const ToggleLocalCurrencyEvent(); +} diff --git a/lib/presentation/currency_bloc/currency_state.dart b/lib/presentation/currency_bloc/currency_state.dart new file mode 100644 index 0000000..bcfab52 --- /dev/null +++ b/lib/presentation/currency_bloc/currency_state.dart @@ -0,0 +1,21 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:equatable/equatable.dart'; + +part 'currency_state.g.dart'; + +@CopyWith() +class CurrencyState extends Equatable { + final String? currencyId; + final bool hasCurrencyLoaded; + + const CurrencyState({ + this.currencyId, + this.hasCurrencyLoaded = false + }); + + @override + List get props => [ + currencyId, + hasCurrencyLoaded, + ]; +} diff --git a/lib/presentation/currency_bloc/currency_state.g.dart b/lib/presentation/currency_bloc/currency_state.g.dart new file mode 100644 index 0000000..6f80207 --- /dev/null +++ b/lib/presentation/currency_bloc/currency_state.g.dart @@ -0,0 +1,69 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'currency_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$CurrencyStateCWProxy { + CurrencyState currencyId(String? currencyId); + + CurrencyState hasCurrencyLoaded(bool hasCurrencyLoaded); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CurrencyState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// CurrencyState(...).copyWith(id: 12, name: "My name") + /// ```` + CurrencyState call({ + String? currencyId, + bool? hasCurrencyLoaded, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCurrencyState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfCurrencyState.copyWith.fieldName(...)` +class _$CurrencyStateCWProxyImpl implements _$CurrencyStateCWProxy { + const _$CurrencyStateCWProxyImpl(this._value); + + final CurrencyState _value; + + @override + CurrencyState currencyId(String? currencyId) => this(currencyId: currencyId); + + @override + CurrencyState hasCurrencyLoaded(bool hasCurrencyLoaded) => + this(hasCurrencyLoaded: hasCurrencyLoaded); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CurrencyState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// CurrencyState(...).copyWith(id: 12, name: "My name") + /// ```` + CurrencyState call({ + Object? currencyId = const $CopyWithPlaceholder(), + Object? hasCurrencyLoaded = const $CopyWithPlaceholder(), + }) { + return CurrencyState( + currencyId: currencyId == const $CopyWithPlaceholder() + ? _value.currencyId + // ignore: cast_nullable_to_non_nullable + : currencyId as String?, + hasCurrencyLoaded: hasCurrencyLoaded == const $CopyWithPlaceholder() || + hasCurrencyLoaded == null + ? _value.hasCurrencyLoaded + // ignore: cast_nullable_to_non_nullable + : hasCurrencyLoaded as bool, + ); + } +} + +extension $CurrencyStateCopyWith on CurrencyState { + /// Returns a callable class that can be used as follows: `instanceOfCurrencyState.copyWith(...)` or like so:`instanceOfCurrencyState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$CurrencyStateCWProxy get copyWith => _$CurrencyStateCWProxyImpl(this); +} diff --git a/lib/presentation/favourites_bloc/favourites_bloc.dart b/lib/presentation/favourites_bloc/favourites_bloc.dart index aa85601..7e43ab7 100644 --- a/lib/presentation/favourites_bloc/favourites_bloc.dart +++ b/lib/presentation/favourites_bloc/favourites_bloc.dart @@ -22,7 +22,7 @@ class FavouritesBloc extends Bloc { final data = prefs.getStringList(_likedPrefsKey); emit(state.copyWith( - likedIds: data, + favouritesIds: data, hasFavouritesLoaded: true, )); } @@ -42,7 +42,7 @@ class FavouritesBloc extends Bloc { prefs.setStringList(_likedPrefsKey, updatedList); emit(state.copyWith( - likedIds: updatedList, + favouritesIds: updatedList, hasFavouritesLoaded: true, )); } diff --git a/lib/presentation/favourites_bloc/favourites_state.g.dart b/lib/presentation/favourites_bloc/favourites_state.g.dart index 20969f4..751ccc3 100644 --- a/lib/presentation/favourites_bloc/favourites_state.g.dart +++ b/lib/presentation/favourites_bloc/favourites_state.g.dart @@ -7,7 +7,7 @@ part of 'favourites_state.dart'; // ************************************************************************** abstract class _$FavouritesStateCWProxy { - FavouritesState likedIds(List? likedIds); + FavouritesState favouritesIds(List? favouritesIds); FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded); @@ -18,7 +18,7 @@ abstract class _$FavouritesStateCWProxy { /// FavouritesState(...).copyWith(id: 12, name: "My name") /// ```` FavouritesState call({ - List? likedIds, + List? favouritesIds, bool? hasFavouritesLoaded, }); } @@ -30,7 +30,8 @@ class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy { final FavouritesState _value; @override - FavouritesState likedIds(List? likedIds) => this(likedIds: likedIds); + FavouritesState favouritesIds(List? favouritesIds) => + this(favouritesIds: favouritesIds); @override FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded) => @@ -45,14 +46,14 @@ class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy { /// FavouritesState(...).copyWith(id: 12, name: "My name") /// ```` FavouritesState call({ - Object? likedIds = const $CopyWithPlaceholder(), + Object? favouritesIds = const $CopyWithPlaceholder(), Object? hasFavouritesLoaded = const $CopyWithPlaceholder(), }) { return FavouritesState( - favouritesIds: likedIds == const $CopyWithPlaceholder() + favouritesIds: favouritesIds == const $CopyWithPlaceholder() ? _value.favouritesIds // ignore: cast_nullable_to_non_nullable - : likedIds as List?, + : favouritesIds as List?, hasFavouritesLoaded: hasFavouritesLoaded == const $CopyWithPlaceholder() || hasFavouritesLoaded == null diff --git a/lib/presentation/favourites_page/favourites_page.dart b/lib/presentation/favourites_page/favourites_page.dart index 1972d93..c89fd34 100644 --- a/lib/presentation/favourites_page/favourites_page.dart +++ b/lib/presentation/favourites_page/favourites_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_android_app/components/extensions/context_x.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_bloc.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_events.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_state.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_bloc.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_events.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_state.dart'; @@ -21,9 +24,12 @@ class FavouritesPage extends StatefulWidget { class _FavouritesPageState extends State { final ScrollController scrollController = ScrollController(); + List? favouritesIds; bool wereIdsPreviouslyLoaded = false; + String? currencyId; + @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -47,41 +53,46 @@ class _FavouritesPageState extends State { if (state.hasFavouritesLoaded && !wereIdsPreviouslyLoaded) { wereIdsPreviouslyLoaded = true; favouritesIds = state.favouritesIds; - context.read().add(HomeLoadFavouritesDataEvent( - ids: favouritesIds, - locale: context.locale, - )); + context.read().add(const LoadLocalCurrencyEvent()); } }, - child: Padding( - padding: EdgeInsets.only(top: 12), - child: Column( - children: [ - CardsList( - onListRefresh: _onRefresh, - onCardLiked: _onLike, - onCardTapped: _navToDetails, - onNextPage: _onNextPage, - ), - BlocBuilder( - builder: (context, state) => state.isPaginationLoading - ? const Padding( - padding: EdgeInsets.all(12), - child: CircularProgressIndicator(), - ) - : const SizedBox.shrink(), - ), - ], + child: BlocListener( + listener: (context, state) { + if (state.hasCurrencyLoaded && state.currencyId != null) { + currencyId = state.currencyId; + context.read().add(HomeLoadFavouritesDataEvent( + ids: favouritesIds, + locale: context.locale, + currencyId: currencyId!, + )); + } + }, + child: Padding( + padding: const EdgeInsets.only(top: 12), + child: Column( + children: [ + CardsList( + onListRefresh: _onRefresh, + onCardLiked: _onLike, + onCardTapped: _navToDetails, + onNextPage: _onNextPage, + ), + BlocBuilder( + builder: (context, state) => state.isPaginationLoading + ? const Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(), + ) + : const SizedBox.shrink(), + ), + ], + ), ), ), ); } Future _onRefresh() { - //context.read().add(HomeLoadFavouritesDataEvent( - // ids: favouritesIds, - // locale: context.locale, - //)); wereIdsPreviouslyLoaded = false; context.read().add(const LoadFavouritesEvent()); return Future.value(null); @@ -120,6 +131,7 @@ class _FavouritesPageState extends State { ids: favouritesIds, nextPage: bloc.state.data?.nextPage, locale: context.locale, + currencyId: currencyId!, )); } } diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart index 9590fef..a17fef8 100644 --- a/lib/presentation/home_page/bloc/bloc.dart +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -15,7 +15,10 @@ class HomeBloc extends Bloc { const int pageSize = 20; if (event.nextPage == null) { - emit(state.copyWith(isLoading: true)); + emit(state.copyWith( + isLoading: true, + error: 'NO_ERROR', + )); } else { emit(state.copyWith(isPaginationLoading: true)); } @@ -23,11 +26,12 @@ class HomeBloc extends Bloc { String? error; final data = await repo.loadData( - search: event.search, - page: event.nextPage ?? 1, - pageSize: pageSize, - onError: (e) => error = e, - locale: event.locale, + search: event.search, + page: event.nextPage ?? 1, + pageSize: pageSize, + onError: (e) => error = e, + locale: event.locale, + currencyId: event.currencyId, ); bool isLastPage = false; @@ -52,7 +56,10 @@ class HomeBloc extends Bloc { const int pageSize = 10; if (event.nextPage == null) { - emit(state.copyWith(isLoading: true)); + emit(state.copyWith( + isLoading: true, + error: 'NO_ERROR', + )); } else { emit(state.copyWith(isPaginationLoading: true)); } @@ -65,6 +72,7 @@ class HomeBloc extends Bloc { pageSize: pageSize, onError: (e) => error = e, locale: event.locale, + currencyId: event.currencyId, ); bool isLastPage = false; diff --git a/lib/presentation/home_page/bloc/events.dart b/lib/presentation/home_page/bloc/events.dart index 53d7b0b..c020fdf 100644 --- a/lib/presentation/home_page/bloc/events.dart +++ b/lib/presentation/home_page/bloc/events.dart @@ -8,14 +8,26 @@ class HomeLoadDataEvent extends HomeEvent { final String? search; final int? nextPage; final AppLocale? locale; + final String currencyId; - const HomeLoadDataEvent({this.search, this.nextPage, this.locale}); + const HomeLoadDataEvent({ + this.search, + this.nextPage, + this.locale, + required this.currencyId, + }); } class HomeLoadFavouritesDataEvent extends HomeEvent { final List? ids; final int? nextPage; final AppLocale? locale; + final String currencyId; - const HomeLoadFavouritesDataEvent({this.ids, this.nextPage, this.locale}); + const HomeLoadFavouritesDataEvent({ + this.ids, + this.nextPage, + this.locale, + required this.currencyId, + }); } diff --git a/lib/presentation/home_page/cards_list.dart b/lib/presentation/home_page/cards_list.dart index be54e1f..0038235 100644 --- a/lib/presentation/home_page/cards_list.dart +++ b/lib/presentation/home_page/cards_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_android_app/components/extensions/context_x.dart'; import 'package:flutter_android_app/domain/models/card.dart'; import 'package:flutter_android_app/presentation/home_page/card_crypto.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -45,40 +46,63 @@ class _CardsListState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) => state.error != null - ? Text( - state.error ?? '', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red), - ) - : state.isLoading - ? const Center(child: Padding( - padding: EdgeInsets.only(top: 20), - child: CircularProgressIndicator(), - )) - : BlocBuilder( - builder: (context, likeState) => Expanded( - child: RefreshIndicator( - onRefresh: widget.onListRefresh, - child: ListView.builder( - controller: scrollController, - padding: EdgeInsets.zero, - itemCount: state.data?.data?.length ?? 0, - itemBuilder: (context, index) { - final data = state.data?.data?[index]; - return data != null - ? CardCrypto.fromData( - data, - isLiked: likeState.favouritesIds?.contains(data.id) == true, - onLike: widget.onCardLiked, - onTap: () => widget.onCardTapped?.call(data), - ) - : const SizedBox.shrink(); - }, + return BlocListener( + listener: (context, state) { + if (state.error != null && state.error != 'NO_ERROR') { + WidgetsBinding.instance.addPostFrameCallback((_) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + context.locale.cardsLoadingFailedSnackBar, + style: Theme.of(context).textTheme.bodyLarge, + ), + backgroundColor: Theme.of(context).colorScheme.errorContainer, + duration: const Duration(seconds: 2), + )); + }); + } + }, + child: BlocBuilder( + builder: (context, state) => state.error != null && state.error != 'NO_ERROR' + ? Center( + child: Padding( + padding: const EdgeInsets.all(20), + child: Text( + context.locale.cardsLoadingFailed, + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Theme.of(context).disabledColor), + ), + ), + ) + : state.isLoading + ? const Center( + child: Padding( + padding: EdgeInsets.only(top: 20), + child: CircularProgressIndicator(), + ) + ) + : BlocBuilder( + builder: (context, likeState) => Expanded( + child: RefreshIndicator( + onRefresh: widget.onListRefresh, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.zero, + itemCount: state.data?.data?.length ?? 0, + itemBuilder: (context, index) { + final data = state.data?.data?[index]; + return data != null + ? CardCrypto.fromData( + data, + isLiked: likeState.favouritesIds?.contains(data.id) == true, + onLike: widget.onCardLiked, + onTap: () => widget.onCardTapped?.call(data), + ) + : const SizedBox.shrink(); + }, + ), ), ), - ), - ) + ) + ), ); } diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index fecdcff..21e8b62 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -11,6 +11,9 @@ import 'package:flutter_android_app/presentation/home_page/cards_list.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../common/svg_objects.dart'; +import '../currency_bloc/currency_bloc.dart'; +import '../currency_bloc/currency_events.dart'; +import '../currency_bloc/currency_state.dart'; import '../favourites_bloc/favourites_bloc.dart'; import '../favourites_bloc/favourites_events.dart'; import '../settings_page/settings_page.dart'; @@ -83,13 +86,14 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final TextEditingController searchController = TextEditingController(); + late String? currencyId; @override void initState() { SvgObjects.init(); WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().add(HomeLoadDataEvent(locale: context.locale)); + context.read().add(const LoadLocalCurrencyEvent()); context.read().add(const LoadFavouritesEvent()); }); @@ -105,51 +109,70 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: SearchBar( - controller: searchController, - onChanged: (search) { - Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search, locale: context.locale))); - }, - leading: const Icon(Icons.search), - trailing: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - if (searchController.text.isNotEmpty) { - searchController.clear(); - context.read().add(HomeLoadDataEvent(locale: context.locale)); - } - }, - ), - ], - hintText: context.locale.searchHint, - elevation: const WidgetStatePropertyAll(0.0), - padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)), - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer), + return BlocListener( + listener: (context, state) { + if (state.hasCurrencyLoaded && state.currencyId != null) { + currencyId = state.currencyId; + context.read().add(HomeLoadDataEvent( + locale: context.locale, + currencyId: currencyId!, + )); + } + }, + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: SearchBar( + controller: searchController, + onChanged: (search) { + Debounce.run(() => context.read().add(HomeLoadDataEvent( + search: search, + locale: context.locale, + currencyId: currencyId!, + ))); + }, + leading: const Icon(Icons.search), + trailing: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + if (searchController.text.isNotEmpty) { + FocusManager.instance.primaryFocus?.unfocus(); + searchController.clear(); + context.read().add(HomeLoadDataEvent( + locale: context.locale, + currencyId: currencyId!, + )); + } + }, + ), + ], + hintText: context.locale.searchHint, + elevation: const WidgetStatePropertyAll(0.0), + padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)), + backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer), + ), ), - ), - CardsList( - onListRefresh: _onRefresh, - onCardLiked: _onLike, - onCardTapped: _navToDetails, - onNextPage: _onNextPage, - ), - BlocBuilder( - builder: (context, state) => state.isPaginationLoading - ? const Padding( - padding: EdgeInsets.all(12), - child: CircularProgressIndicator(), - ) - : const SizedBox.shrink(), - ), - ], - ) + CardsList( + onListRefresh: _onRefresh, + onCardLiked: _onLike, + onCardTapped: _navToDetails, + onNextPage: _onNextPage, + ), + BlocBuilder( + builder: (context, state) => state.isPaginationLoading + ? const Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(), + ) + : const SizedBox.shrink(), + ), + ], + ) + ), ); } @@ -167,7 +190,11 @@ class _HomePageState extends State { } Future _onRefresh() { - context.read().add(HomeLoadDataEvent(search: searchController.text, locale: context.locale)); + context.read().add(HomeLoadDataEvent( + search: searchController.text, + locale: context.locale, + currencyId: currencyId!, + )); return Future.value(null); } @@ -179,6 +206,7 @@ class _HomePageState extends State { } void _navToDetails(CardData data) { + FocusManager.instance.primaryFocus?.unfocus(); Navigator.push(context, MaterialPageRoute( builder: (context) => DetailsPage(data)), ); @@ -191,6 +219,7 @@ class _HomePageState extends State { search: searchController.text, nextPage: bloc.state.data?.nextPage, locale: context.locale, + currencyId: currencyId!, )); } } diff --git a/lib/presentation/settings_page/settings_page.dart b/lib/presentation/settings_page/settings_page.dart index ad68d6c..51ce901 100644 --- a/lib/presentation/settings_page/settings_page.dart +++ b/lib/presentation/settings_page/settings_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_android_app/components/extensions/context_x.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_bloc.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_events.dart'; +import 'package:flutter_android_app/presentation/currency_bloc/currency_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../common/svg_objects.dart'; @@ -40,6 +43,34 @@ class SettingsPage extends StatelessWidget { ), ], ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${context.locale.settingsCurrency}:', + style: Theme.of(context).textTheme.titleMedium, + ), + BlocBuilder( + builder: (context, currencyState) => GestureDetector( + onTap: () => context.read().add(const ToggleLocalCurrencyEvent()), + child: SizedBox.square( + dimension: 50, + child: Padding( + padding: const EdgeInsets.only(right: 0), + child: BlocBuilder( + builder: (context, state) { + return switch (currencyState.currencyId) { + 'rub' => const SvgRu(), + 'usd' => const SvgUs(), + _ => const SvgUs(), + }; + }), + ), + ), + ), + ), + ], + ), ], ), ); diff --git a/lib/repositories/api_interface.dart b/lib/repositories/api_interface.dart index 76c4fb2..46df20c 100644 --- a/lib/repositories/api_interface.dart +++ b/lib/repositories/api_interface.dart @@ -10,6 +10,7 @@ abstract class ApiInterface { int page = 1, int pageSize = 20, AppLocale? locale, + String currencyId, }); Future loadDataWithIds({ @@ -18,5 +19,6 @@ abstract class ApiInterface { int page = 1, int pageSize = 8, AppLocale? locale, + String currencyId, }); } diff --git a/lib/repositories/crypto_repository.dart b/lib/repositories/crypto_repository.dart index 8ebe2df..4857392 100644 --- a/lib/repositories/crypto_repository.dart +++ b/lib/repositories/crypto_repository.dart @@ -29,11 +29,12 @@ class CryptoRepository extends ApiInterface { int page = 1, int pageSize = 20, AppLocale? locale, + String currencyId = '', }) async { try { Map queryParams = { 'x_cg_demo_api_key': _apiKey, - 'vs_currency': _getCurrencyName(locale?.localeName), + 'vs_currency': currencyId, 'per_page': pageSize, 'page': page, }; @@ -68,7 +69,7 @@ class CryptoRepository extends ApiInterface { ); final CoinsDto dto = CoinsDto.fromJson(response.data as List); - final HomeData data = dto.toDomain(locale, page); + final HomeData data = dto.toDomain(locale, currencyId, page); return data; } on DioException catch (e) { @@ -84,11 +85,12 @@ class CryptoRepository extends ApiInterface { int page = 1, int pageSize = 20, AppLocale? locale, + String currencyId = '', }) async { try { Map queryParams = { 'x_cg_demo_api_key': _apiKey, - 'vs_currency': _getCurrencyName(locale?.localeName), + 'vs_currency': currencyId, 'per_page': pageSize, 'page': page, }; @@ -108,7 +110,7 @@ class CryptoRepository extends ApiInterface { ); final CoinsDto dto = CoinsDto.fromJson(response.data as List); - final HomeData data = dto.toDomain(locale, page); + final HomeData data = dto.toDomain(locale, currencyId, page); return data; } on DioException catch (e) { @@ -116,15 +118,4 @@ class CryptoRepository extends ApiInterface { return null; } } - - String _getCurrencyName(String? localeName) { - if (localeName == null) { - return 'usd'; - } - - return switch (localeName) { - 'ru' => 'rub', - _ => 'usd', - }; - } } diff --git a/lib/repositories/mock_repository.dart b/lib/repositories/mock_repository.dart index 55b8e1b..d5b9fef 100644 --- a/lib/repositories/mock_repository.dart +++ b/lib/repositories/mock_repository.dart @@ -13,6 +13,7 @@ class MockRepository extends ApiInterface { int page = 1, int pageSize = 20, AppLocale? locale, + String currencyId = '', }) async { return HomeData(data: [ CardData( @@ -46,6 +47,7 @@ class MockRepository extends ApiInterface { int page = 1, int pageSize = 20, AppLocale? locale, + String currencyId = '', }) async { return HomeData(data: [ CardData(