12 Commits
main ... Lab_7

Author SHA1 Message Date
1377a5dbdd added separate setting to change currency 2024-12-30 11:57:56 +04:00
f0a17af845 final improvements
added internet permission in manifest
added unique app bar titles for each section
fixed card layout
fixed card details page layout
added appbar button for toggling dark mode
2024-12-17 14:31:39 +04:00
1a863ee7f7 implemented favourites page
added navigation bar
renamed "like" to "favourite"
made cards list into separate widget
2024-12-17 12:10:42 +04:00
04120c4847 fixed localizations issues
changed application name
updated svg icons
2024-12-16 22:59:15 +04:00
a676d454ca implemented mock crypto repository
changed Cupertino search bar to Material UI 3
changed baseline color and card colors
implemented fetching crypto data from public API (untested)
2024-12-15 23:23:00 +04:00
efdbe85897 implemented shared preferences 2024-12-12 13:54:24 +04:00
b16bb21a20 added pagination 2024-12-12 00:08:22 +04:00
e24dbfe4be implemented Potter repository 2024-10-22 13:52:57 +04:00
9e3be50899 implemented details page 2024-10-03 01:00:26 +04:00
e1e9ca4194 implemented SnackBar appearance 2024-10-03 00:44:31 +04:00
ff75a800ef refactored 2024-10-03 00:24:49 +04:00
256024775c finished card widget 2024-10-02 23:51:57 +04:00
59 changed files with 3001 additions and 68 deletions

17
Makefile Normal file
View File

@@ -0,0 +1,17 @@
gen:
C:\Users\Oleg\AppData\Local\flutter\bin\flutter.bat pub run build_runner build --delete-conflicting-outputs
icon:
C:\Users\Oleg\AppData\Local\flutter\bin\flutter.bat pub run flutter_launcher_icons:main
init_res:
C:\Users\Oleg\AppData\Local\flutter\bin\dart.bat pub global activate flutter_asset_generator
format:
C:\Users\Oleg\AppData\Local\flutter\bin\dart.bat format . --line-length 200
res:
C:\Users\Oleg\AppData\Local\Pub\Cache\bin\fgen.bat --output lib/components/resources.g.dart --no-watch --no-preview
loc:
flutter gen-l10n

View File

@@ -8,15 +8,15 @@ plugins {
android {
namespace = "com.example.flutter_android_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion "25.1.8937393"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = 17
}
defaultConfig {

View File

@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="flutter_android_app"
android:label="Crypto Exchange"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.3.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}
include ":app"

BIN
assets/launcher.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

19
assets/svg/ru.svg Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_503_2726)">
<rect x="0.25" y="0.25" width="27.5" height="19.5" rx="1.75" fill="white" stroke="#F5F5F5" stroke-width="0.5"/>
<mask id="mask0_503_2726" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
<rect x="0.25" y="0.25" width="27.5" height="19.5" rx="1.75" fill="white" stroke="white" stroke-width="0.5"/>
</mask>
<g mask="url(#mask0_503_2726)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13.3333H28V6.66667H0V13.3333Z" fill="#0C47B7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20H28V13.3333H0V20Z" fill="#E53B35"/>
</g>
</g>
<defs>
<clipPath id="clip0_503_2726">
<rect width="28" height="20" rx="2" fill="white"/>
</clipPath>
</defs>

After

Width:  |  Height:  |  Size: 968 B

