added separate setting to change currency

This commit is contained in:
ShabOl 2024-12-30 11:57:56 +04:00
parent f0a17af845
commit 1377a5dbdd
22 changed files with 495 additions and 176 deletions

View File

@ -10,9 +10,12 @@
"addedToFavourite": "is added to favourites", "addedToFavourite": "is added to favourites",
"removedFromFavourite": "is removed from favourites", "removedFromFavourite": "is removed from favourites",
"cardsLoadingFailed": "Server is unreachable",
"cardsLoadingFailedSnackBar": "Failed to load crypto data",
"coinDataPriceChange": "for the last 24 hours", "coinDataPriceChange": "for the last 24 hours",
"settingsLanguage": "Language", "settingsLanguage": "Language",
"settingsCurrency": "Currency",
"navigationHome": "Home", "navigationHome": "Home",
"navigationFavourites": "Favourites", "navigationFavourites": "Favourites",

View File

@ -10,9 +10,12 @@
"addedToFavourite": "добавлен в избранное", "addedToFavourite": "добавлен в избранное",
"removedFromFavourite": "удален из избранного", "removedFromFavourite": "удален из избранного",
"cardsLoadingFailed": "Сервер недоступен",
"cardsLoadingFailedSnackBar": "Не удалось загрузить данные о криптовалюте",
"coinDataPriceChange": "за последние 24 часа", "coinDataPriceChange": "за последние 24 часа",
"settingsLanguage": "Язык", "settingsLanguage": "Язык",
"settingsCurrency": "Валюта",
"navigationHome": "Главная", "navigationHome": "Главная",
"navigationFavourites": "Избранное", "navigationFavourites": "Избранное",

View File

