added separate setting to change currency
This commit is contained in:
parent
f0a17af845
commit
1377a5dbdd
@ -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",
|
||||
|
@ -10,9 +10,12 @@
|
||||
"addedToFavourite": "добавлен в избранное",
|
||||
"removedFromFavourite": "удален из избранного",
|
||||
|
||||
"cardsLoadingFailed": "Сервер недоступен",
|
||||
"cardsLoadingFailedSnackBar": "Не удалось загрузить данные о криптовалюте",
|
||||
"coinDataPriceChange": "за последние 24 часа",
|
||||
|
||||
"settingsLanguage": "Язык",
|
||||
"settingsCurrency": "Валюта",
|
||||
|
||||
"navigationHome": "Главная",
|
||||
"navigationFavourites": "Избранное",
|
||||
|
@ -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:
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 => 'Главная';
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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<MyApp> {
|
||||
return BlocProvider<FavouritesBloc>(
|
||||
lazy: false,
|
||||
create: (context) => FavouritesBloc(),
|
||||
child: BlocProvider<LocaleBloc>(
|
||||
child: BlocProvider<CurrencyBloc>(
|
||||
lazy: false,
|
||||
create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))),
|
||||
child: BlocBuilder<LocaleBloc, LocaleState>(
|
||||
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<LocaleBloc>(
|
||||
lazy: false,
|
||||
create: (context) => LocaleBloc(Locale(_getLangCode(Platform.localeName))),
|
||||
child: BlocBuilder<LocaleBloc, LocaleState>(
|
||||
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<CryptoRepository>(
|
||||
lazy: true,
|
||||
create: (_) => CryptoRepository(),
|
||||
child: BlocProvider<HomeBloc>(
|
||||
lazy: false,
|
||||
create: (context) => HomeBloc(context.read<CryptoRepository>()),
|
||||
child: MainScaffold(
|
||||
toggleDarkMode: _toggleDarkMode,
|
||||
isDarkModeSelected: isDarkMode,
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: RepositoryProvider<CryptoRepository>(
|
||||
lazy: true,
|
||||
create: (_) => CryptoRepository(),
|
||||
child: BlocProvider<HomeBloc>(
|
||||
lazy: false,
|
||||
create: (context) => HomeBloc(context.read<CryptoRepository>()),
|
||||
child: MainScaffold(
|
||||
toggleDarkMode: _toggleDarkMode,
|
||||
isDarkModeSelected: isDarkMode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
61
lib/presentation/currency_bloc/currency_bloc.dart
Normal file
61
lib/presentation/currency_bloc/currency_bloc.dart
Normal 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,
|
||||
));
|
||||
}
|
||||
}
|
11
lib/presentation/currency_bloc/currency_events.dart
Normal file
11
lib/presentation/currency_bloc/currency_events.dart
Normal file
@ -0,0 +1,11 @@
|
||||
abstract class CurrencyEvent {
|
||||
const CurrencyEvent();
|
||||
}
|
||||
|
||||
class LoadLocalCurrencyEvent extends CurrencyEvent {
|
||||
const LoadLocalCurrencyEvent();
|
||||
}
|
||||
|
||||
class ToggleLocalCurrencyEvent extends CurrencyEvent {
|
||||
const ToggleLocalCurrencyEvent();
|
||||
}
|
21
lib/presentation/currency_bloc/currency_state.dart
Normal file
21
lib/presentation/currency_bloc/currency_state.dart
Normal 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,
|
||||
];
|
||||
}
|
69
lib/presentation/currency_bloc/currency_state.g.dart
Normal file
69
lib/presentation/currency_bloc/currency_state.g.dart
Normal 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);
|
||||
}
|
@ -22,7 +22,7 @@ class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
|
||||
final data = prefs.getStringList(_likedPrefsKey);
|
||||
|
||||
emit(state.copyWith(
|
||||
likedIds: data,
|
||||
favouritesIds: data,
|
||||
hasFavouritesLoaded: true,
|
||||
));
|
||||
}
|
||||
@ -42,7 +42,7 @@ class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
|
||||
prefs.setStringList(_likedPrefsKey, updatedList);
|
||||
|
||||
emit(state.copyWith(
|
||||
likedIds: updatedList,
|
||||
favouritesIds: updatedList,
|
||||
hasFavouritesLoaded: true,
|
||||
));
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ part of 'favourites_state.dart';
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$FavouritesStateCWProxy {
|
||||
FavouritesState likedIds(List<String>? likedIds);
|
||||
FavouritesState favouritesIds(List<String>? favouritesIds);
|
||||
|
||||
FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded);
|
||||
|
||||
@ -18,7 +18,7 @@ abstract class _$FavouritesStateCWProxy {
|
||||
/// FavouritesState(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
FavouritesState call({
|
||||
List<String>? likedIds,
|
||||
List<String>? favouritesIds,
|
||||
bool? hasFavouritesLoaded,
|
||||
});
|
||||
}
|
||||
@ -30,7 +30,8 @@ class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy {
|
||||
final FavouritesState _value;
|
||||
|
||||
@override
|
||||
FavouritesState likedIds(List<String>? likedIds) => this(likedIds: likedIds);
|
||||
FavouritesState favouritesIds(List<String>? 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<String>?,
|
||||
: favouritesIds as List<String>?,
|
||||
hasFavouritesLoaded:
|
||||
hasFavouritesLoaded == const $CopyWithPlaceholder() ||
|
||||
hasFavouritesLoaded == null
|
||||
|
@ -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<FavouritesPage> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
List<String>? favouritesIds;
|
||||
bool wereIdsPreviouslyLoaded = false;
|
||||
|
||||
String? currencyId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@ -47,41 +53,46 @@ class _FavouritesPageState extends State<FavouritesPage> {
|
||||
if (state.hasFavouritesLoaded && !wereIdsPreviouslyLoaded) {
|
||||
wereIdsPreviouslyLoaded = true;
|
||||
favouritesIds = state.favouritesIds;
|
||||
context.read<HomeBloc>().add(HomeLoadFavouritesDataEvent(
|
||||
ids: favouritesIds,
|
||||
locale: context.locale,
|
||||
));
|
||||
context.read<CurrencyBloc>().add(const LoadLocalCurrencyEvent());
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
CardsList(
|
||||
onListRefresh: _onRefresh,
|
||||
onCardLiked: _onLike,
|
||||
onCardTapped: _navToDetails,
|
||||
onNextPage: _onNextPage,
|
||||
),
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.isPaginationLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
child: BlocListener<CurrencyBloc, CurrencyState>(
|
||||
listener: (context, state) {
|
||||
if (state.hasCurrencyLoaded && state.currencyId != null) {
|
||||
currencyId = state.currencyId;
|
||||
context.read<HomeBloc>().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<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.isPaginationLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() {
|
||||
//context.read<HomeBloc>().add(HomeLoadFavouritesDataEvent(
|
||||
// ids: favouritesIds,
|
||||
// locale: context.locale,
|
||||
//));
|
||||
wereIdsPreviouslyLoaded = false;
|
||||
context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
|
||||
return Future.value(null);
|
||||
@ -120,6 +131,7 @@ class _FavouritesPageState extends State<FavouritesPage> {
|
||||
ids: favouritesIds,
|
||||
nextPage: bloc.state.data?.nextPage,
|
||||
locale: context.locale,
|
||||
currencyId: currencyId!,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,10 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
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<HomeEvent, HomeState> {
|
||||
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<HomeEvent, HomeState> {
|
||||
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<HomeEvent, HomeState> {
|
||||
pageSize: pageSize,
|
||||
onError: (e) => error = e,
|
||||
locale: event.locale,
|
||||
currencyId: event.currencyId,
|
||||
);
|
||||
|
||||
bool isLastPage = false;
|
||||
|
@ -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<String>? 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,
|
||||
});
|
||||
}
|
||||
|
@ -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<CardsList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<HomeBloc, HomeState>(
|
||||
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<FavouritesBloc, FavouritesState>(
|
||||
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<HomeBloc, HomeState>(
|
||||
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<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
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
)
|
||||
: BlocBuilder<FavouritesBloc, FavouritesState>(
|
||||
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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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<HomePage> {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
late String? currencyId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
SvgObjects.init();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HomeBloc>().add(HomeLoadDataEvent(locale: context.locale));
|
||||
context.read<CurrencyBloc>().add(const LoadLocalCurrencyEvent());
|
||||
context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
|
||||
});
|
||||
|
||||
@ -105,51 +109,70 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
@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<HomeBloc>().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<HomeBloc>().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<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),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SearchBar(
|
||||
controller: searchController,
|
||||
onChanged: (search) {
|
||||
Debounce.run(() => context.read<HomeBloc>().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<HomeBloc>().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<HomeBloc, HomeState>(
|
||||
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<HomeBloc, HomeState>(
|
||||
builder: (context, state) => state.isPaginationLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -167,7 +190,11 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -179,6 +206,7 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
|
||||
void _navToDetails(CardData data) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
Navigator.push(context, MaterialPageRoute<void>(
|
||||
builder: (context) => DetailsPage(data)),
|
||||
);
|
||||
@ -191,6 +219,7 @@ class _HomePageState extends State<HomePage> {
|
||||
search: searchController.text,
|
||||
nextPage: bloc.state.data?.nextPage,
|
||||
locale: context.locale,
|
||||
currencyId: currencyId!,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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<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(),
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ abstract class ApiInterface {
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
AppLocale? locale,
|
||||
String currencyId,
|
||||
});
|
||||
|
||||
Future<HomeData?> loadDataWithIds({
|
||||
@ -18,5 +19,6 @@ abstract class ApiInterface {
|
||||
int page = 1,
|
||||
int pageSize = 8,
|
||||
AppLocale? locale,
|
||||
String currencyId,
|
||||
});
|
||||
}
|
||||
|
@ -29,11 +29,12 @@ class CryptoRepository extends ApiInterface {
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
AppLocale? locale,
|
||||
String currencyId = '',
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> 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<dynamic>);
|
||||
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<String, dynamic> 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<dynamic>);
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user