34
assets/svg/us.svg Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -4 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_503_3486)">
<rect width="28" height="20" rx="2" fill="white"/>
<mask id="mask0_503_3486" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="20">
<rect width="28" height="20" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 0H0V1.33333H28V0ZM28 2.66667H0V4H28V2.66667ZM0 5.33333H28V6.66667H0V5.33333ZM28 8H0V9.33333H28V8ZM0 10.6667H28V12H0V10.6667ZM28 13.3333H0V14.6667H28V13.3333ZM0 16H28V17.3333H0V16ZM28 18.6667H0V20H28V18.6667Z" fill="#D02F44"/>
<rect width="12" height="9.33333" fill="#46467F"/>
<g filter="url(#filter0_d_503_3486)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66665 1.99999C2.66665 2.36818 2.36817 2.66666 1.99998 2.66666C1.63179 2.66666 1.33331 2.36818 1.33331 1.99999C1.33331 1.63181 1.63179 1.33333 1.99998 1.33333C2.36817 1.33333 2.66665 1.63181 2.66665 1.99999ZM5.33331 1.99999C5.33331 2.36818 5.03484 2.66666 4.66665 2.66666C4.29846 2.66666 3.99998 2.36818 3.99998 1.99999C3.99998 1.63181 4.29846 1.33333 4.66665 1.33333C5.03484 1.33333 5.33331 1.63181 5.33331 1.99999ZM7.33331 2.66666C7.7015 2.66666 7.99998 2.36818 7.99998 1.99999C7.99998 1.63181 7.7015 1.33333 7.33331 1.33333C6.96512 1.33333 6.66665 1.63181 6.66665 1.99999C6.66665 2.36818 6.96512 2.66666 7.33331 2.66666ZM10.6666 1.99999C10.6666 2.36818 10.3682 2.66666 9.99998 2.66666C9.63179 2.66666 9.33331 2.36818 9.33331 1.99999C9.33331 1.63181 9.63179 1.33333 9.99998 1.33333C10.3682 1.33333 10.6666 1.63181 10.6666 1.99999ZM3.33331 3.99999C3.7015 3.99999 3.99998 3.70152 3.99998 3.33333C3.99998 2.96514 3.7015 2.66666 3.33331 2.66666C2.96512 2.66666 2.66665 2.96514 2.66665 3.33333C2.66665 3.70152 2.96512 3.99999 3.33331 3.99999ZM6.66665 3.33333C6.66665 3.70152 6.36817 3.99999 5.99998 3.99999C5.63179 3.99999 5.33331 3.70152 5.33331 3.33333C5.33331 2.96514 5.63179 2.66666 5.99998 2.66666C6.36817 2.66666 6.66665 2.96514 6.66665 3.33333ZM8.66665 3.99999C9.03484 3.99999 9.33331 3.70152 9.33331 3.33333C9.33331 2.96514 9.03484 2.66666 8.66665 2.66666C8.29846 2.66666 7.99998 2.96514 7.99998 3.33333C7.99998 3.70152 8.29846 3.99999 8.66665 3.99999ZM10.6666 4.66666C10.6666 5.03485 10.3682 5.33333 9.99998 5.33333C9.63179 5.33333 9.33331 5.03485 9.33331 4.66666C9.33331 4.29847 9.63179 3.99999 9.99998 3.99999C10.3682 3.99999 10.6666 4.29847 10.6666 4.66666ZM7.33331 5.33333C7.7015 5.33333 7.99998 5.03485 7.99998 4.66666C7.99998 4.29847 7.7015 3.99999 7.33331 3.99999C6.96512 3.99999 6.66665 4.29847 6.66665 4.66666C6.66665 5.03485 6.96512 5.33333 7.33331 5.33333ZM5.33331 4.66666C5.33331 5.03485 5.03484 5.33333 4.66665 5.33333C4.29846 5.33333 3.99998 5.03485 3.99998 4.66666C3.99998 4.29847 4.29846 3.99999 4.66665 3.99999C5.03484 3.99999 5.33331 4.29847 5.33331 4.66666ZM1.99998 5.33333C2.36817 5.33333 2.66665 5.03485 2.66665 4.66666C2.66665 4.29847 2.36817 3.99999 1.99998 3.99999C1.63179 3.99999 1.33331 4.29847 1.33331 4.66666C1.33331 5.03485 1.63179 5.33333 1.99998 5.33333ZM3.99998 5.99999C3.99998 6.36819 3.7015 6.66666 3.33331 6.66666C2.96512 6.66666 2.66665 6.36819 2.66665 5.99999C2.66665 5.6318 2.96512 5.33333 3.33331 5.33333C3.7015 5.33333 3.99998 5.6318 3.99998 5.99999ZM5.99998 6.66666C6.36817 6.66666 6.66665 6.36819 6.66665 5.99999C6.66665 5.6318 6.36817 5.33333 5.99998 5.33333C5.63179 5.33333 5.33331 5.6318 5.33331 5.99999C5.33331 6.36819 5.63179 6.66666 5.99998 6.66666ZM9.33331 5.99999C9.33331 6.36819 9.03484 6.66666 8.66665 6.66666C8.29846 6.66666 7.99998 6.36819 7.99998 5.99999C7.99998 5.6318 8.29846 5.33333 8.66665 5.33333C9.03484 5.33333 9.33331 5.6318 9.33331 5.99999ZM9.99998 8C10.3682 8 10.6666 7.70152 10.6666 7.33333C10.6666 6.96514 10.3682 6.66666 9.99998 6.66666C9.63179 6.66666 9.33331 6.96514 9.33331 7.33333C9.33331 7.70152 9.63179 8 9.99998 8ZM7.99998 7.33333C7.99998 7.70152 7.7015 8 7.33331 8C6.96512 8 6.66665 7.70152 6.66665 7.33333C6.66665 6.96514 6.96512 6.66666 7.33331 6.66666C7.7015 6.66666 7.99998 6.96514 7.99998 7.33333ZM4.66665 8C5.03484 8 5.33331 7.70152 5.33331 7.33333C5.33331 6.96514 5.03484 6.66666 4.66665 6.66666C4.29846 6.66666 3.99998 6.96514 3.99998 7.33333C3.99998 7.70152 4.29846 8 4.66665 8ZM2.66665 7.33333C2.66665 7.70152 2.36817 8 1.99998 8C1.63179 8 1.33331 7.70152 1.33331 7.33333C1.33331 6.96514 1.63179 6.66666 1.99998 6.66666C2.36817 6.66666 2.66665 6.96514 2.66665 7.33333Z" fill="url(#paint0_linear_503_3486)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_d_503_3486" x="1.33331" y="1.33333" width="9.33331" height="7.66667" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_503_3486"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_503_3486" result="shape"/>
</filter>
<linearGradient id="paint0_linear_503_3486" x1="1.33331" y1="1.33333" x2="1.33331" y2="7.99999" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F0F0F0"/>
</linearGradient>
<clipPath id="clip0_503_3486">
<rect width="28" height="20" rx="2" fill="white"/>
</clipPath>
</defs>

After

Width:  |  Height:  |  Size: 5.5 KiB

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

6
l10n.yaml Normal file
View File

@@ -0,0 +1,6 @@
arb-dir: l10n
template-arb-file: app_en.arb
output-localization: app_locale.dart
output-dir: lib/components/locale/l10n
output-class: AppLocale
synthetic-package: false

23
l10n/app_en.arb Normal file
View File

@@ -0,0 +1,23 @@
{
"@@locale": "en",
"mainAppBarTitle": "Cryptocurrency Exchange",
"detailsPageAppBarTitle": "Cryptocurrency info",
"favouritesPageAppBarTitle": "Favourites",
"settingsPageAppBarTitle": "Settings",
"searchHint": "Search",
"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",
"navigationSettings": "Settings"
}

23
l10n/app_ru.arb Normal file
View File

@@ -0,0 +1,23 @@
{
"@@locale": "ru",
"mainAppBarTitle": "Криптобиржа",
"detailsPageAppBarTitle": "Сведения о валюте",
"favouritesPageAppBarTitle": "Избранное",
"settingsPageAppBarTitle": "Настройки",
"searchHint": "Поиск",
"addedToFavourite": "добавлен в избранное",
"removedFromFavourite": "удален из избранного",
"cardsLoadingFailed": "Сервер недоступен",
"cardsLoadingFailedSnackBar": "Не удалось загрузить данные о криптовалюте",
"coinDataPriceChange": "за последние 24 часа",
"settingsLanguage": "Язык",
"settingsCurrency": "Валюта",
"navigationHome": "Главная",
"navigationFavourites": "Избранное",
"navigationSettings": "Настройки"
}

View File

@@ -0,0 +1,6 @@
import 'package:flutter/cupertino.dart';
import '../locale/l10n/app_localizations.dart';
extension LocalContextX on BuildContext {
AppLocale get locale => AppLocale.of(this)!;
}

View File