@ -137,6 +137,18 @@ abstract class AppLocale {
/// **'is removed from favourites'** /// **'is removed from favourites'**
String get removedFromFavourite; 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. /// No description provided for @coinDataPriceChange.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -149,6 +161,12 @@ abstract class AppLocale {
/// **'Language'** /// **'Language'**
String get settingsLanguage; String get settingsLanguage;
/// No description provided for @settingsCurrency.
///
/// In en, this message translates to:
/// **'Currency'**
String get settingsCurrency;
/// No description provided for @navigationHome. /// No description provided for @navigationHome.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@ -27,12 +27,21 @@ class AppLocaleEn extends AppLocale {
@override @override
String get removedFromFavourite => 'is removed from favourites'; String get removedFromFavourite => 'is removed from favourites';
@override
String get cardsLoadingFailed => 'Server is unreachable';
@override
String get cardsLoadingFailedSnackBar => 'Failed to load crypto data';
@override @override
String get coinDataPriceChange => 'for the last 24 hours'; String get coinDataPriceChange => 'for the last 24 hours';
@override @override
String get settingsLanguage => 'Language'; String get settingsLanguage => 'Language';
@override
String get settingsCurrency => 'Currency';
@override @override
String get navigationHome => 'Home'; String get navigationHome => 'Home';

View File

@ -27,12 +27,21 @@ class AppLocaleRu extends AppLocale {
@override @override
String get removedFromFavourite => 'удален из избранного'; String get removedFromFavourite => 'удален из избранного';
@override
String get cardsLoadingFailed => 'Сервер недоступен';
@override
String get cardsLoadingFailedSnackBar => 'Не удалось загрузить данные о криптовалюте';
@override @override
String get coinDataPriceChange => 'за последние 24 часа'; String get coinDataPriceChange => 'за последние 24 часа';
@override @override
String get settingsLanguage => 'Язык'; String get settingsLanguage => 'Язык';
@override
String get settingsCurrency => 'Валюта';
@override @override
String get navigationHome => 'Главная'; String get navigationHome => 'Главная';

View File

@ -4,28 +4,26 @@ import 'package:flutter_android_app/domain/models/card.dart';
import 'package:flutter_android_app/domain/models/home.dart'; import 'package:flutter_android_app/domain/models/home.dart';
extension CoinDataDtoToModel on CoinDataDto { extension CoinDataDtoToModel on CoinDataDto {
CardData toDomain(AppLocale? locale) => CardData( CardData toDomain(AppLocale? locale, String currencyId) => CardData(
id: id ?? 'UNKNOWN', id: id ?? 'UNKNOWN',
title: name ?? 'UNKNOWN', title: name ?? 'UNKNOWN',
imageUrl: image, imageUrl: image,
currentPrice: _getLocalizedPrice(currentPrice, locale?.localeName), currentPrice: '$currentPrice ${_getCurrencySymbol(currencyId)}',
priceChange: _getLocalizedPriceChange(priceChange24h, locale), priceChange: _getLocalizedPriceChange(priceChange24h, locale, currencyId),
); );
String _getLocalizedPrice(double? price, String? localeName) { String _getCurrencySymbol(String currencyId) {
if (localeName == null) { return switch (currencyId) {
return '$price \$'; '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) { if (priceChange == null) {
return '+${_getLocalizedPrice(0, locale?.localeName)}'; return '+0 ${_getCurrencySymbol(currencyId)}';
} }
String retVal = ''; String retVal = '';
@ -33,15 +31,15 @@ extension CoinDataDtoToModel on CoinDataDto {
retVal += '+'; retVal += '+';
} }
retVal += _getLocalizedPrice(priceChange, locale?.localeName); retVal += '$priceChange ${_getCurrencySymbol(currencyId)}';
return '$retVal ${locale?.coinDataPriceChange}'; return '$retVal ${locale?.coinDataPriceChange}';
} }
} }
extension CoinsDtoToModel on CoinsDto { extension CoinsDtoToModel on CoinsDto {
HomeData toDomain(AppLocale? locale, int currentPage) => HomeData( HomeData toDomain(AppLocale? locale, String currencyId, int currentPage) => HomeData(
data: coins?.map((e) => e.toDomain(locale)).toList(), data: coins?.map((e) => e.toDomain(locale, currencyId)).toList(),
nextPage: currentPage + 1, nextPage: currentPage + 1,
); );
} }

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; 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/favourites_bloc/favourites_bloc.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart'; import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart';
import 'package:flutter_android_app/presentation/home_page/home_page.dart'; import 'package:flutter_android_app/presentation/home_page/home_page.dart';
@ -30,6 +31,9 @@ class _MyAppState extends State<MyApp> {
return BlocProvider<FavouritesBloc>( return BlocProvider<FavouritesBloc>(
lazy: false, lazy: false,
create: (context) => FavouritesBloc(), create: (context) => FavouritesBloc(),
child: BlocProvider<CurrencyBloc>(
lazy: false,
create: (context) => CurrencyBloc(),
child: BlocProvider<LocaleBloc>( child: BlocProvider<LocaleBloc>(
lazy: false, lazy: false,
create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))), create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))),
@ -63,6 +67,7 @@ class _MyAppState extends State<MyApp> {
), ),
), ),
), ),
),
); );
} }

View File

@ -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<CurrencyEvent, CurrencyState> {
static const String _currencyPrefsKey = 'local_currency';
static const List<String> _availableCurrencyIds = ['usd', 'rub'];
static final String _defaultCurrencyId = _availableCurrencyIds[0];
CurrencyBloc() : super(const CurrencyState()) {
on<LoadLocalCurrencyEvent>(_onLoadLocalCurrency);
on<ToggleLocalCurrencyEvent>(_onToggleLocalCurrency);
}
Future<void> _onLoadLocalCurrency(
LoadLocalCurrencyEvent event, Emitter<CurrencyState> 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<void> _onToggleLocalCurrency(
ToggleLocalCurrencyEvent event, Emitter<CurrencyState> 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,
));
}
}

View File

@ -0,0 +1,11 @@
abstract class CurrencyEvent {
const CurrencyEvent();
}
class LoadLocalCurrencyEvent extends CurrencyEvent {
const LoadLocalCurrencyEvent();
}
class ToggleLocalCurrencyEvent extends CurrencyEvent {
const ToggleLocalCurrencyEvent();
}

