diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f43ff46
--- /dev/null
+++ b/Makefile
@@ -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
diff --git a/android/app/build.gradle b/android/app/build.gradle
index b36db2e..51f4907 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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 {
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..fb0c949 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..75cbaa9 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..6ea3a7f 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..801f688 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..4457d18 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 7bb2df6..afa1e8e 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/android/settings.gradle b/android/settings.gradle
index b9e43bd..b5e1b3f 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -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"
diff --git a/assets/launcher.jpg b/assets/launcher.jpg
new file mode 100644
index 0000000..9db76a7
Binary files /dev/null and b/assets/launcher.jpg differ
diff --git a/assets/svg/ru.svg b/assets/svg/ru.svg
new file mode 100644
index 0000000..cf24301
--- /dev/null
+++ b/assets/svg/ru.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/svg/us.svg b/assets/svg/us.svg
new file mode 100644
index 0000000..9cfd0c9
--- /dev/null
+++ b/assets/svg/us.svg
@@ -0,0 +1,9 @@
+
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..8bda731
--- /dev/null
+++ b/l10n.yaml
@@ -0,0 +1,6 @@
+arb-dir: l10n
+template-arb-file: app_ru.arb
+output-localization: app_locale.dart
+output-dir: lib/components/locale/l10n
+output-class: AppLocale
+synthetic-package: false
\ No newline at end of file
diff --git a/l10n/app_en.arb b/l10n/app_en.arb
new file mode 100644
index 0000000..3167b3d
--- /dev/null
+++ b/l10n/app_en.arb
@@ -0,0 +1,21 @@
+{
+ "@@locale": "en",
+
+ "appBarTitle": "Crypto Exchange",
+
+ "search": "Search",
+ "liked": "You liked",
+ "unliked": "Like removed from",
+ "errorOccurred": "Error occurred",
+ "noErrorMsg": "No message provided",
+ "retry": "Retry",
+ "unknown": "Unknown",
+
+ "apiYear": "Year",
+ "apiType": "Type",
+ "apiRating": "Rating",
+ "apiDesc": "",
+ "apiNoDesc": "No description provided",
+
+ "arbEnding": "t"
+}
\ No newline at end of file
diff --git a/l10n/app_ru.arb b/l10n/app_ru.arb
new file mode 100644
index 0000000..ede8c3f
--- /dev/null
+++ b/l10n/app_ru.arb
@@ -0,0 +1,21 @@
+{
+ "@@locale": "ru",
+
+ "appBarTitle": "Криптобиржа",
+
+ "search": "Поиск",
+ "liked": "Вы добавили в избранное",
+ "unliked": "Вы удалили из избранного",
+ "errorOccured": "Произошла ошибка",
+ "noErrorMsg": "Нет сообщения",
+ "retry": "Повторить",
+ "unknown": "Неизвестно",
+
+ "apiYear": "Год",
+ "apiType": "Тип",
+ "apiRating": "Рейтинг",
+ "apiDesc": "(Описание доступно только на английском языке)",
+ "apiNoDesc": "Нет описания",
+
+ "arbEnding": "t"
+}
\ No newline at end of file
diff --git a/lib/components/extensions/context_x.dart b/lib/components/extensions/context_x.dart
new file mode 100644
index 0000000..f4204a9
--- /dev/null
+++ b/lib/components/extensions/context_x.dart
@@ -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)!;
+}
diff --git a/lib/components/locale/l10n/app_localizations.dart b/lib/components/locale/l10n/app_localizations.dart
new file mode 100644
index 0000000..030d71e
--- /dev/null
+++ b/lib/components/locale/l10n/app_localizations.dart
@@ -0,0 +1,213 @@
+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, you’ll need to edit this
+/// file.
+///
+/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
+/// Then, in the Project Navigator, open the Info.plist file under the Runner
+/// project’s 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(context, AppLocale);
+ }
+
+ static const LocalizationsDelegate 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> localizationsDelegates = >[
+ delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ];
+
+ /// A list of this localizations delegate's supported locales.
+ static const List supportedLocales = [
+ Locale('en'),
+ Locale('ru')
+ ];
+
+ /// No description provided for @appBarTitle.
+ ///
+ /// In ru, this message translates to:
+ /// **'Криптобиржа'**
+ String get appBarTitle;
+
+ /// No description provided for @search.
+ ///
+ /// In ru, this message translates to:
+ /// **'Поиск'**
+ String get search;
+
+ /// No description provided for @liked.
+ ///
+ /// In ru, this message translates to:
+ /// **'Вы добавили в избранное'**
+ String get liked;
+
+ /// No description provided for @unliked.
+ ///
+ /// In ru, this message translates to:
+ /// **'Вы удалили из избранного'**
+ String get unliked;
+
+ /// No description provided for @errorOccured.
+ ///
+ /// In ru, this message translates to:
+ /// **'Произошла ошибка'**
+ String get errorOccured;
+
+ /// No description provided for @noErrorMsg.
+ ///
+ /// In ru, this message translates to:
+ /// **'Нет сообщения'**
+ String get noErrorMsg;
+
+ /// No description provided for @retry.
+ ///
+ /// In ru, this message translates to:
+ /// **'Повторить'**
+ String get retry;
+
+ /// No description provided for @unknown.
+ ///
+ /// In ru, this message translates to:
+ /// **'Неизвестно'**
+ String get unknown;
+
+ /// No description provided for @apiYear.
+ ///
+ /// In ru, this message translates to:
+ /// **'Год'**
+ String get apiYear;
+
+ /// No description provided for @apiType.
+ ///
+ /// In ru, this message translates to:
+ /// **'Тип'**
+ String get apiType;
+
+ /// No description provided for @apiRating.
+ ///
+ /// In ru, this message translates to:
+ /// **'Рейтинг'**
+ String get apiRating;
+
+ /// No description provided for @apiDesc.
+ ///
+ /// In ru, this message translates to:
+ /// **'(Описание доступно только на английском языке)'**
+ String get apiDesc;
+
+ /// No description provided for @apiNoDesc.
+ ///
+ /// In ru, this message translates to:
+ /// **'Нет описания'**
+ String get apiNoDesc;
+
+ /// No description provided for @arbEnding.
+ ///
+ /// In ru, this message translates to:
+ /// **'t'**
+ String get arbEnding;
+}
+
+class _AppLocaleDelegate extends LocalizationsDelegate {
+ const _AppLocaleDelegate();
+
+ @override
+ Future load(Locale locale) {
+ return SynchronousFuture(lookupAppLocale(locale));
+ }
+
+ @override
+ bool isSupported(Locale locale) => ['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.'
+ );
+}
diff --git a/lib/components/locale/l10n/app_localizations_en.dart b/lib/components/locale/l10n/app_localizations_en.dart
new file mode 100644
index 0000000..4d7f100
--- /dev/null
+++ b/lib/components/locale/l10n/app_localizations_en.dart
@@ -0,0 +1,50 @@
+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 appBarTitle => 'Crypto Exchange';
+
+ @override
+ String get search => 'Search';
+
+ @override
+ String get liked => 'You liked';
+
+ @override
+ String get unliked => 'Like removed from';
+
+ @override
+ String get errorOccured => 'Произошла ошибка';
+
+ @override
+ String get noErrorMsg => 'No message provided';
+
+ @override
+ String get retry => 'Retry';
+
+ @override
+ String get unknown => 'Unknown';
+
+ @override
+ String get apiYear => 'Year';
+
+ @override
+ String get apiType => 'Type';
+
+ @override
+ String get apiRating => 'Rating';
+
+ @override
+ String get apiDesc => '';
+
+ @override
+ String get apiNoDesc => 'No description provided';
+
+ @override
+ String get arbEnding => 't';
+}
diff --git a/lib/components/locale/l10n/app_localizations_ru.dart b/lib/components/locale/l10n/app_localizations_ru.dart
new file mode 100644
index 0000000..d3f54e9
--- /dev/null
+++ b/lib/components/locale/l10n/app_localizations_ru.dart
@@ -0,0 +1,50 @@
+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 appBarTitle => 'Криптобиржа';
+
+ @override
+ String get search => 'Поиск';
+
+ @override
+ String get liked => 'Вы добавили в избранное';
+
+ @override
+ String get unliked => 'Вы удалили из избранного';
+
+ @override
+ String get errorOccured => 'Произошла ошибка';
+
+ @override
+ String get noErrorMsg => 'Нет сообщения';
+
+ @override
+ String get retry => 'Повторить';
+
+ @override
+ String get unknown => 'Неизвестно';
+
+ @override
+ String get apiYear => 'Год';
+
+ @override
+ String get apiType => 'Тип';
+
+ @override
+ String get apiRating => 'Рейтинг';
+
+ @override
+ String get apiDesc => '(Описание доступно только на английском языке)';
+
+ @override
+ String get apiNoDesc => 'Нет описания';
+
+ @override
+ String get arbEnding => 't';
+}
diff --git a/lib/components/resources.g.dart b/lib/components/resources.g.dart
new file mode 100644
index 0000000..915a8d9
--- /dev/null
+++ b/lib/components/resources.g.dart
@@ -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';
+}
diff --git a/lib/data/mappers/characters_mapper.dart b/lib/data/mappers/characters_mapper.dart
index a5fed1e..e6c28bb 100644
--- a/lib/data/mappers/characters_mapper.dart
+++ b/lib/data/mappers/characters_mapper.dart
@@ -17,6 +17,7 @@ extension CharacterDataDtoToModel on CharacterDataDto {
title: attributes?.name ?? 'UNKNOWN',
imageUrl: attributes?.image ?? _imagePlaceholder,
description: _makeDescriptionText(attributes?.born, attributes?.died),
+ id: id,
);
}
diff --git a/lib/domain/models/card.dart b/lib/domain/models/card.dart
index 1132fd0..e7a9b62 100644
--- a/lib/domain/models/card.dart
+++ b/lib/domain/models/card.dart
@@ -5,11 +5,13 @@ class CardData {
final String description;
final IconData icon;
final String? imageUrl;
+ final String? id;
CardData({
required this.title,
required this.description,
this.icon = Icons.adb,
this.imageUrl,
+ this.id,
});
}
diff --git a/lib/main.dart b/lib/main.dart
index eb214ab..a91fc51 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,9 +1,16 @@
+import 'dart:io';
+
import 'package:flutter/material.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/like_bloc/like_bloc.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/potter_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'components/locale/l10n/app_localizations.dart';
+
void main() {
runApp(const MyApp());
}
@@ -13,19 +20,32 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
- useMaterial3: true,
- ),
- home: RepositoryProvider(
- lazy: true,
- create: (_) => PotterRepository(),
- child: BlocProvider(
- lazy: false,
- create: (context) => HomeBloc(context.read()),
- child: const MyHomePage(title: 'Harry Potter characters'),
+ return BlocProvider(
+ lazy: false,
+ create: (context) => LikeBloc(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => LocaleBloc(Locale(Platform.localeName)),
+ child: BlocBuilder(
+ builder: (context, state) => MaterialApp(
+ title: 'Flutter Demo',
+ locale: state.currentLocale,
+ localizationsDelegates: AppLocale.localizationsDelegates,
+ supportedLocales: AppLocale.supportedLocales,
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
+ useMaterial3: true,
+ ),
+ home: RepositoryProvider(
+ lazy: true,
+ create: (_) => PotterRepository(),
+ child: BlocProvider(
+ lazy: false,
+ create: (context) => HomeBloc(context.read()),
+ child: const MyHomePage(title: 'Harry Potter characters'),
+ ),
+ ),
+ ),
),
),
);
diff --git a/lib/presentation/common/svg_objects.dart b/lib/presentation/common/svg_objects.dart
new file mode 100644
index 0000000..1c3292c
--- /dev/null
+++ b/lib/presentation/common/svg_objects.dart
@@ -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 = [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);
+ }
+}
diff --git a/lib/presentation/home_page/card.dart b/lib/presentation/home_page/card.dart
index 5edb47d..f148566 100644
--- a/lib/presentation/home_page/card.dart
+++ b/lib/presentation/home_page/card.dart
@@ -1,49 +1,53 @@
part of 'home_page.dart';
-typedef OnLikeCallback = void Function(String title, bool isLiked)?;
+typedef OnLikeCallback = void Function(String? id, String title, bool isLiked)?;
-class _Card extends StatefulWidget {
+class _Card extends StatelessWidget {
+ final AppLocale locale;
final String title;
final String description;
final IconData icon;
final String? imageUrl;
final OnLikeCallback onLike;
final VoidCallback? onTap;
+ final String? id;
+ final bool isLiked;
const _Card({
super.key,
+ required this.locale,
required this.title,
required this.description,
this.icon = Icons.hail,
this.imageUrl,
this.onLike,
this.onTap,
+ this.id,
+ this.isLiked = false,
});
factory _Card.fromData(
+ AppLocale locale,
CardData data, {
- final OnLikeCallback onLike,
+ OnLikeCallback onLike,
VoidCallback? onTap,
+ bool isLiked = false,
}) => _Card(
+ locale: locale,
title: data.title,
description: data.description,
icon: data.icon,
imageUrl: data.imageUrl,
onLike: onLike,
onTap: onTap,
+ isLiked: isLiked,
+ id: data.id,
);
- @override
- State<_Card> createState() => _CardState();
-}
-
-class _CardState extends State<_Card> {
- bool isLiked = false;
-
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: widget.onTap,
+ onTap: onTap,
child: Container(
margin: const EdgeInsets.fromLTRB(20, 8, 20, 8),
constraints: const BoxConstraints(minHeight: 140),
@@ -73,7 +77,7 @@ class _CardState extends State<_Card> {
height: double.infinity,
width: 100,
child: Image.network(
- widget.imageUrl ?? '',
+ imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
@@ -86,12 +90,12 @@ class _CardState extends State<_Card> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- widget.title,
+ title,
style: Theme.of(context).textTheme.headlineLarge,
),
Text(
- widget.description,
- style: Theme.of(context).textTheme.bodyLarge),
+ description,
+ style: Theme.of(context).textTheme.bodyLarge),
],
),
),
@@ -101,24 +105,19 @@ class _CardState extends State<_Card> {
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 16, 16),
child: GestureDetector(
- onTap: () {
- setState(() {
- isLiked = !isLiked;
- });
- widget.onLike?.call(widget.title, isLiked);
- },
+ onTap: () => onLike?.call(id, title, isLiked),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: isLiked
- ? const Icon(
- Icons.favorite,
- color: Colors.redAccent,
- key: ValueKey(0),
- )
- : const Icon(
- Icons.favorite_border,
- key: ValueKey(1),
- ),
+ ? const Icon(
+ Icons.favorite,
+ color: Colors.redAccent,
+ key: ValueKey(0),
+ )
+ : const Icon(
+ Icons.favorite_border,
+ key: ValueKey(1),
+ ),
),
),
),
diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart
index b824c11..7be7b96 100644
--- a/lib/presentation/home_page/home_page.dart
+++ b/lib/presentation/home_page/home_page.dart
@@ -1,13 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_android_app/components/extensions/context_x.dart';
+import 'package:flutter_android_app/components/locale/l10n/app_localizations.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/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/like_bloc/like_bloc.dart';
+import 'package:flutter_android_app/presentation/like_bloc/like_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import '../common/svg_objects.dart';
+import '../like_bloc/like_events.dart';
+import '../locale_bloc/locale_bloc.dart';
+import '../locale_bloc/locale_events.dart';
+import '../locale_bloc/locale_state.dart';
+
part 'card.dart';
class MyHomePage extends StatefulWidget {
@@ -45,8 +55,11 @@ class _BodyState extends State {
@override
void initState() {
+ SvgObjects.init();
+
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().add(const HomeLoadDataEvent());
+ context.read().add(const LoadLikesEvent());
});
scrollController.addListener(_onNextPageListener);
@@ -60,14 +73,36 @@ class _BodyState extends State {
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column(
children: [
- Padding(
- padding: const EdgeInsets.all(12),
- child: CupertinoSearchTextField(
- controller: searchController,
- onChanged: (search) {
- Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search)));
- },
- ),
+ Row(
+ children: [
+ Expanded(
+ flex: 4,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: CupertinoSearchTextField(
+ controller: searchController,
+ onChanged: (search) {
+ Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search)));
+ },
+ ),
+ ),
+ ),
+ GestureDetector(
+ onTap: () => context.read().add(const ChangeLocaleEvent()),
+ child: SizedBox.square(
+ dimension: 50,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return state.currentLocale.languageCode == 'ru'
+ ? const SvgRu()
+ : const SvgUs();
+ }),
+ ),
+ ),
+ ),
+ ],
),
BlocBuilder(
builder: (context, state) => state.error != null
@@ -77,25 +112,29 @@ class _BodyState extends State {
)
: state.isLoading
? const CircularProgressIndicator()
- : Expanded(
- child: RefreshIndicator(
- onRefresh: _onRefresh,
- 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
- ? _Card.fromData(
- data,
- onLike: (title, isLiked) => _showSnackBar(context, title, isLiked),
- onTap: () => _navToDetails(context, data),
- )
- : const SizedBox.shrink();
- },
+ : BlocBuilder(
+ builder: (context, likeState) => Expanded(
+ child: RefreshIndicator(
+ onRefresh: _onRefresh,
+ 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
+ ? _Card.fromData(
+ context.locale,
+ data,
+ isLiked: likeState.likedIds?.contains(data.id) == true,
+ onLike: _onLike,
+ onTap: () => _navToDetails(context, data),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ ),
),
- ),
)
),
BlocBuilder(
@@ -120,7 +159,7 @@ class _BodyState extends State {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
- 'Card $title ${isLiked ? 'liked' : 'disliked'}',
+ '$title ${isLiked ? context.locale.liked : context.locale.unliked}',
style: Theme.of(context).textTheme.bodyLarge
),
backgroundColor: Colors.deepPurple.shade200,
@@ -149,4 +188,11 @@ class _BodyState extends State {
}
}
}
+
+ void _onLike(String? id, String title, bool isLiked) {
+ if (id != null) {
+ context.read().add(ChangeLikeEvent(id));
+ _showSnackBar(context, title, !isLiked);
+ }
+ }
}
diff --git a/lib/presentation/like_bloc/like_bloc.dart b/lib/presentation/like_bloc/like_bloc.dart
new file mode 100644
index 0000000..7f982f8
--- /dev/null
+++ b/lib/presentation/like_bloc/like_bloc.dart
@@ -0,0 +1,39 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'like_events.dart';
+import 'like_state.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+const String _likedPrefsKey = 'liked';
+
+class LikeBloc extends Bloc {
+ LikeBloc() : super(const LikeState(likedIds: [])) {
+ on(_onChangeLike);
+ on(_onLoadLikes);
+ }
+
+ Future _onLoadLikes(
+ LoadLikesEvent event, Emitter emit
+ ) async {
+ final prefs = await SharedPreferences.getInstance();
+ final data = prefs.getStringList(_likedPrefsKey);
+
+ emit(state.copyWith(likedIds: data));
+ }
+
+ Future _onChangeLike(
+ ChangeLikeEvent event, Emitter emit
+ ) async {
+ final updatedList = List.from(state.likedIds ?? []);
+
+ 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(likedIds: updatedList));
+ }
+}
diff --git a/lib/presentation/like_bloc/like_events.dart b/lib/presentation/like_bloc/like_events.dart
new file mode 100644
index 0000000..938bce0
--- /dev/null
+++ b/lib/presentation/like_bloc/like_events.dart
@@ -0,0 +1,12 @@
+abstract class LikeEvent {
+ const LikeEvent();
+}
+
+class LoadLikesEvent extends LikeEvent {
+ const LoadLikesEvent();
+}
+
+class ChangeLikeEvent extends LikeEvent {
+ final String id;
+ const ChangeLikeEvent(this.id);
+}
diff --git a/lib/presentation/like_bloc/like_state.dart b/lib/presentation/like_bloc/like_state.dart
new file mode 100644
index 0000000..bb9d50a
--- /dev/null
+++ b/lib/presentation/like_bloc/like_state.dart
@@ -0,0 +1,14 @@
+import 'package:copy_with_extension/copy_with_extension.dart';
+import 'package:equatable/equatable.dart';
+
+part 'like_state.g.dart';
+
+@CopyWith()
+class LikeState extends Equatable {
+ final List? likedIds;
+
+ const LikeState({this.likedIds});
+
+ @override
+ List