@@ -0,0 +1,219 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_ru.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocale
/// returned by `AppLocale.of(context)`.
///
/// Applications need to include `AppLocale.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocale.localizationsDelegates,
/// supportedLocales: AppLocale.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocale.supportedLocales
/// property.
abstract class AppLocale {
AppLocale(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocale? of(BuildContext context) {
return Localizations.of<AppLocale>(context, AppLocale);
}
static const LocalizationsDelegate<AppLocale> delegate = _AppLocaleDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('ru')
];
/// No description provided for @mainAppBarTitle.
///
/// In en, this message translates to:
/// **'Cryptocurrency Exchange'**
String get mainAppBarTitle;
/// No description provided for @detailsPageAppBarTitle.
///
/// In en, this message translates to:
/// **'Cryptocurrency info'**
String get detailsPageAppBarTitle;
/// No description provided for @favouritesPageAppBarTitle.
///
/// In en, this message translates to:
/// **'Favourites'**
String get favouritesPageAppBarTitle;
/// No description provided for @settingsPageAppBarTitle.
///
/// In en, this message translates to:
/// **'Settings'**
String get settingsPageAppBarTitle;
/// No description provided for @searchHint.
///
/// In en, this message translates to:
/// **'Search'**
String get searchHint;
/// No description provided for @addedToFavourite.
///
/// In en, this message translates to:
/// **'is added to favourites'**
String get addedToFavourite;
/// No description provided for @removedFromFavourite.
///
/// In en, this message translates to:
/// **'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:
/// **'for the last 24 hours'**
String get coinDataPriceChange;
/// No description provided for @settingsLanguage.
///
/// In en, this message translates to:
/// **'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:
/// **'Home'**
String get navigationHome;
/// No description provided for @navigationFavourites.
///
/// In en, this message translates to:
/// **'Favourites'**
String get navigationFavourites;
/// No description provided for @navigationSettings.
///
/// In en, this message translates to:
/// **'Settings'**
String get navigationSettings;
}
class _AppLocaleDelegate extends LocalizationsDelegate<AppLocale> {
const _AppLocaleDelegate();
@override
Future<AppLocale> load(Locale locale) {
return SynchronousFuture<AppLocale>(lookupAppLocale(locale));
}
@override
bool isSupported(Locale locale) => <String>['en', 'ru'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocaleDelegate old) => false;
}
AppLocale lookupAppLocale(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en': return AppLocaleEn();
case 'ru': return AppLocaleRu();
}
throw FlutterError(
'AppLocale.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
);
}

View File

@@ -0,0 +1,53 @@
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocaleEn extends AppLocale {
AppLocaleEn([String locale = 'en']) : super(locale);
@override
String get mainAppBarTitle => 'Cryptocurrency Exchange';
@override
String get detailsPageAppBarTitle => 'Cryptocurrency info';
@override
String get favouritesPageAppBarTitle => 'Favourites';
@override
String get settingsPageAppBarTitle => 'Settings';
@override
String get searchHint => 'Search';
@override
String get addedToFavourite => 'is added to favourites';
@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';
@override
String get navigationFavourites => 'Favourites';
@override
String get navigationSettings => 'Settings';
}

View File

@@ -0,0 +1,53 @@
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocaleRu extends AppLocale {
AppLocaleRu([String locale = 'ru']) : super(locale);
@override
String get mainAppBarTitle => 'Криптобиржа';
@override
String get detailsPageAppBarTitle => 'Сведения о валюте';
@override
String get favouritesPageAppBarTitle => 'Избранное';
@override
String get settingsPageAppBarTitle => 'Настройки';
@override
String get searchHint => 'Поиск';
@override
String get addedToFavourite => 'добавлен в избранное';
@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 => 'Главная';
@override
String get navigationFavourites => 'Избранное';
@override
String get navigationSettings => 'Настройки';
}

View File

@@ -0,0 +1,10 @@
/// Generate by [asset_generator](https://github.com/fluttercandies/flutter_asset_generator) library.
/// PLEASE DO NOT EDIT MANUALLY.
// ignore_for_file: constant_identifier_names
class R {
const R._();
static const String ASSETS_SVG_RU_SVG = 'assets/svg/ru.svg';
static const String ASSETS_SVG_US_SVG = 'assets/svg/us.svg';
}

View File

@@ -0,0 +1,20 @@
import 'dart:async';
import 'dart:ui';
class Debounce {
factory Debounce() => _instance;
Debounce._();
static final Debounce _instance = Debounce._();
static Timer? _timer;
static void run(
VoidCallback action,
{Duration delay = const Duration(milliseconds: 500)}
) {
_timer?.cancel();
_timer = Timer(delay, action);
}
}

View File

@@ -0,0 +1 @@
typedef OnErrorCallback = void Function(String? error);

View File

@@ -0,0 +1,34 @@
import 'package:json_annotation/json_annotation.dart';
part 'coins_dto.g.dart';
class CoinsDto {
final List<CoinDataDto>? coins;
CoinsDto({this.coins});
factory CoinsDto.fromJson(List<dynamic> json) => CoinsDto(
coins: json.map((e) => CoinDataDto.fromJson(e as Map<String, dynamic>)).toList(),
);
}
@JsonSerializable(createToJson: false)
class CoinDataDto {
final String? id;
final String? name;
final String? image;
@JsonKey(name: 'current_price')
final double? currentPrice;
@JsonKey(name: 'price_change_24h')
final double? priceChange24h;
CoinDataDto({
this.id,
this.name,
this.image,
this.currentPrice,
this.priceChange24h,
});
factory CoinDataDto.fromJson(Map<String, dynamic> json) => _$CoinDataDtoFromJson(json);
}

View File

@@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'coins_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CoinDataDto _$CoinDataDtoFromJson(Map<String, dynamic> json) => CoinDataDto(
id: json['id'] as String?,
name: json['name'] as String?,
image: json['image'] as String?,
currentPrice: (json['current_price'] as num?)?.toDouble(),
priceChange24h: (json['price_change_24h'] as num?)?.toDouble(),
);

View File

@@ -0,0 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
part 'search_coins_dto.g.dart';
@JsonSerializable(createToJson: false)
class SearchCoinsDto {
final List<SearchCoinDto>? coins;
const SearchCoinsDto({this.coins});
factory SearchCoinsDto.fromJson(Map<String, dynamic> json) => _$SearchCoinsDtoFromJson(json);
}
@JsonSerializable(createToJson: false)
class SearchCoinDto {
final String? id;
const SearchCoinDto({this.id});
factory SearchCoinDto.fromJson(Map<String, dynamic> json) => _$SearchCoinDtoFromJson(json);
}

View File

@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_coins_dto.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SearchCoinsDto _$SearchCoinsDtoFromJson(Map<String, dynamic> json) =>
SearchCoinsDto(
coins: (json['coins'] as List<dynamic>?)
?.map((e) => SearchCoinDto.fromJson(e as Map<String, dynamic>))
.toList(),
);
SearchCoinDto _$SearchCoinDtoFromJson(Map<String, dynamic> json) =>
SearchCoinDto(
id: json['id'] as String?,
);

View File