View File

@ -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<Object?> get props => [
currencyId,
hasCurrencyLoaded,
];
}

View File

@ -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);
}

View File

@ -22,7 +22,7 @@ class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
final data = prefs.getStringList(_likedPrefsKey); final data = prefs.getStringList(_likedPrefsKey);
emit(state.copyWith( emit(state.copyWith(
likedIds: data, favouritesIds: data,
hasFavouritesLoaded: true, hasFavouritesLoaded: true,
)); ));
} }
@ -42,7 +42,7 @@ class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
prefs.setStringList(_likedPrefsKey, updatedList); prefs.setStringList(_likedPrefsKey, updatedList);
emit(state.copyWith( emit(state.copyWith(
likedIds: updatedList, favouritesIds: updatedList,
hasFavouritesLoaded: true, hasFavouritesLoaded: true,
)); ));
} }

View File

@ -7,7 +7,7 @@ part of 'favourites_state.dart';
// ************************************************************************** // **************************************************************************
abstract class _$FavouritesStateCWProxy { abstract class _$FavouritesStateCWProxy {
FavouritesState likedIds(List<String>? likedIds); FavouritesState favouritesIds(List<String>? favouritesIds);
FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded); FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded);
@ -18,7 +18,7 @@ abstract class _$FavouritesStateCWProxy {
/// FavouritesState(...).copyWith(id: 12, name: "My name") /// FavouritesState(...).copyWith(id: 12, name: "My name")
/// ```` /// ````
FavouritesState call({ FavouritesState call({
List<String>? likedIds, List<String>? favouritesIds,
bool? hasFavouritesLoaded, bool? hasFavouritesLoaded,
}); });
} }
@ -30,7 +30,8 @@ class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy {
final FavouritesState _value; final FavouritesState _value;
@override @override
FavouritesState likedIds(List<String>? likedIds) => this(likedIds: likedIds); FavouritesState favouritesIds(List<String>? favouritesIds) =>
this(favouritesIds: favouritesIds);
@override @override
FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded) => FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded) =>
@ -45,14 +46,14 @@ class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy {
/// FavouritesState(...).copyWith(id: 12, name: "My name") /// FavouritesState(...).copyWith(id: 12, name: "My name")
/// ```` /// ````
FavouritesState call({ FavouritesState call({
Object? likedIds = const $CopyWithPlaceholder(), Object? favouritesIds = const $CopyWithPlaceholder(),
Object? hasFavouritesLoaded = const $CopyWithPlaceholder(), Object? hasFavouritesLoaded = const $CopyWithPlaceholder(),
}) { }) {
return FavouritesState( return FavouritesState(
favouritesIds: likedIds == const $CopyWithPlaceholder() favouritesIds: favouritesIds == const $CopyWithPlaceholder()
? _value.favouritesIds ? _value.favouritesIds
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: likedIds as List<String>?, : favouritesIds as List<String>?,
hasFavouritesLoaded: hasFavouritesLoaded:
hasFavouritesLoaded == const $CopyWithPlaceholder() || hasFavouritesLoaded == const $CopyWithPlaceholder() ||
hasFavouritesLoaded == null hasFavouritesLoaded == null

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.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_bloc.dart';
import 'package:flutter_android_app/presentation/favourites_bloc/favourites_events.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_events.dart';
import 'package:flutter_android_app/presentation/favourites_bloc/favourites_state.dart'; import 'package:flutter_android_app/presentation/favourites_bloc/favourites_state.dart';
@ -21,9 +24,12 @@ class FavouritesPage extends StatefulWidget {
class _FavouritesPageState extends State<FavouritesPage> { class _FavouritesPageState extends State<FavouritesPage> {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
List<String>? favouritesIds; List<String>? favouritesIds;
bool wereIdsPreviouslyLoaded = false; bool wereIdsPreviouslyLoaded = false;
String? currencyId;
@override @override
void initState() { void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -47,14 +53,22 @@ class _FavouritesPageState extends State<FavouritesPage> {
if (state.hasFavouritesLoaded && !wereIdsPreviouslyLoaded) { if (state.hasFavouritesLoaded && !wereIdsPreviouslyLoaded) {
wereIdsPreviouslyLoaded = true; wereIdsPreviouslyLoaded = true;
favouritesIds = state.favouritesIds; favouritesIds = state.favouritesIds;
context.read<CurrencyBloc>().add(const LoadLocalCurrencyEvent());
}
},
child: BlocListener<CurrencyBloc, CurrencyState>(
listener: (context, state) {
if (state.hasCurrencyLoaded && state.currencyId != null) {
currencyId = state.currencyId;
context.read<HomeBloc>().add(HomeLoadFavouritesDataEvent( context.read<HomeBloc>().add(HomeLoadFavouritesDataEvent(
ids: favouritesIds, ids: favouritesIds,
locale: context.locale, locale: context.locale,
currencyId: currencyId!,
)); ));
} }
}, },
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: Column( child: Column(
children: [ children: [
CardsList( CardsList(
@ -74,14 +88,11 @@ class _FavouritesPageState extends State<FavouritesPage> {
], ],
), ),
), ),
),
); );
} }
Future<void> _onRefresh() { Future<void> _onRefresh() {
//context.read<HomeBloc>().add(HomeLoadFavouritesDataEvent(
// ids: favouritesIds,
// locale: context.locale,
//));
wereIdsPreviouslyLoaded = false; wereIdsPreviouslyLoaded = false;
context.read<FavouritesBloc>().add(const LoadFavouritesEvent()); context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
return Future.value(null); return Future.value(null);
@ -120,6 +131,7 @@ class _FavouritesPageState extends State<FavouritesPage> {
ids: favouritesIds, ids: favouritesIds,
nextPage: bloc.state.data?.nextPage, nextPage: bloc.state.data?.nextPage,
locale: context.locale, locale: context.locale,
currencyId: currencyId!,
)); ));
} }
} }