@@ -0,0 +1,45 @@
import 'package:flutter_android_app/components/locale/l10n/app_localizations.dart';
import 'package:flutter_android_app/data/dtos/coins_dto.dart';
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, String currencyId) => CardData(
id: id ?? 'UNKNOWN',
title: name ?? 'UNKNOWN',
imageUrl: image,
currentPrice: '$currentPrice ${_getCurrencySymbol(currencyId)}',
priceChange: _getLocalizedPriceChange(priceChange24h, locale, currencyId),
);
String _getCurrencySymbol(String currencyId) {
return switch (currencyId) {
'rub' => '',
'usd' => '\$',
_ => '?',
};
}
String _getLocalizedPriceChange(double? priceChange, AppLocale? locale, String currencyId) {
if (priceChange == null) {
return '+0 ${_getCurrencySymbol(currencyId)}';
}
String retVal = '';
if (priceChange >= 0.0) {
retVal += '+';
}
retVal += '$priceChange ${_getCurrencySymbol(currencyId)}';
return '$retVal ${locale?.coinDataPriceChange}';
}
}
extension CoinsDtoToModel on CoinsDto {
HomeData toDomain(AppLocale? locale, String currencyId, int currentPage) => HomeData(
data: coins?.map((e) => e.toDomain(locale, currencyId)).toList(),
nextPage: currentPage + 1,
);
}

View File

@@ -0,0 +1,15 @@
class CardData {
final String id;
final String title;
final String? imageUrl;
final String currentPrice;
final String priceChange;
CardData({
required this.id,
required this.title,
this.imageUrl,
required this.currentPrice,
required this.priceChange,
});
}

View File

@@ -0,0 +1,8 @@
import 'card.dart';
class HomeData {
final List<CardData>? data;
final int? nextPage;
HomeData({this.data, this.nextPage});
}

View File