View File

@ -15,7 +15,10 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
const int pageSize = 20; const int pageSize = 20;
if (event.nextPage == null) { if (event.nextPage == null) {
emit(state.copyWith(isLoading: true)); emit(state.copyWith(
isLoading: true,
error: 'NO_ERROR',
));
} else { } else {
emit(state.copyWith(isPaginationLoading: true)); emit(state.copyWith(isPaginationLoading: true));
} }
@ -28,6 +31,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
pageSize: pageSize, pageSize: pageSize,
onError: (e) => error = e, onError: (e) => error = e,
locale: event.locale, locale: event.locale,
currencyId: event.currencyId,
); );
bool isLastPage = false; bool isLastPage = false;
@ -52,7 +56,10 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
const int pageSize = 10; const int pageSize = 10;
if (event.nextPage == null) { if (event.nextPage == null) {
emit(state.copyWith(isLoading: true)); emit(state.copyWith(
isLoading: true,
error: 'NO_ERROR',
));
} else { } else {
emit(state.copyWith(isPaginationLoading: true)); emit(state.copyWith(isPaginationLoading: true));
} }
@ -65,6 +72,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
pageSize: pageSize, pageSize: pageSize,
onError: (e) => error = e, onError: (e) => error = e,
locale: event.locale, locale: event.locale,
currencyId: event.currencyId,
); );
bool isLastPage = false; bool isLastPage = false;

View File