@@ -1,78 +1,84 @@
import 'dart:math';
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';
import 'package:flutter_android_app/presentation/locale_bloc/locale_bloc.dart';
import 'package:flutter_android_app/presentation/locale_bloc/locale_state.dart';
import 'package:flutter_android_app/repositories/crypto_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'components/locale/l10n/app_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isDarkMode = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
return BlocProvider<FavouritesBloc>(
lazy: false,
create: (context) => FavouritesBloc(),
child: BlocProvider<CurrencyBloc>(
lazy: false,
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,
),
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,
),
),
),
),
),
),
),
home: const MyHomePage(title: 'Шабунов Олег Андреевич 💯'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
Color _color = Colors.deepPurpleAccent;
void _incrementCounter() {
void _toggleDarkMode() {
setState(() {
++_counter;
_color = Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
isDarkMode = !isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: _color,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Кнопка нажата столько раз:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
if (_counter > 10)
Text(
'А теперь потапай хомяка',
style: Theme.of(context).textTheme.headlineMedium,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
backgroundColor: _color,
tooltip: 'Increment',
child: const Icon(Icons.ac_unit),
),
);
String _getLangCode(String fullLocaleName) {
int index = fullLocaleName.indexOf('_');
return index != -1 ? fullLocaleName.substring(0, index) : fullLocaleName;
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../components/resources.g.dart';
abstract class SvgObjects {
static void init() {
final pics = <String>[R.ASSETS_SVG_RU_SVG, R.ASSETS_SVG_US_SVG];
for (final String p in pics) {
final loader = SvgAssetLoader(p);
svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
}
}
}
class SvgRu extends StatelessWidget {
const SvgRu({super.key});
@override
Widget build(BuildContext context) {
return SvgPicture.asset(R.ASSETS_SVG_RU_SVG);
}
}
class SvgUs extends StatelessWidget {
const SvgUs({super.key});
@override
Widget build(BuildContext context) {
return SvgPicture.asset(R.ASSETS_SVG_US_SVG);
}
}

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

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_android_app/domain/models/card.dart';
class DetailsPage extends StatelessWidget {
final CardData data;
const DetailsPage(this.data, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.locale.detailsPageAppBarTitle),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.only(left: 20, right: 20, top: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Image.network(data.imageUrl ?? ''),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(data.title, style: Theme.of(context).textTheme.headlineLarge),
),
Text(data.currentPrice, style: Theme.of(context).textTheme.bodyLarge),
Text(data.priceChange, style: Theme.of(context).textTheme.labelLarge),
],
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'favourites_events.dart';
import 'favourites_state.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
static const String _likedPrefsKey = 'liked';
FavouritesBloc() : super(const FavouritesState(favouritesIds: [])) {
on<LoadFavouritesEvent>(_onLoadFavouritesIds);
on<ChangeFavouriteEvent>(_onChangeFavourite);
}
Future<void> _onLoadFavouritesIds(
LoadFavouritesEvent event, Emitter<FavouritesState> emit
) async {
emit(state.copyWith(
hasFavouritesLoaded: false,
));
final prefs = await SharedPreferences.getInstance();
final data = prefs.getStringList(_likedPrefsKey);
emit(state.copyWith(
favouritesIds: data,
hasFavouritesLoaded: true,
));
}
Future<void> _onChangeFavourite(
ChangeFavouriteEvent event, Emitter<FavouritesState> emit
) async {
final updatedList = List<String>.from(state.favouritesIds ?? []);
if (updatedList.contains(event.id)) {
updatedList.remove(event.id);
} else {
updatedList.add(event.id);
}
final prefs = await SharedPreferences.getInstance();
prefs.setStringList(_likedPrefsKey, updatedList);
emit(state.copyWith(
favouritesIds: updatedList,
hasFavouritesLoaded: true,
));
}
}

View File

@@ -0,0 +1,13 @@
abstract class FavouritesEvent {
const FavouritesEvent();
}
class LoadFavouritesEvent extends FavouritesEvent {
const LoadFavouritesEvent();
}
class ChangeFavouriteEvent extends FavouritesEvent {
final String id;
const ChangeFavouriteEvent(this.id);
}

View File

@@ -0,0 +1,21 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:equatable/equatable.dart';
part 'favourites_state.g.dart';
@CopyWith()
class FavouritesState extends Equatable {
final List<String>? favouritesIds;
final bool hasFavouritesLoaded;
const FavouritesState({
this.favouritesIds,
this.hasFavouritesLoaded = false,
});
@override
List<Object?> get props => [
favouritesIds,
hasFavouritesLoaded,
];
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'favourites_state.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$FavouritesStateCWProxy {
FavouritesState favouritesIds(List<String>? favouritesIds);
FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FavouritesState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// FavouritesState(...).copyWith(id: 12, name: "My name")
/// ````
FavouritesState call({
List<String>? favouritesIds,
bool? hasFavouritesLoaded,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfFavouritesState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfFavouritesState.copyWith.fieldName(...)`
class _$FavouritesStateCWProxyImpl implements _$FavouritesStateCWProxy {
const _$FavouritesStateCWProxyImpl(this._value);
final FavouritesState _value;
@override
FavouritesState favouritesIds(List<String>? favouritesIds) =>
this(favouritesIds: favouritesIds);
@override
FavouritesState hasFavouritesLoaded(bool hasFavouritesLoaded) =>
this(hasFavouritesLoaded: hasFavouritesLoaded);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FavouritesState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// FavouritesState(...).copyWith(id: 12, name: "My name")
/// ````
FavouritesState call({
Object? favouritesIds = const $CopyWithPlaceholder(),
Object? hasFavouritesLoaded = const $CopyWithPlaceholder(),
}) {
return FavouritesState(
favouritesIds: favouritesIds == const $CopyWithPlaceholder()
? _value.favouritesIds
// ignore: cast_nullable_to_non_nullable
: favouritesIds as List<String>?,
hasFavouritesLoaded:
hasFavouritesLoaded == const $CopyWithPlaceholder() ||
hasFavouritesLoaded == null
? _value.hasFavouritesLoaded
// ignore: cast_nullable_to_non_nullable
: hasFavouritesLoaded as bool,
);
}
}
extension $FavouritesStateCopyWith on FavouritesState {
/// Returns a callable class that can be used as follows: `instanceOfFavouritesState.copyWith(...)` or like so:`instanceOfFavouritesState.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$FavouritesStateCWProxy get copyWith => _$FavouritesStateCWProxyImpl(this);
}

View File

@@ -0,0 +1,138 @@
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';
import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/events.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/models/card.dart';
import '../details_page/details_page.dart';
import '../home_page/bloc/state.dart';
import '../home_page/cards_list.dart';
class FavouritesPage extends StatefulWidget {
const FavouritesPage({super.key});
@override
State<FavouritesPage> createState() => _FavouritesPageState();
}
class _FavouritesPageState extends State<FavouritesPage> {
final ScrollController scrollController = ScrollController();
List<String>? favouritesIds;
bool wereIdsPreviouslyLoaded = false;
String? currencyId;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
});
super.initState();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocListener<FavouritesBloc, FavouritesState>(
listener: (context, state) {
if (state.hasFavouritesLoaded && !wereIdsPreviouslyLoaded) {
wereIdsPreviouslyLoaded = true;
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(
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() {
wereIdsPreviouslyLoaded = false;
context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
return Future.value(null);
}
void _showSnackBar(BuildContext context, String title, bool isLiked) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'$title ${isLiked ? context.locale.addedToFavourite : context.locale.removedFromFavourite}',
style: Theme.of(context).textTheme.bodyLarge
),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
duration: const Duration(seconds: 2),
));
});
}
void _onLike(String? id, String title, bool isLiked) {
if (id != null) {
context.read<FavouritesBloc>().add(ChangeFavouriteEvent(id));
_showSnackBar(context, title, !isLiked);
}
}
void _navToDetails(CardData data) {
Navigator.push(context, MaterialPageRoute<void>(
builder: (context) => DetailsPage(data)),
);
}
void _onNextPage() {
final bloc = context.read<HomeBloc>();
if (!bloc.state.isPaginationLoading && !bloc.state.isAllPagesLoaded) {
bloc.add(HomeLoadFavouritesDataEvent(
ids: favouritesIds,
nextPage: bloc.state.data?.nextPage,
locale: context.locale,
currencyId: currencyId!,
));
}
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter_android_app/presentation/home_page/bloc/events.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/state.dart';
import 'package:flutter_android_app/repositories/api_interface.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final ApiInterface repo;
HomeBloc(this.repo) : super(const HomeState()) {
on<HomeLoadDataEvent>(_onLoadData);
on<HomeLoadFavouritesDataEvent>(_onLoadFavouritesData);
}
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
const int pageSize = 20;
if (event.nextPage == null) {
emit(state.copyWith(
isLoading: true,
error: 'NO_ERROR',
));
} else {
emit(state.copyWith(isPaginationLoading: true));
}
String? error;
final data = await repo.loadData(
search: event.search,
page: event.nextPage ?? 1,
pageSize: pageSize,
onError: (e) => error = e,
locale: event.locale,
currencyId: event.currencyId,
);
bool isLastPage = false;
if (data?.data != null && data!.data!.length < pageSize) {
isLastPage = true;
}
if (event.nextPage != null) {
data?.data?.insertAll(0, state.data?.data ?? []);
}
emit(state.copyWith(
isLoading: false,
isPaginationLoading: false,
data: data,
error: error,
isAllPagesLoaded: isLastPage,
));
}
Future<void> _onLoadFavouritesData(HomeLoadFavouritesDataEvent event, Emitter<HomeState> emit) async {
const int pageSize = 10;
if (event.nextPage == null) {
emit(state.copyWith(
isLoading: true,
error: 'NO_ERROR',
));
} else {
emit(state.copyWith(isPaginationLoading: true));
}
String? error;
final data = await repo.loadDataWithIds(
ids: event.ids ?? [],
page: event.nextPage ?? 1,
pageSize: pageSize,
onError: (e) => error = e,
locale: event.locale,
currencyId: event.currencyId,
);
bool isLastPage = false;
if (data?.data != null && data!.data!.length < pageSize) {
isLastPage = true;
}
if (event.nextPage != null) {
data?.data?.insertAll(0, state.data?.data ?? []);
}
emit(state.copyWith(
isLoading: false,
isPaginationLoading: false,
data: data,
error: error,
isAllPagesLoaded: isLastPage,
));
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter_android_app/components/locale/l10n/app_localizations.dart';
abstract class HomeEvent {
const HomeEvent();
}
class HomeLoadDataEvent extends HomeEvent {
final String? search;
final int? nextPage;
final AppLocale? locale;
final String currencyId;
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,
required this.currencyId,
});
}

View File

@@ -0,0 +1,41 @@
import 'package:equatable/equatable.dart';
import '../../../domain/models/home.dart';
class HomeState extends Equatable {
final HomeData? data;
final bool isLoading;
final bool isPaginationLoading;
final String? error;
final bool isAllPagesLoaded;
const HomeState({
this.data,
this.isLoading = false,
this.isPaginationLoading = false,
this.error,
this.isAllPagesLoaded = false,
});
HomeState copyWith({
HomeData? data,
bool? isLoading,
bool? isPaginationLoading,
String? error,
bool? isAllPagesLoaded,
}) => HomeState(
data: data ?? this.data,
isLoading: isLoading ?? this.isLoading,
isPaginationLoading: isPaginationLoading ?? this.isPaginationLoading,
error: error ?? this.error,
isAllPagesLoaded: isAllPagesLoaded ?? this.isAllPagesLoaded,
);
@override
List<Object?> get props => [
data,
isLoading,
isPaginationLoading,
error,
isAllPagesLoaded,
];
}

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import '../../domain/models/card.dart';
typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
class CardCrypto extends StatelessWidget {
final String id;
final String title;
final String? imageUrl;
final String currentPrice;
final String priceChange;
final OnLikeCallback onLike;
final VoidCallback? onTap;
final bool isLiked;
const CardCrypto({
super.key,
required this.id,
required this.title,
this.imageUrl,
required this.currentPrice,
required this.priceChange,
this.onLike,
this.onTap,
this.isLiked = false,
});
factory CardCrypto.fromData(
CardData data, {
OnLikeCallback onLike,
VoidCallback? onTap,
bool isLiked = false,
}) => CardCrypto(
id: data.id,
title: data.title,
imageUrl: data.imageUrl,
currentPrice: data.currentPrice,
priceChange: data.priceChange,
onLike: onLike,
onTap: onTap,
isLiked: isLiked,
);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.fromLTRB(20, 8, 20, 8),
constraints: const BoxConstraints(minHeight: 140),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black38.withOpacity(0.2),
spreadRadius: 4,
offset: const Offset(0, 5),
blurRadius: 6,
)
],
),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(15),
topLeft: Radius.circular(15),
),
child: SizedBox(
height: double.infinity,
width: 140,
child: Image.network(
imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16, top: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
),
Text(
currentPrice,
style: Theme.of(context).textTheme.bodyLarge),
],
),
),
),
Row(
children: [
const Spacer(),
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 16, 16),
child: GestureDetector(
onTap: () => onLike?.call(id, title, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: isLiked
? const Icon(
Icons.star,
color: Colors.orangeAccent,
key: ValueKey(0),
)
: const Icon(
Icons.star_border,
key: ValueKey(1),
),
),
),
),
],
)
],
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,114 @@
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';
import '../favourites_bloc/favourites_bloc.dart';
import '../favourites_bloc/favourites_state.dart';
import 'bloc/bloc.dart';
import 'bloc/state.dart';
class CardsList extends StatefulWidget {
const CardsList({
required this.onListRefresh,
this.onCardLiked,
this.onCardTapped,
this.onNextPage,
super.key,
});
final Future<void> Function() onListRefresh;
final void Function(String? id, String title, bool isLiked)? onCardLiked;
final void Function(CardData data)? onCardTapped;
final void Function()? onNextPage;
@override
State<CardsList> createState() => _CardsListState();
}
class _CardsListState extends State<CardsList> {
final ScrollController scrollController = ScrollController();
@override
void initState() {
scrollController.addListener(_onNextPageListener);
super.initState();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
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();
},
),
),
),
)
),
);
}
void _onNextPageListener() {
if (scrollController.offset >= scrollController.position.maxScrollExtent) {
widget.onNextPage?.call();
}
}
}

View File

@@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:flutter_android_app/components/extensions/context_x.dart';
import 'package:flutter_android_app/components/utils/debounce.dart';
import 'package:flutter_android_app/domain/models/card.dart';
import 'package:flutter_android_app/presentation/details_page/details_page.dart';
import 'package:flutter_android_app/presentation/favourites_page/favourites_page.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/bloc.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/events.dart';
import 'package:flutter_android_app/presentation/home_page/bloc/state.dart';
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';
class MainScaffold extends StatefulWidget {
const MainScaffold({
super.key,
this.toggleDarkMode,
required this.isDarkModeSelected,
});
final void Function()? toggleDarkMode;
final bool isDarkModeSelected;
@override
State<MainScaffold> createState() => _MainScaffoldState();
}
class _MainScaffoldState extends State<MainScaffold> {
int currentPageIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: [
Text(context.locale.mainAppBarTitle),
Text(context.locale.favouritesPageAppBarTitle),
Text(context.locale.settingsPageAppBarTitle),
][currentPageIndex],
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: IconButton(
isSelected: widget.isDarkModeSelected,
onPressed: () => widget.toggleDarkMode?.call(),
icon: const Icon(Icons.wb_sunny_outlined),
selectedIcon: const Icon(Icons.brightness_2_outlined),
),
),
],
),
bottomNavigationBar: NavigationBar(
destinations: [
NavigationDestination(icon: const Icon(Icons.home), label: context.locale.navigationHome),
NavigationDestination(icon: const Icon(Icons.favorite), label: context.locale.navigationFavourites),
NavigationDestination(icon: const Icon(Icons.settings), label: context.locale.navigationSettings),
],
selectedIndex: currentPageIndex,
onDestinationSelected: (int index) => setState(() {
currentPageIndex = index;
}),
),
body: [
const HomePage(),
const FavouritesPage(),
const SettingsPage(),
][currentPageIndex],
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final TextEditingController searchController = TextEditingController();
late String? currencyId;
@override
void initState() {
SvgObjects.init();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CurrencyBloc>().add(const LoadLocalCurrencyEvent());
context.read<FavouritesBloc>().add(const LoadFavouritesEvent());
});
super.initState();
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
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(),
),
],
)
),
);
}
void _showSnackBar(BuildContext context, String title, bool isLiked) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'$title ${isLiked ? context.locale.addedToFavourite : context.locale.removedFromFavourite}',
style: Theme.of(context).textTheme.bodyLarge
),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
duration: const Duration(seconds: 2),
));
});
}
Future<void> _onRefresh() {
context.read<HomeBloc>().add(HomeLoadDataEvent(
search: searchController.text,
locale: context.locale,
currencyId: currencyId!,
));
return Future.value(null);
}
void _onLike(String? id, String title, bool isLiked) {
if (id != null) {
context.read<FavouritesBloc>().add(ChangeFavouriteEvent(id));
_showSnackBar(context, title, !isLiked);
}
}
void _navToDetails(CardData data) {
FocusManager.instance.primaryFocus?.unfocus();
Navigator.push(context, MaterialPageRoute<void>(
builder: (context) => DetailsPage(data)),
);
}
void _onNextPage() {
final bloc = context.read<HomeBloc>();
if (!bloc.state.isPaginationLoading && !bloc.state.isAllPagesLoaded) {
bloc.add(HomeLoadDataEvent(
search: searchController.text,
nextPage: bloc.state.data?.nextPage,
locale: context.locale,
currencyId: currencyId!,
));
}
}
}