@ -8,14 +8,26 @@ class HomeLoadDataEvent extends HomeEvent {
final String? search; final String? search;
final int? nextPage; final int? nextPage;
final AppLocale? locale; 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 { class HomeLoadFavouritesDataEvent extends HomeEvent {
final List<String>? ids; final List<String>? ids;
final int? nextPage; final int? nextPage;
final AppLocale? locale; 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,
});
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/domain/models/card.dart';
import 'package:flutter_android_app/presentation/home_page/card_crypto.dart'; import 'package:flutter_android_app/presentation/home_page/card_crypto.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -45,17 +46,39 @@ class _CardsListState extends State<CardsList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>( return BlocListener<HomeBloc, HomeState>(
builder: (context, state) => state.error != null listener: (context, state) {
? Text( if (state.error != null && state.error != 'NO_ERROR') {
state.error ?? '', WidgetsBinding.instance.addPostFrameCallback((_) {
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red), 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<HomeBloc, HomeState>(
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 : state.isLoading
? const Center(child: Padding( ? const Center(
child: Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
)) )
)
: BlocBuilder<FavouritesBloc, FavouritesState>( : BlocBuilder<FavouritesBloc, FavouritesState>(
builder: (context, likeState) => Expanded( builder: (context, likeState) => Expanded(
child: RefreshIndicator( child: RefreshIndicator(
@ -79,6 +102,7 @@ class _CardsListState extends State<CardsList> {
), ),
), ),
) )
),
); );
} }

View File

@ -11,6 +11,9 @@ import 'package:flutter_android_app/presentation/home_page/cards_list.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../common/svg_objects.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_bloc.dart';
import '../favourites_bloc/favourites_events.dart'; import '../favourites_bloc/favourites_events.dart';
import '../settings_page/settings_page.dart'; import '../settings_page/settings_page.dart';
@ -83,13 +86,14 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
late String? currencyId;
@override @override
void initState() { void initState() {
SvgObjects.init(); SvgObjects.init();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HomeBloc>().add(HomeLoadDataEvent(locale: context.locale)); context.read<CurrencyBloc>().add(const LoadLocalCurrencyEvent());
context.read<FavouritesBloc>().add(const LoadFavouritesEvent()); context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
}); });
@ -105,7 +109,17 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return BlocListener<CurrencyBloc, CurrencyState>(
listener: (context, state) {
if (state.hasCurrencyLoaded && state.currencyId != null) {
currencyId = state.currencyId;
context.read<HomeBloc>().add(HomeLoadDataEvent(
locale: context.locale,
currencyId: currencyId!,
));
}
},
child: Padding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column( child: Column(
children: [ children: [
@ -114,7 +128,11 @@ class _HomePageState extends State<HomePage> {
child: SearchBar( child: SearchBar(
controller: searchController, controller: searchController,
onChanged: (search) { onChanged: (search) {
Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(search: search, locale: context.locale))); Debounce.run(() => context.read<HomeBloc>().add(HomeLoadDataEvent(
search: search,
locale: context.locale,
currencyId: currencyId!,
)));
}, },
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
trailing: [ trailing: [
@ -122,8 +140,12 @@ class _HomePageState extends State<HomePage> {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
if (searchController.text.isNotEmpty) { if (searchController.text.isNotEmpty) {
FocusManager.instance.primaryFocus?.unfocus();
searchController.clear(); searchController.clear();
context.read<HomeBloc>().add(HomeLoadDataEvent(locale: context.locale)); context.read<HomeBloc>().add(HomeLoadDataEvent(
locale: context.locale,
currencyId: currencyId!,
));
} }
}, },
), ),
@ -150,6 +172,7 @@ class _HomePageState extends State<HomePage> {
), ),
], ],
) )
),
); );
} }
@ -167,7 +190,11 @@ class _HomePageState extends State<HomePage> {
} }
Future<void> _onRefresh() { Future<void> _onRefresh() {
context.read<HomeBloc>().add(HomeLoadDataEvent(search: searchController.text, locale: context.locale)); context.read<HomeBloc>().add(HomeLoadDataEvent(
search: searchController.text,
locale: context.locale,
currencyId: currencyId!,
));
return Future.value(null); return Future.value(null);
} }
@ -179,6 +206,7 @@ class _HomePageState extends State<HomePage> {
} }
void _navToDetails(CardData data) { void _navToDetails(CardData data) {
FocusManager.instance.primaryFocus?.unfocus();
Navigator.push(context, MaterialPageRoute<void>( Navigator.push(context, MaterialPageRoute<void>(
builder: (context) => DetailsPage(data)), builder: (context) => DetailsPage(data)),
); );
@ -191,6 +219,7 @@ class _HomePageState extends State<HomePage> {
search: searchController.text, search: searchController.text,
nextPage: bloc.state.data?.nextPage, nextPage: bloc.state.data?.nextPage,
locale: context.locale, locale: context.locale,
currencyId: currencyId!,
)); ));
} }
} }

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.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 'package:flutter_bloc/flutter_bloc.dart';
import '../common/svg_objects.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<CurrencyBloc, CurrencyState>(
builder: (context, currencyState) => GestureDetector(
onTap: () => context.read<CurrencyBloc>().add(const ToggleLocalCurrencyEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 0),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return switch (currencyState.currencyId) {
'rub' => const SvgRu(),
'usd' => const SvgUs(),
_ => const SvgUs(),
};
}),
),
),
),
),
],
),
], ],
), ),
); );

View File

@ -10,6 +10,7 @@ abstract class ApiInterface {
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
AppLocale? locale, AppLocale? locale,
String currencyId,
}); });
Future<HomeData?> loadDataWithIds({ Future<HomeData?> loadDataWithIds({
@ -18,5 +19,6 @@ abstract class ApiInterface {
int page = 1, int page = 1,
int pageSize = 8, int pageSize = 8,
AppLocale? locale, AppLocale? locale,
String currencyId,
}); });
} }

View File

@ -29,11 +29,12 @@ class CryptoRepository extends ApiInterface {
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
AppLocale? locale, AppLocale? locale,
String currencyId = '',
}) async { }) async {
try { try {
Map<String, dynamic> queryParams = { Map<String, dynamic> queryParams = {
'x_cg_demo_api_key': _apiKey, 'x_cg_demo_api_key': _apiKey,
'vs_currency': _getCurrencyName(locale?.localeName), 'vs_currency': currencyId,
'per_page': pageSize, 'per_page': pageSize,
'page': page, 'page': page,
}; };
@ -68,7 +69,7 @@ class CryptoRepository extends ApiInterface {
); );
final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>); final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>);
final HomeData data = dto.toDomain(locale, page); final HomeData data = dto.toDomain(locale, currencyId, page);
return data; return data;
} on DioException catch (e) { } on DioException catch (e) {
@ -84,11 +85,12 @@ class CryptoRepository extends ApiInterface {
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
AppLocale? locale, AppLocale? locale,
String currencyId = '',
}) async { }) async {
try { try {
Map<String, dynamic> queryParams = { Map<String, dynamic> queryParams = {
'x_cg_demo_api_key': _apiKey, 'x_cg_demo_api_key': _apiKey,
'vs_currency': _getCurrencyName(locale?.localeName), 'vs_currency': currencyId,
'per_page': pageSize, 'per_page': pageSize,
'page': page, 'page': page,
}; };
@ -108,7 +110,7 @@ class CryptoRepository extends ApiInterface {
); );
final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>); final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>);
final HomeData data = dto.toDomain(locale, page); final HomeData data = dto.toDomain(locale, currencyId, page);
return data; return data;
} on DioException catch (e) { } on DioException catch (e) {
@ -116,15 +118,4 @@ class CryptoRepository extends ApiInterface {
return null; return null;
} }
} }
String _getCurrencyName(String? localeName) {
if (localeName == null) {
return 'usd';
}
return switch (localeName) {
'ru' => 'rub',
_ => 'usd',
};
}
} }

View File

@ -13,6 +13,7 @@ class MockRepository extends ApiInterface {
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
AppLocale? locale, AppLocale? locale,
String currencyId = '',
}) async { }) async {
return HomeData(data: [ return HomeData(data: [
CardData( CardData(
@ -46,6 +47,7 @@ class MockRepository extends ApiInterface {
int page = 1, int page = 1,
int pageSize = 20, int pageSize = 20,
AppLocale? locale, AppLocale? locale,
String currencyId = '',
}) async { }) async {
return HomeData(data: [ return HomeData(data: [
CardData( CardData(