View File

@@ -0,0 +1,22 @@
import 'dart:ui';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../components/locale/l10n/app_localizations.dart';
import 'locale_events.dart';
import 'locale_state.dart';
class LocaleBloc extends Bloc<LocaleEvent, LocaleState> {
LocaleBloc(Locale defaultLocale) : super(LocaleState(currentLocale: defaultLocale)) {
on<ChangeLocaleEvent>(_onChangeLocale);
}
Future<void> _onChangeLocale(
ChangeLocaleEvent event,
Emitter<LocaleState> emit
) async {
final toChange = AppLocale.supportedLocales.firstWhere(
(loc) => loc.languageCode != state.currentLocale.languageCode);
emit(state.copyWith(currentLocale: toChange));
}
}

View File

@@ -0,0 +1,7 @@
abstract class LocaleEvent {
const LocaleEvent();
}
class ChangeLocaleEvent extends LocaleEvent {
const ChangeLocaleEvent();
}

View File

@@ -0,0 +1,16 @@
import 'dart:ui';
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:equatable/equatable.dart';
part 'locale_state.g.dart';
@CopyWith()
class LocaleState extends Equatable {
final Locale currentLocale;
const LocaleState({required this.currentLocale});
@override
List<Object?> get props => [currentLocale];
}

View File

@@ -0,0 +1,58 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'locale_state.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$LocaleStateCWProxy {
LocaleState currentLocale(Locale currentLocale);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `LocaleState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// LocaleState(...).copyWith(id: 12, name: "My name")
/// ````
LocaleState call({
Locale? currentLocale,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfLocaleState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfLocaleState.copyWith.fieldName(...)`
class _$LocaleStateCWProxyImpl implements _$LocaleStateCWProxy {
const _$LocaleStateCWProxyImpl(this._value);
final LocaleState _value;
@override
LocaleState currentLocale(Locale currentLocale) =>
this(currentLocale: currentLocale);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `LocaleState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// LocaleState(...).copyWith(id: 12, name: "My name")
/// ````
LocaleState call({
Object? currentLocale = const $CopyWithPlaceholder(),
}) {
return LocaleState(
currentLocale:
currentLocale == const $CopyWithPlaceholder() || currentLocale == null
? _value.currentLocale
// ignore: cast_nullable_to_non_nullable
: currentLocale as Locale,
);
}
}
extension $LocaleStateCopyWith on LocaleState {
/// Returns a callable class that can be used as follows: `instanceOfLocaleState.copyWith(...)` or like so:`instanceOfLocaleState.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$LocaleStateCWProxy get copyWith => _$LocaleStateCWProxyImpl(this);
}

View File

@@ -0,0 +1,78 @@
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';
import '../locale_bloc/locale_bloc.dart';
import '../locale_bloc/locale_events.dart';
import '../locale_bloc/locale_state.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 20, right: 16, top: 8),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${context.locale.settingsLanguage}:',
style: Theme.of(context).textTheme.titleMedium,
),
GestureDetector(
onTap: () => context.read<LocaleBloc>().add(const ChangeLocaleEvent()),
child: SizedBox.square(
dimension: 50,
child: Padding(
padding: const EdgeInsets.only(right: 0),
child: BlocBuilder<LocaleBloc, LocaleState>(
builder: (context, state) {
return state.currentLocale.languageCode == 'ru'
? const SvgRu()
: const SvgUs();
}),
),
),
),
],
),
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

@@ -0,0 +1,24 @@
import 'package:flutter_android_app/domain/models/home.dart';
import '../components/locale/l10n/app_localizations.dart';
import '../components/utils/error_callback.dart';
abstract class ApiInterface {
Future<HomeData?> loadData({
OnErrorCallback? onError,
String? search,
int page = 1,
int pageSize = 20,
AppLocale? locale,
String currencyId,
});
Future<HomeData?> loadDataWithIds({
OnErrorCallback? onError,
List<String> ids,
int page = 1,
int pageSize = 8,
AppLocale? locale,
String currencyId,
});
}

View File

@@ -0,0 +1,121 @@
import 'package:dio/dio.dart';
import 'package:flutter_android_app/components/locale/l10n/app_localizations.dart';
import 'package:flutter_android_app/components/utils/error_callback.dart';
import 'package:flutter_android_app/data/dtos/coins_dto.dart';
import 'package:flutter_android_app/data/dtos/search_coins_dto.dart';
import 'package:flutter_android_app/data/mappers/crypto_mapper.dart';
import 'package:flutter_android_app/repositories/api_interface.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import '../domain/models/home.dart';
class CryptoRepository extends ApiInterface {
static const String _baseUrl = 'https://api.coingecko.com/api/v3';
static const String _searchUrl = '/search';
static const String _coinsDataUrl = '/coins/markets';
static const String _apiKey = 'CG-oer6F3AAhVpNxGDxc7mjzZCo';
static final Dio _dio = Dio()
..interceptors.add(PrettyDioLogger(
requestBody: true,
requestHeader: true,
));
@override
Future<HomeData?> loadData({
OnErrorCallback? onError,
String? search,
int page = 1,
int pageSize = 20,
AppLocale? locale,
String currencyId = '',
}) async {
try {
Map<String, dynamic> queryParams = {
'x_cg_demo_api_key': _apiKey,
'vs_currency': currencyId,
'per_page': pageSize,
'page': page,
};
if (search != null && search.isNotEmpty) {
final Response<dynamic> searchResponse = await _dio.get<Map<dynamic, dynamic>>(
'$_baseUrl$_searchUrl',
queryParameters: {
'x_cg_demo_api_key': _apiKey,
'query': search,
}
);
final SearchCoinsDto searchCoinsDto = SearchCoinsDto.fromJson(searchResponse.data as Map<String, dynamic>);
if (searchCoinsDto.coins != null) {
String ids = '';
for (var coinData in searchCoinsDto.coins!) {
ids += coinData.id != null ? '${coinData.id},' : '';
}
if (ids.isEmpty) {
return HomeData();
}
queryParams['ids'] = ids;
}
}
final response = await _dio.get(
'$_baseUrl$_coinsDataUrl',
queryParameters: queryParams,
);
final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>);
final HomeData data = dto.toDomain(locale, currencyId, page);
return data;
} on DioException catch (e) {
onError?.call(e.error?.toString());
return null;
}
}
@override
Future<HomeData?> loadDataWithIds({
OnErrorCallback? onError,
List<String> ids = const [],
int page = 1,
int pageSize = 20,
AppLocale? locale,
String currencyId = '',
}) async {
try {
Map<String, dynamic> queryParams = {
'x_cg_demo_api_key': _apiKey,
'vs_currency': currencyId,
'per_page': pageSize,
'page': page,
};
String idsCommaSeparated = '';
for (var id in ids) {
idsCommaSeparated += '$id,';
}
if (ids.isEmpty) {
return HomeData();
}
queryParams['ids'] = idsCommaSeparated;
final response = await _dio.get(
'$_baseUrl$_coinsDataUrl',
queryParameters: queryParams,
);
final CoinsDto dto = CoinsDto.fromJson(response.data as List<dynamic>);
final HomeData data = dto.toDomain(locale, currencyId, page);
return data;
} on DioException catch (e) {
onError?.call(e.error?.toString());
return null;
}
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter_android_app/domain/models/card.dart';
import 'package:flutter_android_app/repositories/api_interface.dart';
import '../components/locale/l10n/app_localizations.dart';
import '../components/utils/error_callback.dart';
import '../domain/models/home.dart';
class MockRepository extends ApiInterface {
@override
Future<HomeData?> loadData({
OnErrorCallback? onError,
String? search,
int page = 1,
int pageSize = 20,
AppLocale? locale,
String currencyId = '',
}) async {
return HomeData(data: [
CardData(
id: 'bitcoin',
title: 'Bitcoin',
imageUrl: 'https://coin-images.coingecko.com/coins/images/1/large/bitcoin.png?1696501400',
currentPrice: '103233 \$',
priceChange: '+2207.71 \$ for the last 24 hours',
),
CardData(
id: 'ethereum',
title: 'Ethereum',
imageUrl: 'https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628',
currentPrice: '3900.92 \$',
priceChange: '+58.27 \$ for the last 24 hours',
),
CardData(
id: 'tether',
title: 'Tether',
imageUrl: 'https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661',
currentPrice: '1.001 \$',
priceChange: '+0.00059798 \$ for the last 24 hours',
),
]);
}
@override
Future<HomeData?> loadDataWithIds({
OnErrorCallback? onError,
List<String> ids = const [],
int page = 1,
int pageSize = 20,
AppLocale? locale,
String currencyId = '',
}) async {
return HomeData(data: [
CardData(
id: 'bitcoin',
title: 'Bitcoin',
imageUrl: 'https://coin-images.coingecko.com/coins/images/1/large/bitcoin.png?1696501400',
currentPrice: '103233 \$',
priceChange: '+2207.71 \$ for the last 24 hours',
),
CardData(
id: 'ethereum',
title: 'Ethereum',
imageUrl: 'https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628',
currentPrice: '3900.92 \$',
priceChange: '+58.27 \$ for the last 24 hours',
),
CardData(
id: 'tether',
title: 'Tether',
imageUrl: 'https://coin-images.coingecko.com/coins/images/325/large/Tether.png?1696501661',
currentPrice: '1.001 \$',
priceChange: '+0.00059798 \$ for the last 24 hours',
),
]);
}
}

View File

@@ -1,6 +1,43 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev"
source: hosted
version: "72.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev"
source: hosted
version: "6.7.0"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
@@ -9,6 +46,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@@ -17,6 +62,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
url: "https://pub.dev"
source: hosted
version: "1.1.1"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
url: "https://pub.dev"
source: hosted
version: "2.4.13"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
url: "https://pub.dev"
source: hosted
version: "7.3.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
url: "https://pub.dev"
source: hosted
version: "8.9.2"
characters:
dependency: transitive
description:
@@ -25,6 +134,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -33,6 +158,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
url: "https://pub.dev"
source: hosted
version: "4.10.0"
collection:
dependency: transitive
description:
@@ -41,6 +174,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
copy_with_extension:
dependency: "direct main"
description:
name: copy_with_extension
sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125
url: "https://pub.dev"
source: hosted
version: "5.0.4"
copy_with_extension_gen:
dependency: "direct main"
description:
name: copy_with_extension_gen
sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0"
url: "https://pub.dev"
source: hosted
version: "5.0.4"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
@@ -49,6 +214,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
url: "https://pub.dev"
source: hosted
version: "2.3.7"
dio:
dependency: "direct main"
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async:
dependency: transitive
description:
@@ -57,11 +254,51 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
url: "https://pub.dev"
source: hosted
version: "0.14.2"
flutter_lints:
dependency: "direct dev"
description:
@@ -70,11 +307,125 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_localizations:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_svg:
dependency: "direct dev"
description:
name: flutter_svg
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
url: "https://pub.dev"
source: hosted
version: "2.0.16"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http:
dependency: transitive
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image:
dependency: transitive
description:
name: image
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev"
source: hosted
version: "4.3.0"
intl:
dependency: "direct dev"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
url: "https://pub.dev"
source: hosted
version: "6.8.0"
leak_tracker:
dependency: transitive
description:
@@ -107,6 +458,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
@@ -131,6 +498,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
@@ -139,11 +530,195 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pretty_dio_logger:
dependency: "direct main"
description:
name: pretty_dio_logger
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
url: "https://pub.dev"
source: hosted
version: "1.3.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
source_span:
dependency: transitive
description:
@@ -168,6 +743,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
@@ -192,6 +775,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.2"
timing:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.15"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
url: "https://pub.dev"
source: hosted
version: "1.1.16"
vector_math:
dependency: transitive
description:
@@ -208,6 +831,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.2.5"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.24.0"

View File

@@ -36,6 +36,15 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
json_annotation: ^4.9.0
dio: ^5.7.0
pretty_dio_logger: ^1.4.0
flutter_bloc: ^8.1.6
equatable: ^2.0.7
copy_with_extension: ^5.0.4
copy_with_extension_gen: ^5.0.4
shared_preferences: ^2.3.2
dev_dependencies:
flutter_test:
sdk: flutter
@@ -47,11 +56,31 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^4.0.0
flutter_launcher_icons: ^0.14.1
flutter_svg: ^2.0.10+1
flutter_localizations:
sdk: flutter
intl: ^0.19.0
build_runner: ^2.4.13
json_serializable: ^6.8.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
flutter_icons:
android: "ic_launcher"
ios: false
image_path: "assets/launcher.jpg"
min_sdk_android: 25
# The following section is specific to Flutter packages.
flutter:
generate: true
assets:
- assets/svg/
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in