From b36f303f0a753d29526d7939851acf928a59d4fe Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 12:38:44 -0300 Subject: [PATCH 01/91] fix: removed widget_test --- test/widget_test.dart | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From e47ad0ab0b76bc8821c1fa94d2bf307f90dedad1 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 12:39:12 -0300 Subject: [PATCH 02/91] chore: fvm --- .fvm/fvm_config.json | 3 +-- .fvmrc | 4 ++++ .gitignore | 4 +++- .vscode/settings.json | 14 +++++++------- pubspec.lock | 32 ++++++++++++++++---------------- 5 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 .fvmrc diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..e7b55eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.22.3", - "flavors": {} + "flutterSdkVersion": "3.22.3" } \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..e03e940 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.22.3", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d87..7040cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..c959187 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/versions/3.22.3", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f95a63e..493631e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -255,10 +255,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: @@ -279,10 +279,10 @@ packages: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -367,10 +367,10 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.6" package_config: dependency: transitive description: @@ -423,10 +423,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -476,10 +476,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -508,10 +508,10 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: @@ -556,10 +556,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" yaml: dependency: transitive description: From 05b1ab0175652be70a43c05c9334209853e3d55b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 12:44:49 -0300 Subject: [PATCH 03/91] chore: add flutter_bloc as dependency --- pubspec.lock | 32 ++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 2 files changed, 33 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 493631e..b0bc69d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,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: @@ -206,6 +214,14 @@ packages: 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_lints: dependency: "direct dev" description: @@ -371,6 +387,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -395,6 +419,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..a655055 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: sdk: flutter http: ^1.2.2 json_annotation: ^4.9.0 + flutter_bloc: ^8.1.6 dev_dependencies: flutter_test: From 68ff962b2b193ccd5771a80f5881fec1ff3471a4 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 14:29:43 -0300 Subject: [PATCH 04/91] feat: add appbar widget and restaurant tour page initial layout --- .../restaurant_tour/restaurant_tour_page.dart | 26 ++++++++++ .../restaurant_tour/widgets/app_bar.dart | 51 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 lib/ui/pages/restaurant_tour/restaurant_tour_page.dart create mode 100644 lib/ui/pages/restaurant_tour/widgets/app_bar.dart diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart new file mode 100644 index 0000000..59f4f4e --- /dev/null +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../../typography.dart'; + +part 'widgets/app_bar.dart'; + +class RestaurantTourPage extends StatelessWidget { + const RestaurantTourPage({super.key}); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Scaffold( + appBar: _AppBar(), + body: TabBarView( + physics: NeverScrollableScrollPhysics(), + children: [ + Center(child: Text('All Restaurants Content')), + Center(child: Text('My Favorites Content')), + ], + ), + ), + ); + } +} diff --git a/lib/ui/pages/restaurant_tour/widgets/app_bar.dart b/lib/ui/pages/restaurant_tour/widgets/app_bar.dart new file mode 100644 index 0000000..83f08f6 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/app_bar.dart @@ -0,0 +1,51 @@ +part of '../restaurant_tour_page.dart'; + +class _AppBar extends StatefulWidget implements PreferredSizeWidget { + const _AppBar() : preferredSize = const Size.fromHeight(kToolbarHeight + 32); + + @override + final Size preferredSize; + + @override + _AppBarState createState() => _AppBarState(); +} + +class _AppBarState extends State<_AppBar> { + @override + Widget build(BuildContext context) { + return AppBar( + title: const Text('RestauranTour', style: AppTextStyles.loraRegularHeadline), + centerTitle: true, + elevation: 0, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: Material( + elevation: 3, + color: Colors.white, + shadowColor: Colors.black.withOpacity(0.4), + child: TabBar( + tabAlignment: TabAlignment.center, + splashFactory: NoSplash.splashFactory, + overlayColor: const WidgetStatePropertyAll(Colors.transparent), + indicatorColor: Colors.black, + labelStyle: AppTextStyles.openRegularTitleSemiBold, + isScrollable: true, + unselectedLabelStyle: AppTextStyles.openRegularTitleSemiBold.copyWith(color: const Color(0xFF606060)), + indicator: const UnderlineTabIndicator( + borderSide: BorderSide( + width: 2.0, + color: Colors.black, + ), + insets: EdgeInsets.symmetric(horizontal: 0), + ), + labelPadding: const EdgeInsets.symmetric(horizontal: 12), + tabs: const [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), + ), + ), + ); + } +} From 81222745704b82f7ae525e524949fada2c9143de Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 14:40:30 -0300 Subject: [PATCH 05/91] feat: add circular loading --- lib/ui/widgets/circular_loading.dart | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 lib/ui/widgets/circular_loading.dart diff --git a/lib/ui/widgets/circular_loading.dart b/lib/ui/widgets/circular_loading.dart new file mode 100644 index 0000000..e06b13e --- /dev/null +++ b/lib/ui/widgets/circular_loading.dart @@ -0,0 +1,73 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class CircularLoading extends StatefulWidget { + const CircularLoading({super.key}); + + @override + CircularLoadingState createState() => CircularLoadingState(); +} + +class CircularLoadingState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: _LoadingPainter(_controller.value), + child: const SizedBox( + width: 36, + height: 36, + ), + ); + }, + ); + } +} + +class _LoadingPainter extends CustomPainter { + final double progress; + + _LoadingPainter(this.progress); + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = Colors.black + ..strokeWidth = 4.0 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + const double startAngle = -pi / 2; + const double sweepAngle = 2 * pi * 0.75; + final Rect rect = Rect.fromLTWH(0, 0, size.width, size.height); + + final double rotateAngle = 2 * pi * progress; + + canvas.drawArc(rect, startAngle + rotateAngle, sweepAngle, false, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} From 930013dae70dfcb02f18db7f82348ee307f4fc7d Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 14:40:59 -0300 Subject: [PATCH 06/91] feat: add widgets file to export circular loading --- lib/ui/widgets/widgets.dart | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/ui/widgets/widgets.dart diff --git a/lib/ui/widgets/widgets.dart b/lib/ui/widgets/widgets.dart new file mode 100644 index 0000000..59ebe56 --- /dev/null +++ b/lib/ui/widgets/widgets.dart @@ -0,0 +1 @@ +export 'circular_loading.dart'; From dd9df92ea3afba1203035b65caf97649ec4644fc Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 15:34:18 -0300 Subject: [PATCH 07/91] feat: add restaurant item widget --- lib/ui/widgets/restaurant_item.dart | 109 ++++++++++++++++++++++++++++ lib/ui/widgets/widgets.dart | 1 + 2 files changed, 110 insertions(+) create mode 100644 lib/ui/widgets/restaurant_item.dart diff --git a/lib/ui/widgets/restaurant_item.dart b/lib/ui/widgets/restaurant_item.dart new file mode 100644 index 0000000..97a089c --- /dev/null +++ b/lib/ui/widgets/restaurant_item.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import '../../typography.dart'; + +class RestaurantItem extends StatelessWidget { + const RestaurantItem({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: const [ + BoxShadow( + color: Color(0x1F000000), + offset: Offset(0, 1), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ), + child: Row( + children: [ + Container( + height: 88, + width: 88, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 12), + const Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Restaurant Name Goes Here And Wraps 2 Lines', style: AppTextStyles.loraRegularTitle), + SizedBox(height: 4), + Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _Stars(5), + _Status(true), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _Stars extends StatelessWidget { + final int _quantity; + + const _Stars(this._quantity); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + for (int i = 0; i < _quantity; i++) const _StarIcon(), + ], + ); + } +} + +class _StarIcon extends StatelessWidget { + const _StarIcon(); + + @override + Widget build(BuildContext context) { + return const Icon(Icons.star, color: Color(0xFFFFB800), size: 12); + } +} + +class _Status extends StatelessWidget { + final bool _isOpen; + + const _Status(this._isOpen); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(_isOpen ? 'Open now' : 'Closed', style: AppTextStyles.openRegularItalic), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: 2), + child: SizedBox( + height: 8, + width: 8, + child: CircleAvatar(backgroundColor: _isOpen ? const Color(0XFF5CD313) : const Color(0XFFEA5E5E)), + ), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/widgets.dart b/lib/ui/widgets/widgets.dart index 59ebe56..7a46983 100644 --- a/lib/ui/widgets/widgets.dart +++ b/lib/ui/widgets/widgets.dart @@ -1 +1,2 @@ export 'circular_loading.dart'; +export 'restaurant_item.dart'; From d11cf579411e65dc32ae1122499b563a82b05747 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 19:48:50 -0300 Subject: [PATCH 08/91] feat: add star icon widget --- lib/ui/widgets/star_icon.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/ui/widgets/star_icon.dart diff --git a/lib/ui/widgets/star_icon.dart b/lib/ui/widgets/star_icon.dart new file mode 100644 index 0000000..9fa5fb3 --- /dev/null +++ b/lib/ui/widgets/star_icon.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class StarIcon extends StatelessWidget { + const StarIcon({super.key}); + + @override + Widget build(BuildContext context) { + return const Icon(Icons.star, color: Color(0xFFFFB800), size: 12); + } +} From 7af2a77ad770ff2150d8d1a3f6d9c0b5549cf846 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 19:49:03 -0300 Subject: [PATCH 09/91] feat: add status widget --- lib/ui/widgets/status.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/ui/widgets/status.dart diff --git a/lib/ui/widgets/status.dart b/lib/ui/widgets/status.dart new file mode 100644 index 0000000..4346615 --- /dev/null +++ b/lib/ui/widgets/status.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import '../../typography.dart'; + +class Status extends StatelessWidget { + final bool _isOpen; + + const Status(this._isOpen, {super.key}); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(_isOpen ? 'Open now' : 'Closed', style: AppTextStyles.openRegularItalic), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: 2), + child: SizedBox( + height: 8, + width: 8, + child: CircleAvatar(backgroundColor: _isOpen ? const Color(0XFF5CD313) : const Color(0XFFEA5E5E)), + ), + ), + ], + ); + } +} From 1f50bdfbd1808440c55e65fdce08bfc0b2a46bf8 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 19:49:15 -0300 Subject: [PATCH 10/91] feat: add starts widget --- lib/ui/widgets/stars.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/ui/widgets/stars.dart diff --git a/lib/ui/widgets/stars.dart b/lib/ui/widgets/stars.dart new file mode 100644 index 0000000..b88c71a --- /dev/null +++ b/lib/ui/widgets/stars.dart @@ -0,0 +1,18 @@ +import 'package:flutter/widgets.dart'; + +import 'widgets.dart'; + +class Stars extends StatelessWidget { + final int _quantity; + + const Stars(this._quantity, {super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + for (int i = 0; i < _quantity; i++) const StarIcon(), + ], + ); + } +} From 881aa0728519bdce72a25652c3a08159b5ea5773 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 19:50:01 -0300 Subject: [PATCH 11/91] feat: move restaurant item to restaurant tour context --- .../restaurant_tour/restaurant_tour_page.dart | 18 ++- .../pages/restaurant_tour/widgets/image.dart | 20 ++++ .../widgets/restaurant_item.dart | 53 +++++++++ lib/ui/widgets/restaurant_item.dart | 109 ------------------ lib/ui/widgets/widgets.dart | 4 +- 5 files changed, 89 insertions(+), 115 deletions(-) create mode 100644 lib/ui/pages/restaurant_tour/widgets/image.dart create mode 100644 lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart delete mode 100644 lib/ui/widgets/restaurant_item.dart diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 59f4f4e..1954fec 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,23 +1,31 @@ import 'package:flutter/material.dart'; import '../../../typography.dart'; +import '../../widgets/widgets.dart'; part 'widgets/app_bar.dart'; +part 'widgets/image.dart'; +part 'widgets/restaurant_item.dart'; class RestaurantTourPage extends StatelessWidget { const RestaurantTourPage({super.key}); @override Widget build(BuildContext context) { - return const DefaultTabController( + return DefaultTabController( length: 2, child: Scaffold( - appBar: _AppBar(), + appBar: const _AppBar(), body: TabBarView( - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), children: [ - Center(child: Text('All Restaurants Content')), - Center(child: Text('My Favorites Content')), + ListView( + padding: const EdgeInsets.all(16), + children: const [ + _RestaurantItem(), + ], + ), + const Center(child: Text('My Favorites Content')), ], ), ), diff --git a/lib/ui/pages/restaurant_tour/widgets/image.dart b/lib/ui/pages/restaurant_tour/widgets/image.dart new file mode 100644 index 0000000..08cc67a --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/image.dart @@ -0,0 +1,20 @@ +part of '../restaurant_tour_page.dart'; + +class _Image extends StatelessWidget { + final String _url; + + const _Image(this._url); + + @override + Widget build(BuildContext context) { + return Container( + height: 88, + width: 88, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(8), + ), + child: Text(_url), + ); + } +} diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart new file mode 100644 index 0000000..71f90fd --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart @@ -0,0 +1,53 @@ +part of '../restaurant_tour_page.dart'; + +class _RestaurantItem extends StatelessWidget { + const _RestaurantItem(); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.pushNamed(context, '/detail'); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: const [ + BoxShadow( + color: Color(0x1F000000), + offset: Offset(0, 1), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ), + child: const Row( + children: [ + _Image(''), + SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Restaurant Name Goes Here And Wraps 2 Lines', style: AppTextStyles.loraRegularTitle), + SizedBox(height: 4), + Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Stars(5), + Status(true), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/restaurant_item.dart b/lib/ui/widgets/restaurant_item.dart deleted file mode 100644 index 97a089c..0000000 --- a/lib/ui/widgets/restaurant_item.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../typography.dart'; - -class RestaurantItem extends StatelessWidget { - const RestaurantItem({super.key}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () {}, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: const [ - BoxShadow( - color: Color(0x1F000000), - offset: Offset(0, 1), - blurRadius: 5, - spreadRadius: 0, - ), - ], - ), - child: Row( - children: [ - Container( - height: 88, - width: 88, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(8), - ), - ), - const SizedBox(width: 12), - const Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Restaurant Name Goes Here And Wraps 2 Lines', style: AppTextStyles.loraRegularTitle), - SizedBox(height: 4), - Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), - SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _Stars(5), - _Status(true), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _Stars extends StatelessWidget { - final int _quantity; - - const _Stars(this._quantity); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - for (int i = 0; i < _quantity; i++) const _StarIcon(), - ], - ); - } -} - -class _StarIcon extends StatelessWidget { - const _StarIcon(); - - @override - Widget build(BuildContext context) { - return const Icon(Icons.star, color: Color(0xFFFFB800), size: 12); - } -} - -class _Status extends StatelessWidget { - final bool _isOpen; - - const _Status(this._isOpen); - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(_isOpen ? 'Open now' : 'Closed', style: AppTextStyles.openRegularItalic), - const SizedBox(width: 8), - Padding( - padding: const EdgeInsets.only(top: 2), - child: SizedBox( - height: 8, - width: 8, - child: CircleAvatar(backgroundColor: _isOpen ? const Color(0XFF5CD313) : const Color(0XFFEA5E5E)), - ), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/widgets.dart b/lib/ui/widgets/widgets.dart index 7a46983..e5427ff 100644 --- a/lib/ui/widgets/widgets.dart +++ b/lib/ui/widgets/widgets.dart @@ -1,2 +1,4 @@ export 'circular_loading.dart'; -export 'restaurant_item.dart'; +export 'star_icon.dart'; +export 'stars.dart'; +export 'status.dart'; From 36f426bbc550facd13de26876651ea61a49a1db1 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 19:50:23 -0300 Subject: [PATCH 12/91] feat: add restaurant detail initial layout --- .../restaurant_detail_page.dart | 68 +++++++++++++++++++ .../widgets/arrow_back_icon.dart | 13 ++++ .../restaurant_detail/widgets/divider.dart | 15 ++++ .../widgets/favorite_icon.dart | 20 ++++++ .../restaurant_detail/widgets/rating.dart | 22 ++++++ .../widgets/review_item.dart | 34 ++++++++++ 6 files changed, 172 insertions(+) create mode 100644 lib/ui/pages/restaurant_detail/restaurant_detail_page.dart create mode 100644 lib/ui/pages/restaurant_detail/widgets/arrow_back_icon.dart create mode 100644 lib/ui/pages/restaurant_detail/widgets/divider.dart create mode 100644 lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart create mode 100644 lib/ui/pages/restaurant_detail/widgets/rating.dart create mode 100644 lib/ui/pages/restaurant_detail/widgets/review_item.dart diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart new file mode 100644 index 0000000..ef77564 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../../../typography.dart'; +import '../../widgets/widgets.dart'; + +part 'widgets/arrow_back_icon.dart'; +part 'widgets/divider.dart'; +part 'widgets/favorite_icon.dart'; +part 'widgets/rating.dart'; +part 'widgets/review_item.dart'; + +class RestaurantDetailPage extends StatelessWidget { + const RestaurantDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: const _ArrowBackIcon(), + actions: [ + _FavoriteIcon( + onFavorite: () {}, + ), + ], + title: const Text( + 'Restaurant Name Goes Here And Wraps 2 Lines', + style: AppTextStyles.loraRegularHeadline, + ), + ), + body: ListView( + children: [ + Container( + height: 360, + color: Colors.red, + ), + const Padding( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + Status(true), + ], + ), + _Divider(), + Text('Address', style: AppTextStyles.openRegularText), + SizedBox(height: 24), + Text('102 Lakeside Ave\nSeattle, WA 98122', style: AppTextStyles.openRegularTitleSemiBold), + _Divider(), + Text('Overall Rating', style: AppTextStyles.openRegularText), + SizedBox(height: 16), + _Rating(), + _Divider(), + Text('42 reviews', style: AppTextStyles.openRegularText), + SizedBox(height: 16), + _ReviewItem(), + _ReviewItem(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/pages/restaurant_detail/widgets/arrow_back_icon.dart b/lib/ui/pages/restaurant_detail/widgets/arrow_back_icon.dart new file mode 100644 index 0000000..0d4e897 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/arrow_back_icon.dart @@ -0,0 +1,13 @@ +part of '../restaurant_detail_page.dart'; + +class _ArrowBackIcon extends StatelessWidget { + const _ArrowBackIcon(); + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: const Icon(Icons.arrow_back), + onTap: () => Navigator.pop(context), + ); + } +} diff --git a/lib/ui/pages/restaurant_detail/widgets/divider.dart b/lib/ui/pages/restaurant_detail/widgets/divider.dart new file mode 100644 index 0000000..834ec30 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/divider.dart @@ -0,0 +1,15 @@ +part of '../restaurant_detail_page.dart'; + +class _Divider extends StatelessWidget { + final double verticalPadding; + + const _Divider({this.verticalPadding = 24}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: verticalPadding), + child: const Divider(color: Color(0xFFEEEEEE)), + ); + } +} diff --git a/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart new file mode 100644 index 0000000..4dd8e6e --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart @@ -0,0 +1,20 @@ +part of '../restaurant_detail_page.dart'; + +class _FavoriteIcon extends StatelessWidget { + final VoidCallback _onFavorite; + + const _FavoriteIcon({ + required VoidCallback onFavorite, + }) : _onFavorite = onFavorite; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 16), + child: GestureDetector( + onDoubleTap: _onFavorite, + child: const Icon(Icons.favorite_border), + ), + ); + } +} diff --git a/lib/ui/pages/restaurant_detail/widgets/rating.dart b/lib/ui/pages/restaurant_detail/widgets/rating.dart new file mode 100644 index 0000000..1ca7e80 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/rating.dart @@ -0,0 +1,22 @@ +part of '../restaurant_detail_page.dart'; + +class _Rating extends StatelessWidget { + const _Rating(); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '4.6', + style: AppTextStyles.loraRegularHeadline.copyWith(fontSize: 28), + ), + const Padding( + padding: EdgeInsets.only(bottom: 8), + child: StarIcon(), + ), + ], + ); + } +} diff --git a/lib/ui/pages/restaurant_detail/widgets/review_item.dart b/lib/ui/pages/restaurant_detail/widgets/review_item.dart new file mode 100644 index 0000000..2b19438 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/review_item.dart @@ -0,0 +1,34 @@ +part of '../restaurant_detail_page.dart'; + +class _ReviewItem extends StatelessWidget { + const _ReviewItem(); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Stars(4), + SizedBox(height: 8), + Text( + 'Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.', + style: AppTextStyles.openRegularHeadline, + ), + SizedBox(height: 8), + Row( + children: [ + SizedBox( + height: 40, + width: 40, + child: CircleAvatar( + backgroundColor: Colors.grey, + ), + ), + SizedBox(width: 8), + Text('User Name', style: AppTextStyles.openRegularText), + ], + ), + _Divider(verticalPadding: 16), + ], + ); + } +} From 934a527f657ad4d71fbb77da4cb9a0ce056b5beb Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:02:48 -0300 Subject: [PATCH 13/91] feat: add category entity --- lib/domain/entities/category_entity.dart | 9 +++++++++ lib/domain/entities/entities.dart | 1 + 2 files changed, 10 insertions(+) create mode 100644 lib/domain/entities/category_entity.dart create mode 100644 lib/domain/entities/entities.dart diff --git a/lib/domain/entities/category_entity.dart b/lib/domain/entities/category_entity.dart new file mode 100644 index 0000000..26c80ff --- /dev/null +++ b/lib/domain/entities/category_entity.dart @@ -0,0 +1,9 @@ +class CategoryEntity { + final String title; + final String alias; + + CategoryEntity({ + required this.title, + required this.alias, + }); +} diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart new file mode 100644 index 0000000..489e9c0 --- /dev/null +++ b/lib/domain/entities/entities.dart @@ -0,0 +1 @@ +export 'category_entity.dart'; From 03be17dbb07f800fec8e752ede86d09fc891a02e Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:05:20 -0300 Subject: [PATCH 14/91] feat: add user entity --- lib/domain/entities/entities.dart | 1 + lib/domain/entities/user_entity.dart | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 lib/domain/entities/user_entity.dart diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart index 489e9c0..2ae77cb 100644 --- a/lib/domain/entities/entities.dart +++ b/lib/domain/entities/entities.dart @@ -1 +1,2 @@ export 'category_entity.dart'; +export 'user_entity.dart'; diff --git a/lib/domain/entities/user_entity.dart b/lib/domain/entities/user_entity.dart new file mode 100644 index 0000000..1817058 --- /dev/null +++ b/lib/domain/entities/user_entity.dart @@ -0,0 +1,11 @@ +class UserEntity { + final String id; + final String imageUrl; + final String name; + + UserEntity({ + required this.id, + required this.imageUrl, + required this.name, + }); +} From 9c65f1d91cfaf4190761ca90ca6a3a547c4ef88c Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:06:38 -0300 Subject: [PATCH 15/91] feat: add review entity --- lib/domain/entities/entities.dart | 1 + lib/domain/entities/review_entity.dart | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 lib/domain/entities/review_entity.dart diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart index 2ae77cb..1278d20 100644 --- a/lib/domain/entities/entities.dart +++ b/lib/domain/entities/entities.dart @@ -1,2 +1,3 @@ export 'category_entity.dart'; +export 'review_entity.dart'; export 'user_entity.dart'; diff --git a/lib/domain/entities/review_entity.dart b/lib/domain/entities/review_entity.dart new file mode 100644 index 0000000..bd5bfb9 --- /dev/null +++ b/lib/domain/entities/review_entity.dart @@ -0,0 +1,15 @@ +import 'entities.dart'; + +class ReviewEntity { + final String id; + final int rating; + final String text; + final UserEntity user; + + ReviewEntity({ + required this.id, + required this.rating, + required this.text, + required this.user, + }); +} From d9e16ba007620a421f6fb945ddbff414ca7b8ecc Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:12:42 -0300 Subject: [PATCH 16/91] feat: add restaurant entity --- lib/domain/entities/entities.dart | 1 + lib/domain/entities/restaurant_entity.dart | 29 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 lib/domain/entities/restaurant_entity.dart diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart index 1278d20..412fedb 100644 --- a/lib/domain/entities/entities.dart +++ b/lib/domain/entities/entities.dart @@ -1,3 +1,4 @@ export 'category_entity.dart'; +export 'restaurant_entity.dart'; export 'review_entity.dart'; export 'user_entity.dart'; diff --git a/lib/domain/entities/restaurant_entity.dart b/lib/domain/entities/restaurant_entity.dart new file mode 100644 index 0000000..808d911 --- /dev/null +++ b/lib/domain/entities/restaurant_entity.dart @@ -0,0 +1,29 @@ +import 'entities.dart'; + +class RestaurantEntity { + final String id; + final List categories; + final String displayCategory; + final String heroImage; + final bool isOpen; + final String address; + final String name; + final List photos; + final String price; + final int rating; + final List reviews; + + RestaurantEntity({ + required this.id, + required this.categories, + required this.displayCategory, + required this.heroImage, + required this.isOpen, + required this.address, + required this.name, + required this.photos, + required this.price, + required this.rating, + required this.reviews, + }); +} From 7b4db41885163ebca051e09e0192ffbf192a1068 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:14:18 -0300 Subject: [PATCH 17/91] feat: add get restaurants usecase --- lib/domain/usecases/get_restaurants.dart | 5 +++++ lib/domain/usecases/usecases.dart | 1 + 2 files changed, 6 insertions(+) create mode 100644 lib/domain/usecases/get_restaurants.dart create mode 100644 lib/domain/usecases/usecases.dart diff --git a/lib/domain/usecases/get_restaurants.dart b/lib/domain/usecases/get_restaurants.dart new file mode 100644 index 0000000..de8cc73 --- /dev/null +++ b/lib/domain/usecases/get_restaurants.dart @@ -0,0 +1,5 @@ +import '../entities/entities.dart'; + +abstract class GetRestaurants { + Future> call(); +} diff --git a/lib/domain/usecases/usecases.dart b/lib/domain/usecases/usecases.dart new file mode 100644 index 0000000..0082d63 --- /dev/null +++ b/lib/domain/usecases/usecases.dart @@ -0,0 +1 @@ +export 'get_restaurants.dart'; From 77a3fc01906d48b05c3ac41793a5d6fc0524a53f Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:23:48 -0300 Subject: [PATCH 18/91] feat: add domain error enum on domain helpers --- lib/domain/helpers/domain_error.dart | 4 ++++ lib/domain/helpers/helpers.dart | 1 + 2 files changed, 5 insertions(+) create mode 100644 lib/domain/helpers/domain_error.dart create mode 100644 lib/domain/helpers/helpers.dart diff --git a/lib/domain/helpers/domain_error.dart b/lib/domain/helpers/domain_error.dart new file mode 100644 index 0000000..96fea30 --- /dev/null +++ b/lib/domain/helpers/domain_error.dart @@ -0,0 +1,4 @@ +enum DomainError { + unexpected, + invalidCredentials, +} diff --git a/lib/domain/helpers/helpers.dart b/lib/domain/helpers/helpers.dart new file mode 100644 index 0000000..bde4f1b --- /dev/null +++ b/lib/domain/helpers/helpers.dart @@ -0,0 +1 @@ +export 'domain_error.dart'; From 408cb0a921be4c6c815f9b5df6aa5e6e346550dd Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:24:17 -0300 Subject: [PATCH 19/91] feat: add category model --- lib/data/models/category_model.dart | 37 +++++++++++++++++++++++++++++ lib/data/models/models.dart | 1 + 2 files changed, 38 insertions(+) create mode 100644 lib/data/models/category_model.dart create mode 100644 lib/data/models/models.dart diff --git a/lib/data/models/category_model.dart b/lib/data/models/category_model.dart new file mode 100644 index 0000000..c0d147a --- /dev/null +++ b/lib/data/models/category_model.dart @@ -0,0 +1,37 @@ +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; + +class CategoryModel { + final String title; + final String alias; + + CategoryModel({ + required this.title, + required this.alias, + }); + + factory CategoryModel.fromEntity(CategoryEntity category) { + return CategoryModel( + title: category.title, + alias: category.alias, + ); + } + + factory CategoryModel.fromJson(Map json) { + try { + return CategoryModel( + title: json['title'], + alias: json['alias'], + ); + } catch (_) { + throw DomainError.unexpected; + } + } + + CategoryEntity toEntity() { + return CategoryEntity( + title: title, + alias: alias, + ); + } +} diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart new file mode 100644 index 0000000..673aa86 --- /dev/null +++ b/lib/data/models/models.dart @@ -0,0 +1 @@ +export 'category_model.dart'; From 081d94434df81aa86c478093db42b03b9cd1d4d6 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:28:58 -0300 Subject: [PATCH 20/91] feat: add user model --- lib/data/models/models.dart | 1 + lib/data/models/user_model.dart | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/data/models/user_model.dart diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 673aa86..fcf82e2 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -1 +1,2 @@ export 'category_model.dart'; +export 'user_model.dart'; diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart new file mode 100644 index 0000000..8ea716e --- /dev/null +++ b/lib/data/models/user_model.dart @@ -0,0 +1,42 @@ +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; + +class UserModel { + final String id; + final String imageUrl; + final String name; + + UserModel({ + required this.id, + required this.imageUrl, + required this.name, + }); + + factory UserModel.fromEntity(UserEntity user) { + return UserModel( + id: user.id, + imageUrl: user.imageUrl, + name: user.name, + ); + } + + factory UserModel.fromJson(Map json) { + try { + return UserModel( + id: json['id'], + imageUrl: json['imageUrl'], + name: json['name'], + ); + } catch (_) { + throw DomainError.unexpected; + } + } + + UserEntity toEntity() { + return UserEntity( + id: id, + imageUrl: imageUrl, + name: name, + ); + } +} From 3c67f302fcd61e074e92d79f13e6d1a75c0675fc Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:34:34 -0300 Subject: [PATCH 21/91] feat: add review model --- lib/data/models/models.dart | 1 + lib/data/models/review_model.dart | 48 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 lib/data/models/review_model.dart diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index fcf82e2..6c0f6a5 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -1,2 +1,3 @@ export 'category_model.dart'; +export 'review_model.dart'; export 'user_model.dart'; diff --git a/lib/data/models/review_model.dart b/lib/data/models/review_model.dart new file mode 100644 index 0000000..0deb0e8 --- /dev/null +++ b/lib/data/models/review_model.dart @@ -0,0 +1,48 @@ +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import 'models.dart'; + +class ReviewModel { + final String id; + final int rating; + final String text; + final UserModel user; + + ReviewModel({ + required this.id, + required this.rating, + required this.text, + required this.user, + }); + + factory ReviewModel.fromEntity(ReviewEntity entity) { + return ReviewModel( + id: entity.id, + rating: entity.rating, + text: entity.text, + user: UserModel.fromEntity(entity.user), + ); + } + + factory ReviewModel.fromJson(Map json) { + try { + return ReviewModel( + id: json['id'], + rating: json['rating'], + text: json['text'], + user: UserModel.fromJson(json['user']), + ); + } catch (_) { + throw DomainError.unexpected; + } + } + + ReviewEntity toEntity() { + return ReviewEntity( + id: id, + rating: rating, + text: text, + user: user.toEntity(), + ); + } +} From 852e66e13f34b0a668ba269f5ca92351db99b2fa Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 20:45:12 -0300 Subject: [PATCH 22/91] feat: add restaurant model --- lib/data/models/models.dart | 1 + lib/data/models/restaurant_model.dart | 83 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 lib/data/models/restaurant_model.dart diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 6c0f6a5..7c72d7f 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -1,3 +1,4 @@ export 'category_model.dart'; +export 'restaurant_model.dart'; export 'review_model.dart'; export 'user_model.dart'; diff --git a/lib/data/models/restaurant_model.dart b/lib/data/models/restaurant_model.dart new file mode 100644 index 0000000..d8cb4cc --- /dev/null +++ b/lib/data/models/restaurant_model.dart @@ -0,0 +1,83 @@ +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import 'models.dart'; + +class RestaurantModel { + final String id; + final List categories; + final String displayCategory; + final String heroImage; + final bool isOpen; + final String address; + final String name; + final List photos; + final String price; + final int rating; + final List reviews; + + RestaurantModel({ + required this.id, + required this.categories, + required this.displayCategory, + required this.heroImage, + required this.isOpen, + required this.address, + required this.name, + required this.photos, + required this.price, + required this.rating, + required this.reviews, + }); + + factory RestaurantModel.fromEntity(RestaurantEntity entity) { + return RestaurantModel( + id: entity.id, + rating: entity.rating, + categories: entity.categories.map((category) => CategoryModel.fromEntity(category)).toList(), + displayCategory: entity.displayCategory, + heroImage: entity.heroImage, + isOpen: entity.isOpen, + address: entity.address, + name: entity.name, + photos: entity.photos, + price: entity.price, + reviews: entity.reviews.map((review) => ReviewModel.fromEntity(review)).toList(), + ); + } + + factory RestaurantModel.fromJson(Map json) { + try { + return RestaurantModel( + id: json['id'], + categories: (json['categories'] as List).map((item) => CategoryModel.fromJson(json)).toList(), + displayCategory: json['displayCategory'], + heroImage: json['heroImage'], + isOpen: json['isOpen'], + address: json['address'], + name: json['name'], + photos: json['photos'], + price: json['price'], + rating: json['rating'], + reviews: (json['reviews'] as List).map(((item) => ReviewModel.fromJson(item))).toList(), + ); + } catch (_) { + throw DomainError.unexpected; + } + } + + RestaurantEntity toEntity() { + return RestaurantEntity( + id: id, + rating: rating, + categories: categories.map((category) => category.toEntity()).toList(), + displayCategory: displayCategory, + heroImage: heroImage, + isOpen: isOpen, + address: address, + name: name, + photos: photos, + price: price, + reviews: reviews.map((review) => review.toEntity()).toList(), + ); + } +} From 0970dcd4b10498b4cb2d92b54f3cb896a4a3d505 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sat, 14 Sep 2024 22:05:23 -0300 Subject: [PATCH 23/91] feat: add http adapter --- lib/infra/http/http.dart | 2 ++ lib/infra/http/http_adapter.dart | 46 ++++++++++++++++++++++++++++++++ lib/infra/http/http_client.dart | 7 +++++ lib/infra/http/http_error.dart | 8 ++++++ 4 files changed, 63 insertions(+) create mode 100644 lib/infra/http/http.dart create mode 100644 lib/infra/http/http_adapter.dart create mode 100644 lib/infra/http/http_client.dart create mode 100644 lib/infra/http/http_error.dart diff --git a/lib/infra/http/http.dart b/lib/infra/http/http.dart new file mode 100644 index 0000000..b86686b --- /dev/null +++ b/lib/infra/http/http.dart @@ -0,0 +1,2 @@ +export 'http_client.dart'; +export 'http_error.dart'; diff --git a/lib/infra/http/http_adapter.dart b/lib/infra/http/http_adapter.dart new file mode 100644 index 0000000..5d7c6ad --- /dev/null +++ b/lib/infra/http/http_adapter.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +import 'http.dart'; + +class HttpAdapter implements HttpClient { + final Client _client; + + HttpAdapter(this._client); + + @override + Future request({required String url, required String method, Map? data}) async { + final headers = {'content-type': 'application/json', 'accept': 'application/json'}; + final uri = Uri.parse(url); + var response = Response('', 500); + + try { + if (method == 'get') { + response = await _client.get(uri, headers: headers); + } + } catch (_) { + throw HttpError.serverError; + } + + return _handleResponse(response); + } + + Map? _handleResponse(Response response) { + if (response.statusCode == 200) { + return response.body.isEmpty ? null : jsonDecode(response.body); + } else if (response.statusCode == 204) { + return null; + } else if (response.statusCode == 400) { + throw HttpError.badRequest; + } else if (response.statusCode == 401) { + throw HttpError.unauthorized; + } else if (response.statusCode == 403) { + throw HttpError.forbbiden; + } else if (response.statusCode == 404) { + throw HttpError.notFound; + } else { + throw HttpError.serverError; + } + } +} diff --git a/lib/infra/http/http_client.dart b/lib/infra/http/http_client.dart new file mode 100644 index 0000000..754b661 --- /dev/null +++ b/lib/infra/http/http_client.dart @@ -0,0 +1,7 @@ +abstract class HttpClient { + Future request({ + required String url, + required String method, + Map? data, + }); +} diff --git a/lib/infra/http/http_error.dart b/lib/infra/http/http_error.dart new file mode 100644 index 0000000..d9f29e2 --- /dev/null +++ b/lib/infra/http/http_error.dart @@ -0,0 +1,8 @@ +enum HttpError { + badRequest, + notFound, + serverError, + unauthorized, + forbbiden, + invalidData, +} From 7ba5143d068afa219629fd541911d6a159e376f3 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 11:18:04 -0300 Subject: [PATCH 24/91] feat: add http adapter --- lib/infra/http/http.dart | 1 + lib/infra/http/http_adapter.dart | 20 +++++--------------- lib/infra/http/http_client.dart | 4 ++-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/infra/http/http.dart b/lib/infra/http/http.dart index b86686b..41964ea 100644 --- a/lib/infra/http/http.dart +++ b/lib/infra/http/http.dart @@ -1,2 +1,3 @@ +export 'http_adapter.dart'; export 'http_client.dart'; export 'http_error.dart'; diff --git a/lib/infra/http/http_adapter.dart b/lib/infra/http/http_adapter.dart index 5d7c6ad..219c7b9 100644 --- a/lib/infra/http/http_adapter.dart +++ b/lib/infra/http/http_adapter.dart @@ -4,20 +4,20 @@ import 'package:http/http.dart'; import 'http.dart'; -class HttpAdapter implements HttpClient { +class HttpAdapter implements HttpClient { final Client _client; HttpAdapter(this._client); @override - Future request({required String url, required String method, Map? data}) async { + Future request({required String url, required String method, String? data}) async { final headers = {'content-type': 'application/json', 'accept': 'application/json'}; final uri = Uri.parse(url); var response = Response('', 500); try { - if (method == 'get') { - response = await _client.get(uri, headers: headers); + if (method == 'post') { + response = await _client.post(uri, headers: headers, body: data); } } catch (_) { throw HttpError.serverError; @@ -26,19 +26,9 @@ class HttpAdapter implements HttpClient { return _handleResponse(response); } - Map? _handleResponse(Response response) { + Map _handleResponse(Response response) { if (response.statusCode == 200) { return response.body.isEmpty ? null : jsonDecode(response.body); - } else if (response.statusCode == 204) { - return null; - } else if (response.statusCode == 400) { - throw HttpError.badRequest; - } else if (response.statusCode == 401) { - throw HttpError.unauthorized; - } else if (response.statusCode == 403) { - throw HttpError.forbbiden; - } else if (response.statusCode == 404) { - throw HttpError.notFound; } else { throw HttpError.serverError; } diff --git a/lib/infra/http/http_client.dart b/lib/infra/http/http_client.dart index 754b661..5becda2 100644 --- a/lib/infra/http/http_client.dart +++ b/lib/infra/http/http_client.dart @@ -1,7 +1,7 @@ -abstract class HttpClient { +abstract class HttpClient { Future request({ required String url, required String method, - Map? data, + DataType data, }); } From b022cd0c6af165d88cba92c58add36669ab25aed Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 11:18:28 -0300 Subject: [PATCH 25/91] feat: add http client factory --- lib/factories/http/http.dart | 2 ++ lib/factories/http/http_client_factory.dart | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 lib/factories/http/http.dart create mode 100644 lib/factories/http/http_client_factory.dart diff --git a/lib/factories/http/http.dart b/lib/factories/http/http.dart new file mode 100644 index 0000000..65f549c --- /dev/null +++ b/lib/factories/http/http.dart @@ -0,0 +1,2 @@ +export 'api_url_factory.dart'; +export 'http_client_factory.dart'; \ No newline at end of file diff --git a/lib/factories/http/http_client_factory.dart b/lib/factories/http/http_client_factory.dart new file mode 100644 index 0000000..9c1927d --- /dev/null +++ b/lib/factories/http/http_client_factory.dart @@ -0,0 +1,8 @@ +import 'package:http/http.dart'; + +import '../../../infra/http/http.dart'; + +HttpAdapter makeHttpAdapter() { + final client = Client(); + return HttpAdapter(client); +} From c45a824c5dd87466ac747420d39db8ddf5dfba1b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 11:18:46 -0300 Subject: [PATCH 26/91] feat: add api url factory --- lib/factories/http/api_url_factory.dart | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/factories/http/api_url_factory.dart diff --git a/lib/factories/http/api_url_factory.dart b/lib/factories/http/api_url_factory.dart new file mode 100644 index 0000000..b450b69 --- /dev/null +++ b/lib/factories/http/api_url_factory.dart @@ -0,0 +1 @@ +String makeApiUrl() => 'https://api.yelp.com/v3/graphql'; From 2ba37bfc4fc5bdd67f5741c36405363ac2f241bb Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 11:19:18 -0300 Subject: [PATCH 27/91] feat: add graphql get restaurants use case --- .../usecases/graphql_get_restaurants.dart | 26 +++++++++++++++++++ lib/data/usecases/usecases.dart | 1 + lib/domain/usecases/get_restaurants.dart | 4 +-- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 lib/data/usecases/graphql_get_restaurants.dart create mode 100644 lib/data/usecases/usecases.dart diff --git a/lib/data/usecases/graphql_get_restaurants.dart b/lib/data/usecases/graphql_get_restaurants.dart new file mode 100644 index 0000000..5d9ebca --- /dev/null +++ b/lib/data/usecases/graphql_get_restaurants.dart @@ -0,0 +1,26 @@ +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import '../../domain/usecases/usecases.dart'; +import '../../infra/http/http.dart'; +import '../models/models.dart'; + +class GraphqlGetRestaurants implements GetRestaurants { + final HttpClient _client; + final String _url; + + const GraphqlGetRestaurants({ + required HttpClient client, + required String url, + }) : _client = client, + _url = url; + + @override + Future> call(String data) async { + try { + final response = await _client.request(url: _url, method: 'post', data: data); + return (response as List).map((item) => RestaurantModel.fromJson(item).toEntity()).toList(); + } catch (_) { + throw DomainError.unexpected; + } + } +} diff --git a/lib/data/usecases/usecases.dart b/lib/data/usecases/usecases.dart new file mode 100644 index 0000000..d355a02 --- /dev/null +++ b/lib/data/usecases/usecases.dart @@ -0,0 +1 @@ +export 'graphql_get_restaurants.dart'; diff --git a/lib/domain/usecases/get_restaurants.dart b/lib/domain/usecases/get_restaurants.dart index de8cc73..1c99bb8 100644 --- a/lib/domain/usecases/get_restaurants.dart +++ b/lib/domain/usecases/get_restaurants.dart @@ -1,5 +1,5 @@ import '../entities/entities.dart'; -abstract class GetRestaurants { - Future> call(); +abstract class GetRestaurants { + Future> call(T data); } From 0434fe5c28c7f36c6b88eba0447d0cd917a71d3a Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 11:19:42 -0300 Subject: [PATCH 28/91] feat: add get restaurants factory --- lib/factories/factories.dart | 2 ++ lib/factories/usecases/get_restaurants_factory.dart | 9 +++++++++ lib/factories/usecases/usecases.dart | 1 + 3 files changed, 12 insertions(+) create mode 100644 lib/factories/factories.dart create mode 100644 lib/factories/usecases/get_restaurants_factory.dart create mode 100644 lib/factories/usecases/usecases.dart diff --git a/lib/factories/factories.dart b/lib/factories/factories.dart new file mode 100644 index 0000000..bfa8e20 --- /dev/null +++ b/lib/factories/factories.dart @@ -0,0 +1,2 @@ +export 'http/http.dart'; +export 'usecases/usecases.dart'; diff --git a/lib/factories/usecases/get_restaurants_factory.dart b/lib/factories/usecases/get_restaurants_factory.dart new file mode 100644 index 0000000..daefb35 --- /dev/null +++ b/lib/factories/usecases/get_restaurants_factory.dart @@ -0,0 +1,9 @@ +import '../../data/usecases/usecases.dart'; +import '../../domain/usecases/usecases.dart'; +import '../factories.dart'; + +GetRestaurants makeGetRestaurants() { + final client = makeHttpAdapter(); + final url = makeApiUrl(); + return GraphqlGetRestaurants(client: client, url: url); +} diff --git a/lib/factories/usecases/usecases.dart b/lib/factories/usecases/usecases.dart new file mode 100644 index 0000000..21cfa4d --- /dev/null +++ b/lib/factories/usecases/usecases.dart @@ -0,0 +1 @@ +export 'get_restaurants_factory.dart'; From 05f75822697085e44a4755795be8e362585ecaea Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:15:39 -0300 Subject: [PATCH 29/91] feat: graphql get restaurants use case adjustments --- .../usecases/graphql_get_restaurants.dart | 48 +++++++++++++++++-- lib/domain/usecases/get_restaurants.dart | 4 +- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/data/usecases/graphql_get_restaurants.dart b/lib/data/usecases/graphql_get_restaurants.dart index 5d9ebca..d401937 100644 --- a/lib/data/usecases/graphql_get_restaurants.dart +++ b/lib/data/usecases/graphql_get_restaurants.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + import '../../domain/entities/entities.dart'; import '../../domain/helpers/helpers.dart'; import '../../domain/usecases/usecases.dart'; import '../../infra/http/http.dart'; import '../models/models.dart'; -class GraphqlGetRestaurants implements GetRestaurants { +class GraphqlGetRestaurants implements GetRestaurants { final HttpClient _client; final String _url; @@ -14,12 +16,48 @@ class GraphqlGetRestaurants implements GetRestaurants { }) : _client = client, _url = url; + static const query = ''' + query getRestaurants { + search(location: "Las Vegas", limit: 20, offset: 0) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + } + '''; + @override - Future> call(String data) async { + Future> call() async { try { - final response = await _client.request(url: _url, method: 'post', data: data); - return (response as List).map((item) => RestaurantModel.fromJson(item).toEntity()).toList(); - } catch (_) { + final response = await _client.request(url: _url, method: 'post', data: query); + return (response['data']['search']['business'] as List).map((item) => RestaurantModel.fromJson(item).toEntity()).toList(); + } catch (e, s) { + debugPrintStack(label: 'Error $e', stackTrace: s); throw DomainError.unexpected; } } diff --git a/lib/domain/usecases/get_restaurants.dart b/lib/domain/usecases/get_restaurants.dart index 1c99bb8..de8cc73 100644 --- a/lib/domain/usecases/get_restaurants.dart +++ b/lib/domain/usecases/get_restaurants.dart @@ -1,5 +1,5 @@ import '../entities/entities.dart'; -abstract class GetRestaurants { - Future> call(T data); +abstract class GetRestaurants { + Future> call(); } From db95a33ef4fa5637e849ffbaedc0842713982ef8 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:16:28 -0300 Subject: [PATCH 30/91] refact: add headers via depencency injection --- lib/factories/http/http_client_factory.dart | 7 ++++++- lib/infra/http/http_adapter.dart | 14 +++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/factories/http/http_client_factory.dart b/lib/factories/http/http_client_factory.dart index 9c1927d..ed2e649 100644 --- a/lib/factories/http/http_client_factory.dart +++ b/lib/factories/http/http_client_factory.dart @@ -4,5 +4,10 @@ import '../../../infra/http/http.dart'; HttpAdapter makeHttpAdapter() { final client = Client(); - return HttpAdapter(client); + final headers = { + 'Content-Type': 'application/graphql', + 'Authorization': + 'Bearer 1hH9pW24WpBHxamMbgShaygY4eqRSW5NZq3e6eVImPkhFU8RR4KHIZ59pJ0D5YL4GCizkxjdxegx1f5igGGQuVEMER8gR-Jo8JpT1V6D3OUjUcucwL8uF5pxNqzlZnYx', + }; + return HttpAdapter(client: client, headers: headers); } diff --git a/lib/infra/http/http_adapter.dart b/lib/infra/http/http_adapter.dart index 219c7b9..952e0d7 100644 --- a/lib/infra/http/http_adapter.dart +++ b/lib/infra/http/http_adapter.dart @@ -6,18 +6,22 @@ import 'http.dart'; class HttpAdapter implements HttpClient { final Client _client; + final Map _headers; - HttpAdapter(this._client); + HttpAdapter({ + required Client client, + required Map headers, + }) : _client = client, + _headers = headers; @override Future request({required String url, required String method, String? data}) async { - final headers = {'content-type': 'application/json', 'accept': 'application/json'}; final uri = Uri.parse(url); var response = Response('', 500); try { if (method == 'post') { - response = await _client.post(uri, headers: headers, body: data); + response = await _client.post(uri, headers: _headers, body: data); } } catch (_) { throw HttpError.serverError; @@ -29,6 +33,10 @@ class HttpAdapter implements HttpClient { Map _handleResponse(Response response) { if (response.statusCode == 200) { return response.body.isEmpty ? null : jsonDecode(response.body); + } else if (response.statusCode == 400) { + throw HttpError.badRequest; + } else if (response.statusCode == 401) { + throw HttpError.unauthorized; } else { throw HttpError.serverError; } From 297228292d4823817771bfcf407a8cd43e873874 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:17:47 -0300 Subject: [PATCH 31/91] feat: add cubit restaurant tour presenter initial impl --- .../cubit_restaurant_tour_presenter.dart | 37 +++++++++++++++++++ .../restaurant_tour_presenter.dart | 4 ++ 2 files changed, 41 insertions(+) create mode 100644 lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart create mode 100644 lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart new file mode 100644 index 0000000..43aa093 --- /dev/null +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../domain/usecases/usecases.dart'; +import '../../ui/pages/pages.dart'; + +part 'states.dart'; + +class CubitRestaurantTourPresenter extends Cubit implements RestaurantTourPresenter { + final GetRestaurants _getRestaurants; + + CubitRestaurantTourPresenter({required GetRestaurants getRestaurants}) + : _getRestaurants = getRestaurants, + super(RestaurantInitialState()) { + getAllRestaurants(); + } + + @override + Future getFavoriteRestaurants() { + // TODO: implement getFavoriteRestaurants + throw UnimplementedError(); + } + + @override + Future getAllRestaurants() async { + try { + emit(RestaurantLoadingState()); + await _getRestaurants(); + emit(RestaurantSuccessState()); + } catch (e, s) { + debugPrintStack(label: 'Error - $e', stackTrace: s); + emit(RestaurantErrorState('There was an error when searching for restaurants.')); + } + } + + void dispose() {} +} diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart new file mode 100644 index 0000000..df0b668 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -0,0 +1,4 @@ +abstract class RestaurantTourPresenter { + Future getAllRestaurants(); + Future getFavoriteRestaurants(); +} From 07aad5be24b55dafc0917755f3367838dda13383 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:18:42 -0300 Subject: [PATCH 32/91] feat: add presentation factory file --- lib/factories/presentation/presentation.dart | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/factories/presentation/presentation.dart diff --git a/lib/factories/presentation/presentation.dart b/lib/factories/presentation/presentation.dart new file mode 100644 index 0000000..5c7594e --- /dev/null +++ b/lib/factories/presentation/presentation.dart @@ -0,0 +1 @@ +export 'restaurant_tour_presenter_factory.dart'; From 4b64e2547888aeaddcb797ca3d99e9773d49a2ae Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:19:11 -0300 Subject: [PATCH 33/91] feat: add presentation states --- lib/presentation/presentation.dart | 1 + lib/presentation/restaurant_tour/states.dart | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 lib/presentation/presentation.dart create mode 100644 lib/presentation/restaurant_tour/states.dart diff --git a/lib/presentation/presentation.dart b/lib/presentation/presentation.dart new file mode 100644 index 0000000..5d18f74 --- /dev/null +++ b/lib/presentation/presentation.dart @@ -0,0 +1 @@ +export 'restaurant_tour/cubit_restaurant_tour_presenter.dart'; diff --git a/lib/presentation/restaurant_tour/states.dart b/lib/presentation/restaurant_tour/states.dart new file mode 100644 index 0000000..c386783 --- /dev/null +++ b/lib/presentation/restaurant_tour/states.dart @@ -0,0 +1,18 @@ +part of 'cubit_restaurant_tour_presenter.dart'; + +@immutable +class RestaurantState {} + +class RestaurantInitialState extends RestaurantState {} + +class RestaurantLoadingState extends RestaurantState {} + +class RestaurantSuccessState extends RestaurantState {} + +class RestaurantErrorState extends RestaurantState { + final String message; + + RestaurantErrorState(this.message); +} + +class RestaurantTabState extends RestaurantState {} From eadcfe3eb1c45e99f538205a1c9b97414b2c9801 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:20:11 -0300 Subject: [PATCH 34/91] feat: add restaurant tour presenter factory --- .../presentation/restaurant_tour_presenter_factory.dart | 7 +++++++ lib/ui/pages/restaurant_tour/restaurant_tour.dart | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 lib/factories/presentation/restaurant_tour_presenter_factory.dart create mode 100644 lib/ui/pages/restaurant_tour/restaurant_tour.dart diff --git a/lib/factories/presentation/restaurant_tour_presenter_factory.dart b/lib/factories/presentation/restaurant_tour_presenter_factory.dart new file mode 100644 index 0000000..68bfbc2 --- /dev/null +++ b/lib/factories/presentation/restaurant_tour_presenter_factory.dart @@ -0,0 +1,7 @@ +import '../../presentation/presentation.dart'; +import '../../ui/pages/pages.dart'; +import '../factories.dart'; + +RestaurantTourPresenter makeRestaurantTourPresenter() { + return CubitRestaurantTourPresenter(getRestaurants: makeGetRestaurants()); +} diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour.dart b/lib/ui/pages/restaurant_tour/restaurant_tour.dart new file mode 100644 index 0000000..bbffbb0 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/restaurant_tour.dart @@ -0,0 +1,2 @@ +export 'restaurant_tour_page.dart'; +export 'restaurant_tour_presenter.dart'; From 3e1c0d77900fe01102d858a9bd016b97626bf1c9 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:20:45 -0300 Subject: [PATCH 35/91] feat: presenter initial impl ou restaurant tour page --- .../restaurant_tour/restaurant_tour_page.dart | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 1954fec..0da42c8 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,14 +1,49 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../presentation/presentation.dart'; import '../../../typography.dart'; import '../../widgets/widgets.dart'; +import '../pages.dart'; part 'widgets/app_bar.dart'; part 'widgets/image.dart'; part 'widgets/restaurant_item.dart'; -class RestaurantTourPage extends StatelessWidget { - const RestaurantTourPage({super.key}); +class RestaurantTourPage extends StatefulWidget { + final RestaurantTourPresenter _presenter; + + const RestaurantTourPage({ + super.key, + required RestaurantTourPresenter presenter, + }) : _presenter = presenter; + + @override + State createState() => _RestaurantTourPageState(); +} + +class _RestaurantTourPageState extends State { + late final CubitRestaurantTourPresenter _bloc; + + void _setUp() { + _bloc = widget._presenter as CubitRestaurantTourPresenter; + } + + void _dispose() { + _bloc.dispose(); + } + + @override + void initState() { + super.initState(); + _setUp(); + } + + @override + void dispose() { + super.dispose(); + _dispose(); + } @override Widget build(BuildContext context) { @@ -16,17 +51,24 @@ class RestaurantTourPage extends StatelessWidget { length: 2, child: Scaffold( appBar: const _AppBar(), - body: TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - ListView( - padding: const EdgeInsets.all(16), - children: const [ - _RestaurantItem(), + body: BlocBuilder( + bloc: _bloc, + builder: (context, state) { + if (state is RestaurantLoadingState) return const Center(child: CircularLoading()); + if (state is RestaurantErrorState) return Center(child: Text(state.message)); + return TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: [ + ListView( + padding: const EdgeInsets.all(16), + children: const [ + _RestaurantItem(), + ], + ), + const Center(child: Text('My Favorites Content')), ], - ), - const Center(child: Text('My Favorites Content')), - ], + ); + }, ), ), ); From 4de2debf1526682d1192b1701a324541b4278ef4 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:21:06 -0300 Subject: [PATCH 36/91] feat: add restaurant tour page factory --- lib/factories/factories.dart | 2 ++ lib/factories/ui/restaurant_tour_page_factory.dart | 6 ++++++ lib/factories/ui/ui.dart | 1 + lib/ui/pages/pages.dart | 1 + 4 files changed, 10 insertions(+) create mode 100644 lib/factories/ui/restaurant_tour_page_factory.dart create mode 100644 lib/factories/ui/ui.dart create mode 100644 lib/ui/pages/pages.dart diff --git a/lib/factories/factories.dart b/lib/factories/factories.dart index bfa8e20..7fe53be 100644 --- a/lib/factories/factories.dart +++ b/lib/factories/factories.dart @@ -1,2 +1,4 @@ export 'http/http.dart'; +export 'presentation/presentation.dart'; +export 'ui/ui.dart'; export 'usecases/usecases.dart'; diff --git a/lib/factories/ui/restaurant_tour_page_factory.dart b/lib/factories/ui/restaurant_tour_page_factory.dart new file mode 100644 index 0000000..9172cbf --- /dev/null +++ b/lib/factories/ui/restaurant_tour_page_factory.dart @@ -0,0 +1,6 @@ +import '../../ui/pages/pages.dart'; +import '../factories.dart'; + +RestaurantTourPage makeRestaurantTourPage() { + return RestaurantTourPage(presenter: makeRestaurantTourPresenter()); +} diff --git a/lib/factories/ui/ui.dart b/lib/factories/ui/ui.dart new file mode 100644 index 0000000..1b089b9 --- /dev/null +++ b/lib/factories/ui/ui.dart @@ -0,0 +1 @@ +export 'restaurant_tour_page_factory.dart'; diff --git a/lib/ui/pages/pages.dart b/lib/ui/pages/pages.dart new file mode 100644 index 0000000..badd2fd --- /dev/null +++ b/lib/ui/pages/pages.dart @@ -0,0 +1 @@ +export 'restaurant_tour/restaurant_tour.dart'; From e4fafeed60aa32381debb2a0dc48a19acb522713 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 21:46:55 -0300 Subject: [PATCH 37/91] feat: add restaurant list widget on tour page --- .../cubit_restaurant_tour_presenter.dart | 14 ++++- .../restaurant_tour/restaurant_tour_page.dart | 56 +++++++++++-------- .../restaurant_tour_presenter.dart | 3 + .../widgets/restaurant_item.dart | 22 ++++---- .../widgets/restaurant_list.dart | 17 ++++++ 5 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 43aa093..53a91c6 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/entities/entities.dart'; import '../../domain/usecases/usecases.dart'; import '../../ui/pages/pages.dart'; @@ -15,6 +16,11 @@ class CubitRestaurantTourPresenter extends Cubit implements Res getAllRestaurants(); } + final _restaurantList = []; + + @override + List get restaurantList => _restaurantList; + @override Future getFavoriteRestaurants() { // TODO: implement getFavoriteRestaurants @@ -25,13 +31,15 @@ class CubitRestaurantTourPresenter extends Cubit implements Res Future getAllRestaurants() async { try { emit(RestaurantLoadingState()); - await _getRestaurants(); + _restaurantList.addAll(await _getRestaurants()); emit(RestaurantSuccessState()); } catch (e, s) { debugPrintStack(label: 'Error - $e', stackTrace: s); - emit(RestaurantErrorState('There was an error when searching for restaurants.')); + emit(RestaurantErrorState('There was an error \nwhen get restaurants.')); } } - void dispose() {} + void dispose() { + _restaurantList.clear(); + } } diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 0da42c8..05bd450 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../domain/entities/entities.dart'; import '../../../presentation/presentation.dart'; import '../../../typography.dart'; import '../../widgets/widgets.dart'; @@ -9,6 +10,7 @@ import '../pages.dart'; part 'widgets/app_bar.dart'; part 'widgets/image.dart'; part 'widgets/restaurant_item.dart'; +part 'widgets/restaurant_list.dart'; class RestaurantTourPage extends StatefulWidget { final RestaurantTourPresenter _presenter; @@ -47,28 +49,38 @@ class _RestaurantTourPageState extends State { @override Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: const _AppBar(), - body: BlocBuilder( - bloc: _bloc, - builder: (context, state) { - if (state is RestaurantLoadingState) return const Center(child: CircularLoading()); - if (state is RestaurantErrorState) return Center(child: Text(state.message)); - return TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - ListView( - padding: const EdgeInsets.all(16), - children: const [ - _RestaurantItem(), - ], - ), - const Center(child: Text('My Favorites Content')), - ], - ); - }, + return BlocProvider( + create: (_) => _bloc, + child: DefaultTabController( + length: 2, + child: Scaffold( + appBar: const _AppBar(), + body: BlocBuilder( + bloc: _bloc, + builder: (context, state) { + if (state is RestaurantLoadingState) { + return const Center(child: CircularLoading()); + } + + if (state is RestaurantErrorState) { + return Center( + child: Text( + state.message, + style: AppTextStyles.openRegularHeadline, + textAlign: TextAlign.center, + ), + ); + } + + return const TabBarView( + physics: NeverScrollableScrollPhysics(), + children: [ + _RestaurantList(), + Center(child: Text('My Favorites Content')), + ], + ); + }, + ), ), ), ); diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart index df0b668..1ef5c90 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -1,4 +1,7 @@ +import '../../../domain/entities/entities.dart'; + abstract class RestaurantTourPresenter { + List get restaurantList; Future getAllRestaurants(); Future getFavoriteRestaurants(); } diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart index 71f90fd..c3f2560 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart @@ -1,7 +1,9 @@ part of '../restaurant_tour_page.dart'; class _RestaurantItem extends StatelessWidget { - const _RestaurantItem(); + final RestaurantEntity _restaurant; + + const _RestaurantItem(this._restaurant); @override Widget build(BuildContext context) { @@ -23,23 +25,23 @@ class _RestaurantItem extends StatelessWidget { ), ], ), - child: const Row( + child: Row( children: [ - _Image(''), - SizedBox(width: 12), + _Image(_restaurant.heroImage), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Restaurant Name Goes Here And Wraps 2 Lines', style: AppTextStyles.loraRegularTitle), - SizedBox(height: 4), - Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), - SizedBox(height: 4), + Text(_restaurant.name, style: AppTextStyles.loraRegularTitle), + const SizedBox(height: 4), + const Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Stars(5), - Status(true), + Stars(_restaurant.rating), + Status(_restaurant.isOpen), ], ), ], diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart new file mode 100644 index 0000000..16ba219 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart @@ -0,0 +1,17 @@ +part of '../restaurant_tour_page.dart'; + +class _RestaurantList extends StatelessWidget { + const _RestaurantList(); + + @override + Widget build(BuildContext context) { + final restaurantList = context.read().restaurantList; + return ListView.builder( + padding: const EdgeInsets.all(16), + itemBuilder: (itemBuilder, index) { + final restaurant = restaurantList[index]; + return _RestaurantItem(restaurant); + }, + ); + } +} From 2670dacace7192bd2e7d4d54e922aa19fdd86db3 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 22:19:10 -0300 Subject: [PATCH 38/91] feat: add cache protocols --- lib/infra/cache/cache.dart | 3 +++ lib/infra/cache/delete_cache.dart | 3 +++ lib/infra/cache/fetch_cache.dart | 3 +++ lib/infra/cache/save_cache.dart | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 lib/infra/cache/cache.dart create mode 100644 lib/infra/cache/delete_cache.dart create mode 100644 lib/infra/cache/fetch_cache.dart create mode 100644 lib/infra/cache/save_cache.dart diff --git a/lib/infra/cache/cache.dart b/lib/infra/cache/cache.dart new file mode 100644 index 0000000..fe0958e --- /dev/null +++ b/lib/infra/cache/cache.dart @@ -0,0 +1,3 @@ +export 'delete_cache.dart'; +export 'fetch_cache.dart'; +export 'save_cache.dart'; diff --git a/lib/infra/cache/delete_cache.dart b/lib/infra/cache/delete_cache.dart new file mode 100644 index 0000000..75109fb --- /dev/null +++ b/lib/infra/cache/delete_cache.dart @@ -0,0 +1,3 @@ +abstract class DeleteCache { + Future delete(String key); +} diff --git a/lib/infra/cache/fetch_cache.dart b/lib/infra/cache/fetch_cache.dart new file mode 100644 index 0000000..6d58705 --- /dev/null +++ b/lib/infra/cache/fetch_cache.dart @@ -0,0 +1,3 @@ +abstract class FetchCache { + Future fetch(String key); +} diff --git a/lib/infra/cache/save_cache.dart b/lib/infra/cache/save_cache.dart new file mode 100644 index 0000000..d93aa94 --- /dev/null +++ b/lib/infra/cache/save_cache.dart @@ -0,0 +1,3 @@ +abstract class SaveCache { + Future save({required String key, String? value}); +} From 332b1ab1e26b91dd9974e66eeaabeae61208ddca Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 22:20:53 -0300 Subject: [PATCH 39/91] chore: add shared preferences as dependency --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 +++++++++++++ pubspec.lock | 119 ++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 ios/Podfile diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index b0bc69d..452d2fb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ 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: @@ -235,6 +243,11 @@ packages: 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: @@ -411,6 +424,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.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" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + 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: @@ -443,6 +496,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + 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: @@ -592,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" yaml: dependency: transitive description: @@ -602,4 +719,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index a655055..f51121e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: http: ^1.2.2 json_annotation: ^4.9.0 flutter_bloc: ^8.1.6 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: From 66d9b21da50f6df7965e9b197fdf9b2db30e99ad Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 22:45:53 -0300 Subject: [PATCH 40/91] feat: add local storage adapter --- .../cache/local_storage_adapter_factory.dart | 5 ++++ lib/infra/cache/cache.dart | 1 + lib/infra/cache/local_storage_adapter.dart | 26 +++++++++++++++++++ lib/infra/cache/save_cache.dart | 4 +-- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 lib/factories/infra/cache/local_storage_adapter_factory.dart create mode 100644 lib/infra/cache/local_storage_adapter.dart diff --git a/lib/factories/infra/cache/local_storage_adapter_factory.dart b/lib/factories/infra/cache/local_storage_adapter_factory.dart new file mode 100644 index 0000000..f8a0156 --- /dev/null +++ b/lib/factories/infra/cache/local_storage_adapter_factory.dart @@ -0,0 +1,5 @@ +import '../../../infra/infra.dart'; + +LocalStorageAdapter makeLocalStorageAdapter() { + return LocalStorageAdapter(); +} diff --git a/lib/infra/cache/cache.dart b/lib/infra/cache/cache.dart index fe0958e..22013c4 100644 --- a/lib/infra/cache/cache.dart +++ b/lib/infra/cache/cache.dart @@ -1,3 +1,4 @@ export 'delete_cache.dart'; export 'fetch_cache.dart'; export 'save_cache.dart'; +export 'local_storage_adapter.dart'; diff --git a/lib/infra/cache/local_storage_adapter.dart b/lib/infra/cache/local_storage_adapter.dart new file mode 100644 index 0000000..cd807bf --- /dev/null +++ b/lib/infra/cache/local_storage_adapter.dart @@ -0,0 +1,26 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +import 'cache.dart'; + +class LocalStorageAdapter implements SaveCache, FetchCache, DeleteCache { + late final SharedPreferences storage; + + LocalStorageAdapter() { + SharedPreferences.getInstance().then((instance) => storage = instance); + } + + @override + Future save({required String key, required String value}) async { + await storage.setString(key, value); + } + + @override + Future fetch(String key) async { + return storage.getString(key); + } + + @override + Future delete(String key) async { + storage.remove(key); + } +} diff --git a/lib/infra/cache/save_cache.dart b/lib/infra/cache/save_cache.dart index d93aa94..fc79c84 100644 --- a/lib/infra/cache/save_cache.dart +++ b/lib/infra/cache/save_cache.dart @@ -1,3 +1,3 @@ -abstract class SaveCache { - Future save({required String key, String? value}); +abstract class SaveCache { + Future save({required String key, required String value}); } From b10f2641d0490e72a057ba1f88f548ada23e4439 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 22:46:20 -0300 Subject: [PATCH 41/91] refact: move http folder factory to cache folder factory --- lib/factories/{ => infra}/http/api_url_factory.dart | 0 lib/factories/{ => infra}/http/http.dart | 0 lib/factories/{ => infra}/http/http_client_factory.dart | 2 +- lib/infra/infra.dart | 2 ++ 4 files changed, 3 insertions(+), 1 deletion(-) rename lib/factories/{ => infra}/http/api_url_factory.dart (100%) rename lib/factories/{ => infra}/http/http.dart (100%) rename lib/factories/{ => infra}/http/http_client_factory.dart (90%) create mode 100644 lib/infra/infra.dart diff --git a/lib/factories/http/api_url_factory.dart b/lib/factories/infra/http/api_url_factory.dart similarity index 100% rename from lib/factories/http/api_url_factory.dart rename to lib/factories/infra/http/api_url_factory.dart diff --git a/lib/factories/http/http.dart b/lib/factories/infra/http/http.dart similarity index 100% rename from lib/factories/http/http.dart rename to lib/factories/infra/http/http.dart diff --git a/lib/factories/http/http_client_factory.dart b/lib/factories/infra/http/http_client_factory.dart similarity index 90% rename from lib/factories/http/http_client_factory.dart rename to lib/factories/infra/http/http_client_factory.dart index ed2e649..ddcce6a 100644 --- a/lib/factories/http/http_client_factory.dart +++ b/lib/factories/infra/http/http_client_factory.dart @@ -1,6 +1,6 @@ import 'package:http/http.dart'; -import '../../../infra/http/http.dart'; +import '../../../../infra/http/http.dart'; HttpAdapter makeHttpAdapter() { final client = Client(); diff --git a/lib/infra/infra.dart b/lib/infra/infra.dart new file mode 100644 index 0000000..1bf41b0 --- /dev/null +++ b/lib/infra/infra.dart @@ -0,0 +1,2 @@ +export 'cache/cache.dart'; +export 'http/http.dart'; From 93461863717dfbabf958a4121604af1bffaf6558 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 22:48:27 -0300 Subject: [PATCH 42/91] refact: factories file adjustments --- lib/factories/factories.dart | 2 +- lib/factories/infra/cache/cache.dart | 1 + lib/factories/infra/infra.dart | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 lib/factories/infra/cache/cache.dart create mode 100644 lib/factories/infra/infra.dart diff --git a/lib/factories/factories.dart b/lib/factories/factories.dart index 7fe53be..f0bedc2 100644 --- a/lib/factories/factories.dart +++ b/lib/factories/factories.dart @@ -1,4 +1,4 @@ -export 'http/http.dart'; +export 'infra/infra.dart'; export 'presentation/presentation.dart'; export 'ui/ui.dart'; export 'usecases/usecases.dart'; diff --git a/lib/factories/infra/cache/cache.dart b/lib/factories/infra/cache/cache.dart new file mode 100644 index 0000000..bdd08a9 --- /dev/null +++ b/lib/factories/infra/cache/cache.dart @@ -0,0 +1 @@ +export 'local_storage_adapter_factory.dart'; diff --git a/lib/factories/infra/infra.dart b/lib/factories/infra/infra.dart new file mode 100644 index 0000000..1bf41b0 --- /dev/null +++ b/lib/factories/infra/infra.dart @@ -0,0 +1,2 @@ +export 'cache/cache.dart'; +export 'http/http.dart'; From 5464aa2173f56e6234f64d5210fefd05fed10ea7 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 23:06:56 -0300 Subject: [PATCH 43/91] feat: fill detail page with restaurant entity data --- .../restaurant_detail_page.dart | 48 ++++++++++--------- .../restaurant_detail/widgets/rating.dart | 6 ++- .../widgets/review_item.dart | 22 +++++---- .../widgets/review_list.dart | 25 ++++++++++ 4 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 lib/ui/pages/restaurant_detail/widgets/review_list.dart diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index ef77564..55ebbf0 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../../domain/entities/entities.dart'; import '../../../typography.dart'; import '../../widgets/widgets.dart'; @@ -8,9 +9,15 @@ part 'widgets/divider.dart'; part 'widgets/favorite_icon.dart'; part 'widgets/rating.dart'; part 'widgets/review_item.dart'; +part 'widgets/review_list.dart'; class RestaurantDetailPage extends StatelessWidget { - const RestaurantDetailPage({super.key}); + final RestaurantEntity _restaurant; + + const RestaurantDetailPage({ + super.key, + required RestaurantEntity restaurant, + }) : _restaurant = restaurant; @override Widget build(BuildContext context) { @@ -22,42 +29,39 @@ class RestaurantDetailPage extends StatelessWidget { onFavorite: () {}, ), ], - title: const Text( - 'Restaurant Name Goes Here And Wraps 2 Lines', + title: Text( + _restaurant.name, style: AppTextStyles.loraRegularHeadline, ), ), body: ListView( children: [ - Container( + SizedBox( height: 360, - color: Colors.red, + child: Image.network(_restaurant.heroImage), ), - const Padding( - padding: EdgeInsets.all(24), + Padding( + padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), - Status(true), + const Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + Status(_restaurant.isOpen), ], ), - _Divider(), - Text('Address', style: AppTextStyles.openRegularText), - SizedBox(height: 24), - Text('102 Lakeside Ave\nSeattle, WA 98122', style: AppTextStyles.openRegularTitleSemiBold), - _Divider(), - Text('Overall Rating', style: AppTextStyles.openRegularText), - SizedBox(height: 16), - _Rating(), - _Divider(), - Text('42 reviews', style: AppTextStyles.openRegularText), - SizedBox(height: 16), - _ReviewItem(), - _ReviewItem(), + const _Divider(), + const Text('Address', style: AppTextStyles.openRegularText), + const SizedBox(height: 24), + Text(_restaurant.address, style: AppTextStyles.openRegularTitleSemiBold), + const _Divider(), + const Text('Overall Rating', style: AppTextStyles.openRegularText), + const SizedBox(height: 16), + _Rating(_restaurant.rating.toString()), + const _Divider(), + _ReviewList(_restaurant.reviews), ], ), ), diff --git a/lib/ui/pages/restaurant_detail/widgets/rating.dart b/lib/ui/pages/restaurant_detail/widgets/rating.dart index 1ca7e80..0fce9ef 100644 --- a/lib/ui/pages/restaurant_detail/widgets/rating.dart +++ b/lib/ui/pages/restaurant_detail/widgets/rating.dart @@ -1,7 +1,9 @@ part of '../restaurant_detail_page.dart'; class _Rating extends StatelessWidget { - const _Rating(); + final String _rating; + + const _Rating(this._rating); @override Widget build(BuildContext context) { @@ -9,7 +11,7 @@ class _Rating extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - '4.6', + _rating, style: AppTextStyles.loraRegularHeadline.copyWith(fontSize: 28), ), const Padding( diff --git a/lib/ui/pages/restaurant_detail/widgets/review_item.dart b/lib/ui/pages/restaurant_detail/widgets/review_item.dart index 2b19438..cdd1ac1 100644 --- a/lib/ui/pages/restaurant_detail/widgets/review_item.dart +++ b/lib/ui/pages/restaurant_detail/widgets/review_item.dart @@ -1,33 +1,35 @@ part of '../restaurant_detail_page.dart'; class _ReviewItem extends StatelessWidget { - const _ReviewItem(); + final ReviewEntity _review; + + const _ReviewItem(this._review); @override Widget build(BuildContext context) { - return const Column( + return Column( children: [ - Stars(4), - SizedBox(height: 8), + Stars(_review.rating), + const SizedBox(height: 8), Text( - 'Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.', + _review.text, style: AppTextStyles.openRegularHeadline, ), - SizedBox(height: 8), + const SizedBox(height: 8), Row( children: [ SizedBox( height: 40, width: 40, child: CircleAvatar( - backgroundColor: Colors.grey, + child: Image.network(_review.user.imageUrl), ), ), - SizedBox(width: 8), - Text('User Name', style: AppTextStyles.openRegularText), + const SizedBox(width: 8), + Text(_review.user.name, style: AppTextStyles.openRegularText), ], ), - _Divider(verticalPadding: 16), + const _Divider(verticalPadding: 16), ], ); } diff --git a/lib/ui/pages/restaurant_detail/widgets/review_list.dart b/lib/ui/pages/restaurant_detail/widgets/review_list.dart new file mode 100644 index 0000000..ebf1379 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/review_list.dart @@ -0,0 +1,25 @@ +part of '../restaurant_detail_page.dart'; + +class _ReviewList extends StatelessWidget { + final List _reviews; + + const _ReviewList(this._reviews); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text('${_reviews.length} reviews', style: AppTextStyles.openRegularText), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + itemCount: _reviews.length, + itemBuilder: (context, index) { + final review = _reviews[index]; + return _ReviewItem(review); + }, + ), + ], + ); + } +} From 65be3a72abfd726e53e7d7d05dfba19b2bb5d68f Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 23:08:46 -0300 Subject: [PATCH 44/91] refact: remove app widget from main file --- ios/Podfile.lock | 23 ++++ ios/Runner.xcodeproj/project.pbxproj | 112 ++++++++++++++++++ .../contents.xcworkspacedata | 3 + lib/main.dart | 82 +------------ lib/restaurant_tour.dart | 24 ++++ 5 files changed, 163 insertions(+), 81 deletions(-) create mode 100644 ios/Podfile.lock create mode 100644 lib/restaurant_tour.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..39c22b7 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 182fb57..e52502d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5BF3A6EEB678A0BEDC7DC002 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025F475AA2CC185B321E613A /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B5FF564FF705753D0B264C84 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374876DA1A901122796EE1BC /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,10 +42,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 025F475AA2CC185B321E613A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 334A71043A1D8E2D8C6236B4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 374876DA1A901122796EE1BC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -55,13 +60,27 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CB89848961AA2D2EF6D7A465 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D1F1E58588222E5ED7C3A4EE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E6C96E8930E1BB81E2283805 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E7BD18E113576D7009DDC15B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EBBFC6439AB8493DBCDAE3DF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 35FCC4A6367841382A086E2A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BF3A6EEB678A0BEDC7DC002 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B5FF564FF705753D0B264C84 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,20 @@ path = RunnerTests; sourceTree = ""; }; + 737F57C08BCA69E04DC18677 /* Pods */ = { + isa = PBXGroup; + children = ( + CB89848961AA2D2EF6D7A465 /* Pods-Runner.debug.xcconfig */, + EBBFC6439AB8493DBCDAE3DF /* Pods-Runner.release.xcconfig */, + D1F1E58588222E5ED7C3A4EE /* Pods-Runner.profile.xcconfig */, + E6C96E8930E1BB81E2283805 /* Pods-RunnerTests.debug.xcconfig */, + E7BD18E113576D7009DDC15B /* Pods-RunnerTests.release.xcconfig */, + 334A71043A1D8E2D8C6236B4 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +127,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 737F57C08BCA69E04DC18677 /* Pods */, + D5B101D8DFED79F5616FE27C /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +156,15 @@ path = Runner; sourceTree = ""; }; + D5B101D8DFED79F5616FE27C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 374876DA1A901122796EE1BC /* Pods_Runner.framework */, + 025F475AA2CC185B321E613A /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 83D7EB3AE6CF6A51F3A0C342 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 35FCC4A6367841382A086E2A /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + ACC17BD020EA506076E8ECF9 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + AD620165592B4FBF3C4EE416 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -238,6 +286,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 83D7EB3AE6CF6A51F3A0C342 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +323,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + ACC17BD020EA506076E8ECF9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AD620165592B4FBF3C4EE416 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E6C96E8930E1BB81E2283805 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E7BD18E113576D7009DDC15B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 334A71043A1D8E2D8C6236B4 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/main.dart b/lib/main.dart index ae7012a..a819cbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,87 +1,7 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:restaurant_tour/models/restaurant.dart'; -import 'package:restaurant_tour/query.dart'; -const _apiKey = ''; -const _baseUrl = 'https://api.yelp.com/v3/graphql'; +import 'restaurant_tour.dart'; void main() { runApp(const RestaurantTour()); } - -class RestaurantTour extends StatelessWidget { - const RestaurantTour({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Restaurant Tour', - home: HomePage(), - ); - } -} - -// TODO: Architect code -// This is just a POC of the API integration -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - Future getRestaurants({int offset = 0}) async { - final headers = { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }; - - try { - final response = await http.post( - Uri.parse(_baseUrl), - headers: headers, - body: query(offset), - ); - - if (response.statusCode == 200) { - return RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ); - } else { - print('Failed to load restaurants: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error fetching restaurants: $e'); - return null; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - try { - final result = await getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/restaurant_tour.dart b/lib/restaurant_tour.dart new file mode 100644 index 0000000..a5cc804 --- /dev/null +++ b/lib/restaurant_tour.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'factories/factories.dart'; + +class RestaurantTour extends StatelessWidget { + const RestaurantTour({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Restaurant Tour', + initialRoute: '/', + routes: { + '/': (_) => makeRestaurantTourPage(), + }, + theme: ThemeData.from( + colorScheme: ColorScheme.fromSwatch( + backgroundColor: const Color(0xFFFAFAFA), + ), + ), + ); + } +} From 66b8b3a51784f21c276e22b16ae141ea1381364e Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 23:10:48 -0300 Subject: [PATCH 45/91] refact: remove query file --- lib/query.dart | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 lib/query.dart diff --git a/lib/query.dart b/lib/query.dart deleted file mode 100644 index 7a8993b..0000000 --- a/lib/query.dart +++ /dev/null @@ -1,34 +0,0 @@ -String query(int offset) => ''' - query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total - business { - id - name - price - rating - photos - reviews { - id - rating - text - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address - } - } - } - } - '''; From cc422b070183bedb31e85ea6b920fb5966d4d079 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 23:21:31 -0300 Subject: [PATCH 46/91] refact: change from graphql_get_restaurants to remote_get_restaurants --- ...aphql_get_restaurants.dart => remote_get_restaurants.dart} | 4 ++-- lib/data/usecases/usecases.dart | 2 +- lib/factories/usecases/get_restaurants_factory.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename lib/data/usecases/{graphql_get_restaurants.dart => remote_get_restaurants.dart} (94%) diff --git a/lib/data/usecases/graphql_get_restaurants.dart b/lib/data/usecases/remote_get_restaurants.dart similarity index 94% rename from lib/data/usecases/graphql_get_restaurants.dart rename to lib/data/usecases/remote_get_restaurants.dart index d401937..fe1d33f 100644 --- a/lib/data/usecases/graphql_get_restaurants.dart +++ b/lib/data/usecases/remote_get_restaurants.dart @@ -6,11 +6,11 @@ import '../../domain/usecases/usecases.dart'; import '../../infra/http/http.dart'; import '../models/models.dart'; -class GraphqlGetRestaurants implements GetRestaurants { +class RemoteGetRestaurants implements GetRestaurants { final HttpClient _client; final String _url; - const GraphqlGetRestaurants({ + const RemoteGetRestaurants({ required HttpClient client, required String url, }) : _client = client, diff --git a/lib/data/usecases/usecases.dart b/lib/data/usecases/usecases.dart index d355a02..a2a3681 100644 --- a/lib/data/usecases/usecases.dart +++ b/lib/data/usecases/usecases.dart @@ -1 +1 @@ -export 'graphql_get_restaurants.dart'; +export 'remote_get_restaurants.dart'; diff --git a/lib/factories/usecases/get_restaurants_factory.dart b/lib/factories/usecases/get_restaurants_factory.dart index daefb35..0c57fe7 100644 --- a/lib/factories/usecases/get_restaurants_factory.dart +++ b/lib/factories/usecases/get_restaurants_factory.dart @@ -5,5 +5,5 @@ import '../factories.dart'; GetRestaurants makeGetRestaurants() { final client = makeHttpAdapter(); final url = makeApiUrl(); - return GraphqlGetRestaurants(client: client, url: url); + return RemoteGetRestaurants(client: client, url: url); } From 110e1e500e7d9d61b927c107fbac4e37a32a73c5 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Sun, 15 Sep 2024 23:23:33 -0300 Subject: [PATCH 47/91] feat: add save, get, update protocols --- lib/domain/entities/entities.dart | 1 + .../entities/favorite_restaurant_entity.dart | 17 +++++++++++++++++ .../usecases/get_favorite_restaurants.dart | 5 +++++ .../remove_restaurant_from_favorites.dart | 3 +++ .../usecases/save_restaurant_as_favorite.dart | 5 +++++ lib/domain/usecases/usecases.dart | 3 +++ 6 files changed, 34 insertions(+) create mode 100644 lib/domain/entities/favorite_restaurant_entity.dart create mode 100644 lib/domain/usecases/get_favorite_restaurants.dart create mode 100644 lib/domain/usecases/remove_restaurant_from_favorites.dart create mode 100644 lib/domain/usecases/save_restaurant_as_favorite.dart diff --git a/lib/domain/entities/entities.dart b/lib/domain/entities/entities.dart index 412fedb..0dbd033 100644 --- a/lib/domain/entities/entities.dart +++ b/lib/domain/entities/entities.dart @@ -1,4 +1,5 @@ export 'category_entity.dart'; +export 'favorite_restaurant_entity.dart'; export 'restaurant_entity.dart'; export 'review_entity.dart'; export 'user_entity.dart'; diff --git a/lib/domain/entities/favorite_restaurant_entity.dart b/lib/domain/entities/favorite_restaurant_entity.dart new file mode 100644 index 0000000..db57d90 --- /dev/null +++ b/lib/domain/entities/favorite_restaurant_entity.dart @@ -0,0 +1,17 @@ +import 'restaurant_entity.dart'; + +class FavoriteRestaurantEntity extends RestaurantEntity { + FavoriteRestaurantEntity({ + required super.id, + required super.categories, + required super.displayCategory, + required super.heroImage, + required super.isOpen, + required super.address, + required super.name, + required super.photos, + required super.price, + required super.rating, + required super.reviews, + }); +} diff --git a/lib/domain/usecases/get_favorite_restaurants.dart b/lib/domain/usecases/get_favorite_restaurants.dart new file mode 100644 index 0000000..20e7d64 --- /dev/null +++ b/lib/domain/usecases/get_favorite_restaurants.dart @@ -0,0 +1,5 @@ +import '../entities/entities.dart'; + +abstract class GetFavoriteRestaurants { + Future> call(); +} diff --git a/lib/domain/usecases/remove_restaurant_from_favorites.dart b/lib/domain/usecases/remove_restaurant_from_favorites.dart new file mode 100644 index 0000000..2797227 --- /dev/null +++ b/lib/domain/usecases/remove_restaurant_from_favorites.dart @@ -0,0 +1,3 @@ +abstract class RemoveArticleFromFavorites { + Future call(String key); +} diff --git a/lib/domain/usecases/save_restaurant_as_favorite.dart b/lib/domain/usecases/save_restaurant_as_favorite.dart new file mode 100644 index 0000000..2a2bd6c --- /dev/null +++ b/lib/domain/usecases/save_restaurant_as_favorite.dart @@ -0,0 +1,5 @@ +import '../entities/entities.dart'; + +abstract class SaveRestaurantAsFavorite { + Future call(RestaurantEntity restaurant); +} diff --git a/lib/domain/usecases/usecases.dart b/lib/domain/usecases/usecases.dart index 0082d63..85e90e7 100644 --- a/lib/domain/usecases/usecases.dart +++ b/lib/domain/usecases/usecases.dart @@ -1 +1,4 @@ +export 'get_favorite_restaurants.dart'; export 'get_restaurants.dart'; +export 'remove_restaurant_from_favorites.dart'; +export 'save_restaurant_as_favorite.dart'; From 6f9df0da8b5aa46a286553b9d534ff55414e745b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 00:03:32 -0300 Subject: [PATCH 48/91] feat: add cache data usecases --- lib/data/models/category_model.dart | 7 +++++ lib/data/models/restaurant_model.dart | 31 +++++++++++++++++++ lib/data/models/review_model.dart | 9 ++++++ lib/data/models/user_model.dart | 8 +++++ .../local_get_favorite_restaurants.dart | 27 ++++++++++++++++ .../local_save_favorite_restaurants.dart | 26 ++++++++++++++++ lib/data/usecases/usecases.dart | 2 ++ .../remove_restaurant_from_favorites.dart | 3 -- .../usecases/save_favorite_restaurants.dart | 5 +++ .../usecases/save_restaurant_as_favorite.dart | 5 --- lib/domain/usecases/usecases.dart | 3 +- .../get_favorite_restaurants_factory.dart | 8 +++++ .../save_favorite_restaurants_factory.dart | 8 +++++ lib/factories/usecases/usecases.dart | 2 ++ 14 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 lib/data/usecases/local_get_favorite_restaurants.dart create mode 100644 lib/data/usecases/local_save_favorite_restaurants.dart delete mode 100644 lib/domain/usecases/remove_restaurant_from_favorites.dart create mode 100644 lib/domain/usecases/save_favorite_restaurants.dart delete mode 100644 lib/domain/usecases/save_restaurant_as_favorite.dart create mode 100644 lib/factories/usecases/get_favorite_restaurants_factory.dart create mode 100644 lib/factories/usecases/save_favorite_restaurants_factory.dart diff --git a/lib/data/models/category_model.dart b/lib/data/models/category_model.dart index c0d147a..6608a3d 100644 --- a/lib/data/models/category_model.dart +++ b/lib/data/models/category_model.dart @@ -28,6 +28,13 @@ class CategoryModel { } } + Map toJson() { + return { + 'title': title, + 'alias': alias, + }; + } + CategoryEntity toEntity() { return CategoryEntity( title: title, diff --git a/lib/data/models/restaurant_model.dart b/lib/data/models/restaurant_model.dart index d8cb4cc..0dc7a4d 100644 --- a/lib/data/models/restaurant_model.dart +++ b/lib/data/models/restaurant_model.dart @@ -65,6 +65,21 @@ class RestaurantModel { } } + Map toJson() { + return { + 'id': id, + 'categories': categories.map((category) => category.toJson()).toList(), + 'displayCategory': displayCategory, + 'heroImage': heroImage, + 'isOpen': isOpen, + 'address': address, + 'photos': photos, + 'price': price, + 'rating': rating, + 'reviews': reviews.map((review) => review.toJson()).toList(), + }; + } + RestaurantEntity toEntity() { return RestaurantEntity( id: id, @@ -80,4 +95,20 @@ class RestaurantModel { reviews: reviews.map((review) => review.toEntity()).toList(), ); } + + FavoriteRestaurantEntity toFavoriteEntity() { + return FavoriteRestaurantEntity( + id: id, + rating: rating, + categories: categories.map((category) => category.toEntity()).toList(), + displayCategory: displayCategory, + heroImage: heroImage, + isOpen: isOpen, + address: address, + name: name, + photos: photos, + price: price, + reviews: reviews.map((review) => review.toEntity()).toList(), + ); + } } diff --git a/lib/data/models/review_model.dart b/lib/data/models/review_model.dart index 0deb0e8..72c6b22 100644 --- a/lib/data/models/review_model.dart +++ b/lib/data/models/review_model.dart @@ -37,6 +37,15 @@ class ReviewModel { } } + Map toJson() { + return { + 'id': id, + 'rating': rating, + 'text': text, + 'user': user.toJson(), + }; + } + ReviewEntity toEntity() { return ReviewEntity( id: id, diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 8ea716e..6137475 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -32,6 +32,14 @@ class UserModel { } } + Map toJson() { + return { + 'id': id, + 'imageUrl': imageUrl, + 'name': name, + }; + } + UserEntity toEntity() { return UserEntity( id: id, diff --git a/lib/data/usecases/local_get_favorite_restaurants.dart b/lib/data/usecases/local_get_favorite_restaurants.dart new file mode 100644 index 0000000..3f4692c --- /dev/null +++ b/lib/data/usecases/local_get_favorite_restaurants.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import '../../domain/usecases/usecases.dart'; +import '../../infra/infra.dart'; +import '../models/models.dart'; + +class LocalGetFavoriteRestaurants implements GetFavoriteRestaurants { + final FetchCache cache; + + LocalGetFavoriteRestaurants({ + required this.cache, + }); + + @override + Future> call() async { + try { + final result = await cache.fetch('favorite_restaurants'); + if (result == null) return []; + final jsonDecoded = jsonDecode(result); + return (jsonDecoded as List).map((article) => RestaurantModel.fromJson(article).toFavoriteEntity()).toList(); + } catch (_) { + throw DomainError.unexpected; + } + } +} diff --git a/lib/data/usecases/local_save_favorite_restaurants.dart b/lib/data/usecases/local_save_favorite_restaurants.dart new file mode 100644 index 0000000..2492a3f --- /dev/null +++ b/lib/data/usecases/local_save_favorite_restaurants.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import '../../domain/usecases/usecases.dart'; +import '../../infra/cache/cache.dart'; +import '../models/models.dart'; + +class LocalSaveFavoriteRestaurants implements SaveFavoriteRestaurants { + final SaveCache cache; + + LocalSaveFavoriteRestaurants({required this.cache}); + + @override + Future call(List restaurants) async { + try { + final value = jsonEncode(restaurants.map((restaurant) => RestaurantModel.fromEntity(restaurant).toJson()).toList()); + return await cache.save( + key: 'favorite_restaurants', + value: value, + ); + } catch (_) { + throw DomainError.unexpected; + } + } +} diff --git a/lib/data/usecases/usecases.dart b/lib/data/usecases/usecases.dart index a2a3681..53ee661 100644 --- a/lib/data/usecases/usecases.dart +++ b/lib/data/usecases/usecases.dart @@ -1 +1,3 @@ +export 'local_get_favorite_restaurants.dart'; +export 'local_save_favorite_restaurants.dart'; export 'remote_get_restaurants.dart'; diff --git a/lib/domain/usecases/remove_restaurant_from_favorites.dart b/lib/domain/usecases/remove_restaurant_from_favorites.dart deleted file mode 100644 index 2797227..0000000 --- a/lib/domain/usecases/remove_restaurant_from_favorites.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class RemoveArticleFromFavorites { - Future call(String key); -} diff --git a/lib/domain/usecases/save_favorite_restaurants.dart b/lib/domain/usecases/save_favorite_restaurants.dart new file mode 100644 index 0000000..95075ac --- /dev/null +++ b/lib/domain/usecases/save_favorite_restaurants.dart @@ -0,0 +1,5 @@ +import '../entities/entities.dart'; + +abstract class SaveFavoriteRestaurants { + Future call(List restaurant); +} diff --git a/lib/domain/usecases/save_restaurant_as_favorite.dart b/lib/domain/usecases/save_restaurant_as_favorite.dart deleted file mode 100644 index 2a2bd6c..0000000 --- a/lib/domain/usecases/save_restaurant_as_favorite.dart +++ /dev/null @@ -1,5 +0,0 @@ -import '../entities/entities.dart'; - -abstract class SaveRestaurantAsFavorite { - Future call(RestaurantEntity restaurant); -} diff --git a/lib/domain/usecases/usecases.dart b/lib/domain/usecases/usecases.dart index 85e90e7..258616d 100644 --- a/lib/domain/usecases/usecases.dart +++ b/lib/domain/usecases/usecases.dart @@ -1,4 +1,3 @@ export 'get_favorite_restaurants.dart'; export 'get_restaurants.dart'; -export 'remove_restaurant_from_favorites.dart'; -export 'save_restaurant_as_favorite.dart'; +export 'save_favorite_restaurants.dart'; diff --git a/lib/factories/usecases/get_favorite_restaurants_factory.dart b/lib/factories/usecases/get_favorite_restaurants_factory.dart new file mode 100644 index 0000000..14b6664 --- /dev/null +++ b/lib/factories/usecases/get_favorite_restaurants_factory.dart @@ -0,0 +1,8 @@ +import '../../data/usecases/usecases.dart'; +import '../../domain/usecases/usecases.dart'; +import '../factories.dart'; + +GetFavoriteRestaurants makeGetFavoriteRestaurants() { + final localStorageAdapter = makeLocalStorageAdapter(); + return LocalGetFavoriteRestaurants(cache: localStorageAdapter); +} diff --git a/lib/factories/usecases/save_favorite_restaurants_factory.dart b/lib/factories/usecases/save_favorite_restaurants_factory.dart new file mode 100644 index 0000000..7cbfae8 --- /dev/null +++ b/lib/factories/usecases/save_favorite_restaurants_factory.dart @@ -0,0 +1,8 @@ +import '../../data/usecases/usecases.dart'; +import '../../domain/usecases/usecases.dart'; +import '../factories.dart'; + +SaveFavoriteRestaurants makeSaveFavoriteRestaurants() { + final localStorageAdapter = makeLocalStorageAdapter(); + return LocalSaveFavoriteRestaurants(cache: localStorageAdapter); +} diff --git a/lib/factories/usecases/usecases.dart b/lib/factories/usecases/usecases.dart index 21cfa4d..fa90c3c 100644 --- a/lib/factories/usecases/usecases.dart +++ b/lib/factories/usecases/usecases.dart @@ -1 +1,3 @@ +export 'get_favorite_restaurants_factory.dart'; export 'get_restaurants_factory.dart'; +export 'save_favorite_restaurants_factory.dart'; From 7bd10a8a724101f1b2ee61f8d298562334b29b0a Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 12:40:09 -0300 Subject: [PATCH 49/91] refact: add error content ui to your own widget --- .../restaurant_tour/restaurant_tour_page.dart | 9 +- .../widgets/error_content.dart | 20 +++ pubspec.lock | 132 +++++++++--------- 3 files changed, 88 insertions(+), 73 deletions(-) create mode 100644 lib/ui/pages/restaurant_tour/widgets/error_content.dart diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 05bd450..bf9cca2 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -8,6 +8,7 @@ import '../../widgets/widgets.dart'; import '../pages.dart'; part 'widgets/app_bar.dart'; +part 'widgets/error_content.dart'; part 'widgets/image.dart'; part 'widgets/restaurant_item.dart'; part 'widgets/restaurant_list.dart'; @@ -63,13 +64,7 @@ class _RestaurantTourPageState extends State { } if (state is RestaurantErrorState) { - return Center( - child: Text( - state.message, - style: AppTextStyles.openRegularHeadline, - textAlign: TextAlign.center, - ), - ); + return _ErrorContent(state.message); } return const TabBarView( diff --git a/lib/ui/pages/restaurant_tour/widgets/error_content.dart b/lib/ui/pages/restaurant_tour/widgets/error_content.dart new file mode 100644 index 0000000..175b411 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/error_content.dart @@ -0,0 +1,20 @@ +part of '../restaurant_tour_page.dart'; + +class _ErrorContent extends StatelessWidget { + final String message; + + const _ErrorContent( + this.message, + ); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + message, + style: AppTextStyles.openRegularHeadline, + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 452d2fb..70dd6f0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.1" built_collection: dependency: transitive description: @@ -121,22 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -165,26 +157,26 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" fake_async: dependency: transitive description: @@ -205,18 +197,18 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -252,26 +244,26 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http: dependency: "direct main" description: @@ -292,18 +284,18 @@ packages: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: @@ -364,10 +356,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -412,10 +404,10 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -468,10 +460,10 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" provider: dependency: transitive description: @@ -484,18 +476,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" shared_preferences: dependency: "direct main" description: @@ -556,18 +548,18 @@ packages: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -657,10 +649,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_math: dependency: transitive description: @@ -681,10 +673,10 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: @@ -693,14 +685,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.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: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" xdg_directories: dependency: transitive description: @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" From ec4104d4b37360dba918feefd932d3c95b33fded Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 12:52:38 -0300 Subject: [PATCH 50/91] feat: add get favorite restaurants on restaurant tour presenter --- .../restaurant_tour_presenter_factory.dart | 5 ++- .../cubit_restaurant_tour_presenter.dart | 33 +++++++++++++++---- .../restaurant_tour_presenter.dart | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/factories/presentation/restaurant_tour_presenter_factory.dart b/lib/factories/presentation/restaurant_tour_presenter_factory.dart index 68bfbc2..593651b 100644 --- a/lib/factories/presentation/restaurant_tour_presenter_factory.dart +++ b/lib/factories/presentation/restaurant_tour_presenter_factory.dart @@ -3,5 +3,8 @@ import '../../ui/pages/pages.dart'; import '../factories.dart'; RestaurantTourPresenter makeRestaurantTourPresenter() { - return CubitRestaurantTourPresenter(getRestaurants: makeGetRestaurants()); + return CubitRestaurantTourPresenter( + getRestaurants: makeGetRestaurants(), + getFavoriteRestaurants: makeGetFavoriteRestaurants(), + ); } diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 53a91c6..e2c7770 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/domain/usecases/get_restaurants.dart'; import '../../domain/entities/entities.dart'; import '../../domain/usecases/usecases.dart'; @@ -9,22 +10,41 @@ part 'states.dart'; class CubitRestaurantTourPresenter extends Cubit implements RestaurantTourPresenter { final GetRestaurants _getRestaurants; + final GetFavoriteRestaurants _getFavoriteRestaurants; - CubitRestaurantTourPresenter({required GetRestaurants getRestaurants}) - : _getRestaurants = getRestaurants, + CubitRestaurantTourPresenter({ + required GetRestaurants getRestaurants, + required GetFavoriteRestaurants getFavoriteRestaurants, + }) : _getRestaurants = getRestaurants, + _getFavoriteRestaurants = getFavoriteRestaurants, super(RestaurantInitialState()) { + _load(); + } + + void _load() { getAllRestaurants(); + getFavoriteRestaurants(); } final _restaurantList = []; + final _favoriteRestaurantList = []; @override List get restaurantList => _restaurantList; @override - Future getFavoriteRestaurants() { - // TODO: implement getFavoriteRestaurants - throw UnimplementedError(); + List get favoriteRestaurantList => _favoriteRestaurantList; + + @override + Future getFavoriteRestaurants() async { + try { + emit(RestaurantLoadingState()); + _favoriteRestaurantList.addAll(await _getFavoriteRestaurants()); + emit(RestaurantSuccessState()); + } catch (e, s) { + debugPrintStack(label: 'Error - getFavoriteRestaurants - $e', stackTrace: s); + emit(RestaurantErrorState('There was an error \nwhen get favorite restaurants.')); + } } @override @@ -34,12 +54,13 @@ class CubitRestaurantTourPresenter extends Cubit implements Res _restaurantList.addAll(await _getRestaurants()); emit(RestaurantSuccessState()); } catch (e, s) { - debugPrintStack(label: 'Error - $e', stackTrace: s); + debugPrintStack(label: 'Error - getAllRestaurants - $e', stackTrace: s); emit(RestaurantErrorState('There was an error \nwhen get restaurants.')); } } void dispose() { _restaurantList.clear(); + _favoriteRestaurantList.clear(); } } diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart index 1ef5c90..f88f394 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -3,5 +3,6 @@ import '../../../domain/entities/entities.dart'; abstract class RestaurantTourPresenter { List get restaurantList; Future getAllRestaurants(); + List get favoriteRestaurantList; Future getFavoriteRestaurants(); } From 0b0daf6a03c9331f071d0df7bfb67fc02965a27d Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 12:56:47 -0300 Subject: [PATCH 51/91] feat: add save restaurants use case on restaurant tour presenter --- .../restaurant_tour_presenter_factory.dart | 1 + .../cubit_restaurant_tour_presenter.dart | 13 ++++++++++++- .../restaurant_tour/restaurant_tour_presenter.dart | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/factories/presentation/restaurant_tour_presenter_factory.dart b/lib/factories/presentation/restaurant_tour_presenter_factory.dart index 593651b..17bf3a6 100644 --- a/lib/factories/presentation/restaurant_tour_presenter_factory.dart +++ b/lib/factories/presentation/restaurant_tour_presenter_factory.dart @@ -6,5 +6,6 @@ RestaurantTourPresenter makeRestaurantTourPresenter() { return CubitRestaurantTourPresenter( getRestaurants: makeGetRestaurants(), getFavoriteRestaurants: makeGetFavoriteRestaurants(), + saveFavoriteRestaurants: makeSaveFavoriteRestaurants(), ); } diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index e2c7770..772b8f1 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/domain/usecases/get_restaurants.dart'; import '../../domain/entities/entities.dart'; import '../../domain/usecases/usecases.dart'; @@ -11,12 +10,15 @@ part 'states.dart'; class CubitRestaurantTourPresenter extends Cubit implements RestaurantTourPresenter { final GetRestaurants _getRestaurants; final GetFavoriteRestaurants _getFavoriteRestaurants; + final SaveFavoriteRestaurants _saveFavoriteRestaurants; CubitRestaurantTourPresenter({ required GetRestaurants getRestaurants, required GetFavoriteRestaurants getFavoriteRestaurants, + required SaveFavoriteRestaurants saveFavoriteRestaurants, }) : _getRestaurants = getRestaurants, _getFavoriteRestaurants = getFavoriteRestaurants, + _saveFavoriteRestaurants = saveFavoriteRestaurants, super(RestaurantInitialState()) { _load(); } @@ -59,6 +61,15 @@ class CubitRestaurantTourPresenter extends Cubit implements Res } } + @override + Future addFavoriteRestaurants() async { + try { + _saveFavoriteRestaurants(_favoriteRestaurantList); + } catch (e, s) { + debugPrintStack(label: 'Error - addFavoriteRestaurants - $e', stackTrace: s); + } + } + void dispose() { _restaurantList.clear(); _favoriteRestaurantList.clear(); diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart index f88f394..ae0f926 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -5,4 +5,5 @@ abstract class RestaurantTourPresenter { Future getAllRestaurants(); List get favoriteRestaurantList; Future getFavoriteRestaurants(); + Future addFavoriteRestaurants(); } From 642786264291b3c6d4f3bde62b93ff5f16a23c8b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 14:17:18 -0300 Subject: [PATCH 52/91] feat: match favorite restaurants on presenter --- .../cubit_restaurant_tour_presenter.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 772b8f1..b39282e 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -20,12 +20,20 @@ class CubitRestaurantTourPresenter extends Cubit implements Res _getFavoriteRestaurants = getFavoriteRestaurants, _saveFavoriteRestaurants = saveFavoriteRestaurants, super(RestaurantInitialState()) { - _load(); + _loadData(); } - void _load() { - getAllRestaurants(); - getFavoriteRestaurants(); + Future _loadData() async { + await getFavoriteRestaurants(); + await getAllRestaurants(); + _matchFavoriteRestaurants(); + } + + void _matchFavoriteRestaurants() { + for (var favoriteRestaurant in _favoriteRestaurantList) { + final index = _restaurantList.indexWhere((restaurant) => restaurant.id == favoriteRestaurant.id); + if (index != -1) _restaurantList[index] = favoriteRestaurant; + } } final _restaurantList = []; From acefe045caef3263eb93a4a7b167c19f7918ef2b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 15:08:20 -0300 Subject: [PATCH 53/91] feat: handling errors on restaurant tour ui --- .../cubit_restaurant_tour_presenter.dart | 4 +-- .../restaurant_tour/restaurant_tour_page.dart | 32 +++++++++++-------- .../restaurant_tour/widgets/app_bar.dart | 10 +++++- .../widgets/favorite_restaurant_list.dart | 21 ++++++++++++ ...rror_content.dart => message_content.dart} | 4 +-- .../widgets/restaurant_list.dart | 18 +++++++---- .../restaurant_tour/widgets/snack_bar.dart | 17 ++++++++++ 7 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart rename lib/ui/pages/restaurant_tour/widgets/{error_content.dart => message_content.dart} (80%) create mode 100644 lib/ui/pages/restaurant_tour/widgets/snack_bar.dart diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index b39282e..69af5e6 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -53,7 +53,7 @@ class CubitRestaurantTourPresenter extends Cubit implements Res emit(RestaurantSuccessState()); } catch (e, s) { debugPrintStack(label: 'Error - getFavoriteRestaurants - $e', stackTrace: s); - emit(RestaurantErrorState('There was an error \nwhen get favorite restaurants.')); + emit(RestaurantErrorState('An error occurred when get favorite restaurants')); } } @@ -65,7 +65,7 @@ class CubitRestaurantTourPresenter extends Cubit implements Res emit(RestaurantSuccessState()); } catch (e, s) { debugPrintStack(label: 'Error - getAllRestaurants - $e', stackTrace: s); - emit(RestaurantErrorState('There was an error \nwhen get restaurants.')); + emit(RestaurantErrorState('An error occurred when searching for the restaurants')); } } diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index bf9cca2..4d0f623 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart'; import '../../../domain/entities/entities.dart'; import '../../../presentation/presentation.dart'; @@ -8,10 +9,12 @@ import '../../widgets/widgets.dart'; import '../pages.dart'; part 'widgets/app_bar.dart'; -part 'widgets/error_content.dart'; +part 'widgets/message_content.dart'; part 'widgets/image.dart'; +part 'widgets/favorite_restaurant_list.dart'; part 'widgets/restaurant_item.dart'; part 'widgets/restaurant_list.dart'; +part 'widgets/snack_bar.dart'; class RestaurantTourPage extends StatefulWidget { final RestaurantTourPresenter _presenter; @@ -25,15 +28,18 @@ class RestaurantTourPage extends StatefulWidget { State createState() => _RestaurantTourPageState(); } -class _RestaurantTourPageState extends State { +class _RestaurantTourPageState extends State with SingleTickerProviderStateMixin { late final CubitRestaurantTourPresenter _bloc; + late final TabController _tabController; void _setUp() { _bloc = widget._presenter as CubitRestaurantTourPresenter; + _tabController = TabController(length: 2, vsync: this); } void _dispose() { _bloc.dispose(); + _tabController.dispose(); } @override @@ -53,25 +59,25 @@ class _RestaurantTourPageState extends State { return BlocProvider( create: (_) => _bloc, child: DefaultTabController( - length: 2, + length: _tabController.length, child: Scaffold( - appBar: const _AppBar(), - body: BlocBuilder( + appBar: _AppBar(_tabController), + body: BlocConsumer( + listener: (context, state) { + if (state is RestaurantErrorState) showSnackBar(context, state: state); + }, bloc: _bloc, builder: (context, state) { if (state is RestaurantLoadingState) { return const Center(child: CircularLoading()); } - if (state is RestaurantErrorState) { - return _ErrorContent(state.message); - } - - return const TabBarView( - physics: NeverScrollableScrollPhysics(), - children: [ + return TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ _RestaurantList(), - Center(child: Text('My Favorites Content')), + _FavoriteRestaurantList(), ], ); }, diff --git a/lib/ui/pages/restaurant_tour/widgets/app_bar.dart b/lib/ui/pages/restaurant_tour/widgets/app_bar.dart index 83f08f6..d81d357 100644 --- a/lib/ui/pages/restaurant_tour/widgets/app_bar.dart +++ b/lib/ui/pages/restaurant_tour/widgets/app_bar.dart @@ -1,7 +1,9 @@ part of '../restaurant_tour_page.dart'; class _AppBar extends StatefulWidget implements PreferredSizeWidget { - const _AppBar() : preferredSize = const Size.fromHeight(kToolbarHeight + 32); + final TabController _tabController; + + const _AppBar(this._tabController) : preferredSize = const Size.fromHeight(kToolbarHeight + 32); @override final Size preferredSize; @@ -11,6 +13,10 @@ class _AppBar extends StatefulWidget implements PreferredSizeWidget { } class _AppBarState extends State<_AppBar> { + void _onTabBarTapped(int index) { + widget._tabController.animateTo(index); + } + @override Widget build(BuildContext context) { return AppBar( @@ -24,6 +30,8 @@ class _AppBarState extends State<_AppBar> { color: Colors.white, shadowColor: Colors.black.withOpacity(0.4), child: TabBar( + controller: widget._tabController, + onTap: _onTabBarTapped, tabAlignment: TabAlignment.center, splashFactory: NoSplash.splashFactory, overlayColor: const WidgetStatePropertyAll(Colors.transparent), diff --git a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart new file mode 100644 index 0000000..e5e38ec --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart @@ -0,0 +1,21 @@ +part of '../restaurant_tour_page.dart'; + +class _FavoriteRestaurantList extends StatelessWidget { + const _FavoriteRestaurantList(); + + @override + Widget build(BuildContext context) { + final favoriteRestaurantList = context.read().favoriteRestaurantList; + if (favoriteRestaurantList.isEmpty) { + return const _MessageContent('No favorites.'); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemBuilder: (itemBuilder, index) { + final restaurant = favoriteRestaurantList[index]; + return _RestaurantItem(restaurant); + }, + ); + } + } +} diff --git a/lib/ui/pages/restaurant_tour/widgets/error_content.dart b/lib/ui/pages/restaurant_tour/widgets/message_content.dart similarity index 80% rename from lib/ui/pages/restaurant_tour/widgets/error_content.dart rename to lib/ui/pages/restaurant_tour/widgets/message_content.dart index 175b411..eb6c3cf 100644 --- a/lib/ui/pages/restaurant_tour/widgets/error_content.dart +++ b/lib/ui/pages/restaurant_tour/widgets/message_content.dart @@ -1,9 +1,9 @@ part of '../restaurant_tour_page.dart'; -class _ErrorContent extends StatelessWidget { +class _MessageContent extends StatelessWidget { final String message; - const _ErrorContent( + const _MessageContent( this.message, ); diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart index 16ba219..b568f9d 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart @@ -6,12 +6,16 @@ class _RestaurantList extends StatelessWidget { @override Widget build(BuildContext context) { final restaurantList = context.read().restaurantList; - return ListView.builder( - padding: const EdgeInsets.all(16), - itemBuilder: (itemBuilder, index) { - final restaurant = restaurantList[index]; - return _RestaurantItem(restaurant); - }, - ); + if (restaurantList.isEmpty) { + return const _MessageContent('No restaurants.'); + } else { + return ListView.builder( + padding: const EdgeInsets.all(16), + itemBuilder: (itemBuilder, index) { + final restaurant = restaurantList[index]; + return _RestaurantItem(restaurant); + }, + ); + } } } diff --git a/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart b/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart new file mode 100644 index 0000000..6f65a82 --- /dev/null +++ b/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart @@ -0,0 +1,17 @@ +part of '../restaurant_tour_page.dart'; + +void showSnackBar(BuildContext context, {required RestaurantErrorState state}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + elevation: 0, + backgroundColor: Colors.red.withOpacity(.8), + duration: const Duration(milliseconds: 2000), + content: Text( + state.message, + style: AppTextStyles.openRegularHeadline.copyWith( + color: Colors.white, + ), + ), + ), + ); +} From 5330656a800ce82ff90bf74145193c0b1d9b1d67 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 15:50:56 -0300 Subject: [PATCH 54/91] feat: add general ui adjusments --- lib/ui/pages/pages.dart | 1 + .../restaurant_detail/restaurant_detail.dart | 1 + .../restaurant_detail_page.dart | 8 ++++++-- .../widgets/favorite_icon.dart | 4 +++- .../restaurant_detail/widgets/review_item.dart | 1 + .../restaurant_detail/widgets/review_list.dart | 2 ++ .../restaurant_tour/restaurant_tour_page.dart | 2 -- .../restaurant_tour_presenter.dart | 4 ++-- .../widgets/favorite_restaurant_list.dart | 6 ++++-- .../widgets/restaurant_item.dart | 18 +++++++++++++++--- .../widgets/restaurant_list.dart | 6 ++++-- 11 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 lib/ui/pages/restaurant_detail/restaurant_detail.dart diff --git a/lib/ui/pages/pages.dart b/lib/ui/pages/pages.dart index badd2fd..0c255b1 100644 --- a/lib/ui/pages/pages.dart +++ b/lib/ui/pages/pages.dart @@ -1 +1,2 @@ +export 'restaurant_detail/restaurant_detail.dart'; export 'restaurant_tour/restaurant_tour.dart'; diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail.dart b/lib/ui/pages/restaurant_detail/restaurant_detail.dart new file mode 100644 index 0000000..ae8cfbd --- /dev/null +++ b/lib/ui/pages/restaurant_detail/restaurant_detail.dart @@ -0,0 +1 @@ +export 'restaurant_detail_page.dart'; diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index 55ebbf0..8392dc8 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -13,11 +13,14 @@ part 'widgets/review_list.dart'; class RestaurantDetailPage extends StatelessWidget { final RestaurantEntity _restaurant; + final VoidCallback _onFavorite; const RestaurantDetailPage({ super.key, required RestaurantEntity restaurant, - }) : _restaurant = restaurant; + required VoidCallback onFavorite, + }) : _restaurant = restaurant, + _onFavorite = onFavorite; @override Widget build(BuildContext context) { @@ -26,7 +29,8 @@ class RestaurantDetailPage extends StatelessWidget { leading: const _ArrowBackIcon(), actions: [ _FavoriteIcon( - onFavorite: () {}, + isFavorite: _restaurant is FavoriteRestaurantEntity, + onFavorite: _onFavorite, ), ], title: Text( diff --git a/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart index 4dd8e6e..b8be4a3 100644 --- a/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart +++ b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart @@ -1,9 +1,11 @@ part of '../restaurant_detail_page.dart'; class _FavoriteIcon extends StatelessWidget { + final bool isFavorite; final VoidCallback _onFavorite; const _FavoriteIcon({ + this.isFavorite = false, required VoidCallback onFavorite, }) : _onFavorite = onFavorite; @@ -13,7 +15,7 @@ class _FavoriteIcon extends StatelessWidget { padding: const EdgeInsets.only(right: 16), child: GestureDetector( onDoubleTap: _onFavorite, - child: const Icon(Icons.favorite_border), + child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border), ), ); } diff --git a/lib/ui/pages/restaurant_detail/widgets/review_item.dart b/lib/ui/pages/restaurant_detail/widgets/review_item.dart index cdd1ac1..4bbe9c5 100644 --- a/lib/ui/pages/restaurant_detail/widgets/review_item.dart +++ b/lib/ui/pages/restaurant_detail/widgets/review_item.dart @@ -8,6 +8,7 @@ class _ReviewItem extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Stars(_review.rating), const SizedBox(height: 8), diff --git a/lib/ui/pages/restaurant_detail/widgets/review_list.dart b/lib/ui/pages/restaurant_detail/widgets/review_list.dart index ebf1379..2a178a1 100644 --- a/lib/ui/pages/restaurant_detail/widgets/review_list.dart +++ b/lib/ui/pages/restaurant_detail/widgets/review_list.dart @@ -8,10 +8,12 @@ class _ReviewList extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${_reviews.length} reviews', style: AppTextStyles.openRegularText), const SizedBox(height: 16), ListView.builder( + physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: _reviews.length, itemBuilder: (context, index) { diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 4d0f623..15ebf16 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart'; - import '../../../domain/entities/entities.dart'; import '../../../presentation/presentation.dart'; import '../../../typography.dart'; diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart index ae0f926..9a65fae 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -3,7 +3,7 @@ import '../../../domain/entities/entities.dart'; abstract class RestaurantTourPresenter { List get restaurantList; Future getAllRestaurants(); - List get favoriteRestaurantList; + List get favoriteRestaurantList; Future getFavoriteRestaurants(); - Future addFavoriteRestaurants(); + Future addFavoriteRestaurants(RestaurantEntity restaurant); } diff --git a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart index e5e38ec..590f4ec 100644 --- a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart @@ -9,12 +9,14 @@ class _FavoriteRestaurantList extends StatelessWidget { if (favoriteRestaurantList.isEmpty) { return const _MessageContent('No favorites.'); } else { - return ListView.builder( + return ListView.separated( + itemCount: favoriteRestaurantList.length, padding: const EdgeInsets.all(16), - itemBuilder: (itemBuilder, index) { + itemBuilder: (context, index) { final restaurant = favoriteRestaurantList[index]; return _RestaurantItem(restaurant); }, + separatorBuilder: (_, __) => const SizedBox(height: 12), ); } } diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart index c3f2560..49a2a07 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart @@ -5,12 +5,24 @@ class _RestaurantItem extends StatelessWidget { const _RestaurantItem(this._restaurant); + void _onItemTapped(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RestaurantDetailPage( + restaurant: _restaurant, + onFavorite: () { + context.read().addFavoriteRestaurants(_restaurant); + }, + ), + ), + ); + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - Navigator.pushNamed(context, '/detail'); - }, + onTap: () => _onItemTapped(context), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart index b568f9d..3a223f4 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart @@ -9,12 +9,14 @@ class _RestaurantList extends StatelessWidget { if (restaurantList.isEmpty) { return const _MessageContent('No restaurants.'); } else { - return ListView.builder( + return ListView.separated( padding: const EdgeInsets.all(16), - itemBuilder: (itemBuilder, index) { + itemCount: restaurantList.length, + itemBuilder: (context, index) { final restaurant = restaurantList[index]; return _RestaurantItem(restaurant); }, + separatorBuilder: (_, __) => const SizedBox(height: 12), ); } } From 1322b469db1bb507dd3e96bee9565c294d0f0dda Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 22:09:54 -0300 Subject: [PATCH 55/91] feat: add null check fromJson models --- lib/data/models/category_model.dart | 4 ++-- lib/data/models/restaurant_model.dart | 18 +++++++++--------- lib/data/models/review_model.dart | 6 +++--- lib/data/models/user_model.dart | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/data/models/category_model.dart b/lib/data/models/category_model.dart index 6608a3d..b244b9f 100644 --- a/lib/data/models/category_model.dart +++ b/lib/data/models/category_model.dart @@ -20,8 +20,8 @@ class CategoryModel { factory CategoryModel.fromJson(Map json) { try { return CategoryModel( - title: json['title'], - alias: json['alias'], + title: json['title'] ?? '', + alias: json['alias'] ?? '', ); } catch (_) { throw DomainError.unexpected; diff --git a/lib/data/models/restaurant_model.dart b/lib/data/models/restaurant_model.dart index 0dc7a4d..a173c65 100644 --- a/lib/data/models/restaurant_model.dart +++ b/lib/data/models/restaurant_model.dart @@ -48,16 +48,16 @@ class RestaurantModel { factory RestaurantModel.fromJson(Map json) { try { return RestaurantModel( - id: json['id'], + id: json['id'] ?? '', categories: (json['categories'] as List).map((item) => CategoryModel.fromJson(json)).toList(), - displayCategory: json['displayCategory'], - heroImage: json['heroImage'], - isOpen: json['isOpen'], - address: json['address'], - name: json['name'], - photos: json['photos'], - price: json['price'], - rating: json['rating'], + displayCategory: json['displayCategory'] ?? '', + heroImage: json['heroImage'] ?? '', + isOpen: json['isOpen'] ?? true, + address: json['address'] ?? '', + name: json['name'] ?? '', + photos: List.from(json['photos'] ?? []), + price: json['price'] ?? '', + rating: json['rating'] ?? 5, reviews: (json['reviews'] as List).map(((item) => ReviewModel.fromJson(item))).toList(), ); } catch (_) { diff --git a/lib/data/models/review_model.dart b/lib/data/models/review_model.dart index 72c6b22..d5ce1f0 100644 --- a/lib/data/models/review_model.dart +++ b/lib/data/models/review_model.dart @@ -27,9 +27,9 @@ class ReviewModel { factory ReviewModel.fromJson(Map json) { try { return ReviewModel( - id: json['id'], - rating: json['rating'], - text: json['text'], + id: json['id'] ?? '', + rating: json['rating'] ?? 5, + text: json['text'] ?? '', user: UserModel.fromJson(json['user']), ); } catch (_) { diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 6137475..4e0ad00 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -23,9 +23,9 @@ class UserModel { factory UserModel.fromJson(Map json) { try { return UserModel( - id: json['id'], - imageUrl: json['imageUrl'], - name: json['name'], + id: json['id'] ?? '', + imageUrl: json['imageUrl'] ?? '', + name: json['name'] ?? '', ); } catch (_) { throw DomainError.unexpected; From 1cd12f0ff67357942b3b8b7be53df4e763027531 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 22:16:50 -0300 Subject: [PATCH 56/91] refact: remove unnecessary protocol --- lib/infra/cache/cache.dart | 1 - lib/infra/cache/delete_cache.dart | 3 --- lib/infra/cache/local_storage_adapter.dart | 15 +++------------ 3 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 lib/infra/cache/delete_cache.dart diff --git a/lib/infra/cache/cache.dart b/lib/infra/cache/cache.dart index 22013c4..16d5638 100644 --- a/lib/infra/cache/cache.dart +++ b/lib/infra/cache/cache.dart @@ -1,4 +1,3 @@ -export 'delete_cache.dart'; export 'fetch_cache.dart'; export 'save_cache.dart'; export 'local_storage_adapter.dart'; diff --git a/lib/infra/cache/delete_cache.dart b/lib/infra/cache/delete_cache.dart deleted file mode 100644 index 75109fb..0000000 --- a/lib/infra/cache/delete_cache.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class DeleteCache { - Future delete(String key); -} diff --git a/lib/infra/cache/local_storage_adapter.dart b/lib/infra/cache/local_storage_adapter.dart index cd807bf..003210c 100644 --- a/lib/infra/cache/local_storage_adapter.dart +++ b/lib/infra/cache/local_storage_adapter.dart @@ -2,25 +2,16 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'cache.dart'; -class LocalStorageAdapter implements SaveCache, FetchCache, DeleteCache { - late final SharedPreferences storage; - - LocalStorageAdapter() { - SharedPreferences.getInstance().then((instance) => storage = instance); - } - +class LocalStorageAdapter implements SaveCache, FetchCache { @override Future save({required String key, required String value}) async { + final storage = await SharedPreferences.getInstance(); await storage.setString(key, value); } @override Future fetch(String key) async { + final storage = await SharedPreferences.getInstance(); return storage.getString(key); } - - @override - Future delete(String key) async { - storage.remove(key); - } } From 77eb3647d70b325441c95c4ed068bcc6bea102bc Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 22:17:17 -0300 Subject: [PATCH 57/91] feat: add name prop on restaurant model toJson --- lib/data/models/restaurant_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/data/models/restaurant_model.dart b/lib/data/models/restaurant_model.dart index a173c65..c802f09 100644 --- a/lib/data/models/restaurant_model.dart +++ b/lib/data/models/restaurant_model.dart @@ -68,6 +68,7 @@ class RestaurantModel { Map toJson() { return { 'id': id, + 'name': name, 'categories': categories.map((category) => category.toJson()).toList(), 'displayCategory': displayCategory, 'heroImage': heroImage, From 8f2a4e10069697175a0f387518d55dc7cf8c647d Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 23:10:52 -0300 Subject: [PATCH 58/91] fix: models --- ...model.dart => local_restaurant_model.dart} | 78 ++++++------------- lib/data/models/models.dart | 3 +- lib/data/models/remote_restaurant_model.dart | 62 +++++++++++++++ lib/data/models/review_model.dart | 2 +- lib/data/models/user_model.dart | 2 +- 5 files changed, 91 insertions(+), 56 deletions(-) rename lib/data/models/{restaurant_model.dart => local_restaurant_model.dart} (57%) create mode 100644 lib/data/models/remote_restaurant_model.dart diff --git a/lib/data/models/restaurant_model.dart b/lib/data/models/local_restaurant_model.dart similarity index 57% rename from lib/data/models/restaurant_model.dart rename to lib/data/models/local_restaurant_model.dart index c802f09..6e6ddef 100644 --- a/lib/data/models/restaurant_model.dart +++ b/lib/data/models/local_restaurant_model.dart @@ -2,24 +2,20 @@ import '../../domain/entities/entities.dart'; import '../../domain/helpers/helpers.dart'; import 'models.dart'; -class RestaurantModel { +class LocalRestaurantModel { final String id; final List categories; - final String displayCategory; - final String heroImage; final bool isOpen; final String address; final String name; final List photos; final String price; - final int rating; + final double rating; final List reviews; - RestaurantModel({ + LocalRestaurantModel({ required this.id, required this.categories, - required this.displayCategory, - required this.heroImage, required this.isOpen, required this.address, required this.name, @@ -29,35 +25,17 @@ class RestaurantModel { required this.reviews, }); - factory RestaurantModel.fromEntity(RestaurantEntity entity) { - return RestaurantModel( - id: entity.id, - rating: entity.rating, - categories: entity.categories.map((category) => CategoryModel.fromEntity(category)).toList(), - displayCategory: entity.displayCategory, - heroImage: entity.heroImage, - isOpen: entity.isOpen, - address: entity.address, - name: entity.name, - photos: entity.photos, - price: entity.price, - reviews: entity.reviews.map((review) => ReviewModel.fromEntity(review)).toList(), - ); - } - - factory RestaurantModel.fromJson(Map json) { + factory LocalRestaurantModel.fromJson(Map json) { try { - return RestaurantModel( - id: json['id'] ?? '', + return LocalRestaurantModel( + id: json['id'], categories: (json['categories'] as List).map((item) => CategoryModel.fromJson(json)).toList(), - displayCategory: json['displayCategory'] ?? '', - heroImage: json['heroImage'] ?? '', - isOpen: json['isOpen'] ?? true, - address: json['address'] ?? '', - name: json['name'] ?? '', - photos: List.from(json['photos'] ?? []), - price: json['price'] ?? '', - rating: json['rating'] ?? 5, + isOpen: json['is_open'], + address: json['address'], + name: json['name'], + photos: List.from(json['photos']), + price: json['price'], + rating: json['rating'], reviews: (json['reviews'] as List).map(((item) => ReviewModel.fromJson(item))).toList(), ); } catch (_) { @@ -70,9 +48,7 @@ class RestaurantModel { 'id': id, 'name': name, 'categories': categories.map((category) => category.toJson()).toList(), - 'displayCategory': displayCategory, - 'heroImage': heroImage, - 'isOpen': isOpen, + 'is_open': isOpen, 'address': address, 'photos': photos, 'price': price, @@ -81,29 +57,25 @@ class RestaurantModel { }; } - RestaurantEntity toEntity() { - return RestaurantEntity( - id: id, - rating: rating, - categories: categories.map((category) => category.toEntity()).toList(), - displayCategory: displayCategory, - heroImage: heroImage, - isOpen: isOpen, - address: address, - name: name, - photos: photos, - price: price, - reviews: reviews.map((review) => review.toEntity()).toList(), + factory LocalRestaurantModel.fromEntity(RestaurantEntity entity) { + return LocalRestaurantModel( + id: entity.id, + rating: entity.rating, + categories: entity.categories.map((category) => CategoryModel.fromEntity(category)).toList(), + isOpen: entity.isOpen, + address: entity.address, + name: entity.name, + photos: entity.photos, + price: entity.price, + reviews: entity.reviews.map((review) => ReviewModel.fromEntity(review)).toList(), ); } - FavoriteRestaurantEntity toFavoriteEntity() { + FavoriteRestaurantEntity toEntity() { return FavoriteRestaurantEntity( id: id, rating: rating, categories: categories.map((category) => category.toEntity()).toList(), - displayCategory: displayCategory, - heroImage: heroImage, isOpen: isOpen, address: address, name: name, diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 7c72d7f..8208ddb 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -1,4 +1,5 @@ export 'category_model.dart'; -export 'restaurant_model.dart'; +export 'local_restaurant_model.dart'; +export 'remote_restaurant_model.dart'; export 'review_model.dart'; export 'user_model.dart'; diff --git a/lib/data/models/remote_restaurant_model.dart b/lib/data/models/remote_restaurant_model.dart new file mode 100644 index 0000000..bc4a6ad --- /dev/null +++ b/lib/data/models/remote_restaurant_model.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; + +import '../../domain/entities/entities.dart'; +import '../../domain/helpers/helpers.dart'; +import 'models.dart'; + +class RemoteRestaurantModel { + final String id; + final List categories; + final bool isOpen; + final String address; + final String name; + final List photos; + final String price; + final double rating; + final List reviews; + + RemoteRestaurantModel({ + required this.id, + required this.categories, + required this.isOpen, + required this.address, + required this.name, + required this.photos, + required this.price, + required this.rating, + required this.reviews, + }); + + factory RemoteRestaurantModel.fromJson(Map json) { + try { + return RemoteRestaurantModel( + id: json['id'] ?? '', + categories: (json['categories'] as List).map((item) => CategoryModel.fromJson(json)).toList(), + isOpen: (json['hours'] ?? []).first['is_open_now'] ?? true, + address: json['location']['formatted_address'] ?? '', + name: json['name'] ?? '', + photos: List.from(json['photos'] ?? []), + price: json['price'] ?? '', + rating: json['rating'] ?? 5.0, + reviews: (json['reviews'] as List).map(((item) => ReviewModel.fromJson(item))).toList(), + ); + } catch (e, s) { + debugPrintStack(label: 'Error $e', stackTrace: s); + throw DomainError.unexpected; + } + } + + RestaurantEntity toEntity() { + return RestaurantEntity( + id: id, + rating: rating, + categories: categories.map((category) => category.toEntity()).toList(), + isOpen: isOpen, + address: address, + name: name, + photos: photos, + price: price, + reviews: reviews.map((review) => review.toEntity()).toList(), + ); + } +} diff --git a/lib/data/models/review_model.dart b/lib/data/models/review_model.dart index d5ce1f0..f21d362 100644 --- a/lib/data/models/review_model.dart +++ b/lib/data/models/review_model.dart @@ -28,7 +28,7 @@ class ReviewModel { try { return ReviewModel( id: json['id'] ?? '', - rating: json['rating'] ?? 5, + rating: (json['rating'] ?? 5.0).round(), text: json['text'] ?? '', user: UserModel.fromJson(json['user']), ); diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 4e0ad00..6381032 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -24,7 +24,7 @@ class UserModel { try { return UserModel( id: json['id'] ?? '', - imageUrl: json['imageUrl'] ?? '', + imageUrl: json['image_url'] ?? '', name: json['name'] ?? '', ); } catch (_) { From c59c1ab319b4366034a0483ca376a8a4d827fe48 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 23:11:07 -0300 Subject: [PATCH 59/91] feat: add no image widget --- lib/ui/widgets/no_image.dart | 19 +++++++++++++++++++ lib/ui/widgets/widgets.dart | 1 + 2 files changed, 20 insertions(+) create mode 100644 lib/ui/widgets/no_image.dart diff --git a/lib/ui/widgets/no_image.dart b/lib/ui/widgets/no_image.dart new file mode 100644 index 0000000..57a5ef1 --- /dev/null +++ b/lib/ui/widgets/no_image.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import '../../typography.dart'; + +class NoImage extends StatelessWidget { + final TextStyle? style; + + const NoImage({super.key, this.style}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + 'No image', + style: style ?? AppTextStyles.openRegularText, + ), + ); + } +} diff --git a/lib/ui/widgets/widgets.dart b/lib/ui/widgets/widgets.dart index e5427ff..2391fa6 100644 --- a/lib/ui/widgets/widgets.dart +++ b/lib/ui/widgets/widgets.dart @@ -1,4 +1,5 @@ export 'circular_loading.dart'; +export 'no_image.dart'; export 'star_icon.dart'; export 'stars.dart'; export 'status.dart'; From 5390eb4fdb4da1925db9cbb3d4874625be3bd234 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Mon, 16 Sep 2024 23:11:41 -0300 Subject: [PATCH 60/91] refact: remove unnecessary fields on restaurant entity --- lib/domain/entities/favorite_restaurant_entity.dart | 2 -- lib/domain/entities/restaurant_entity.dart | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/domain/entities/favorite_restaurant_entity.dart b/lib/domain/entities/favorite_restaurant_entity.dart index db57d90..70f41b9 100644 --- a/lib/domain/entities/favorite_restaurant_entity.dart +++ b/lib/domain/entities/favorite_restaurant_entity.dart @@ -4,8 +4,6 @@ class FavoriteRestaurantEntity extends RestaurantEntity { FavoriteRestaurantEntity({ required super.id, required super.categories, - required super.displayCategory, - required super.heroImage, required super.isOpen, required super.address, required super.name, diff --git a/lib/domain/entities/restaurant_entity.dart b/lib/domain/entities/restaurant_entity.dart index 808d911..d339749 100644 --- a/lib/domain/entities/restaurant_entity.dart +++ b/lib/domain/entities/restaurant_entity.dart @@ -3,21 +3,17 @@ import 'entities.dart'; class RestaurantEntity { final String id; final List categories; - final String displayCategory; - final String heroImage; final bool isOpen; final String address; final String name; final List photos; final String price; - final int rating; + final double rating; final List reviews; RestaurantEntity({ required this.id, required this.categories, - required this.displayCategory, - required this.heroImage, required this.isOpen, required this.address, required this.name, From 7f0699eac67701c57f76b45fae0075470bec1c01 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:15:43 -0300 Subject: [PATCH 61/91] refactor: ui general adjustments --- .../restaurant_detail_page.dart | 7 ++-- .../widgets/favorite_icon.dart | 25 ++++++++++++-- .../widgets/review_item.dart | 13 ++++--- .../restaurant_tour/restaurant_tour_page.dart | 1 - .../restaurant_tour_presenter.dart | 3 +- .../widgets/favorite_restaurant_list.dart | 34 +++++++++++-------- .../pages/restaurant_tour/widgets/image.dart | 14 ++++---- .../widgets/restaurant_item.dart | 21 ++++++++---- .../restaurant_tour/widgets/snack_bar.dart | 2 +- lib/ui/widgets/no_image.dart | 2 +- 10 files changed, 79 insertions(+), 43 deletions(-) diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index 8392dc8..9fdabd7 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -40,9 +40,10 @@ class RestaurantDetailPage extends StatelessWidget { ), body: ListView( children: [ - SizedBox( + Container( height: 360, - child: Image.network(_restaurant.heroImage), + color: Colors.black12, + child: _restaurant.photos.isEmpty ? const NoImage() : Image.network(_restaurant.photos.first, fit: BoxFit.cover), ), Padding( padding: const EdgeInsets.all(24), @@ -52,7 +53,7 @@ class RestaurantDetailPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + Text(_restaurant.price, style: AppTextStyles.openRegularText), Status(_restaurant.isOpen), ], ), diff --git a/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart index b8be4a3..b40893e 100644 --- a/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart +++ b/lib/ui/pages/restaurant_detail/widgets/favorite_icon.dart @@ -1,6 +1,6 @@ part of '../restaurant_detail_page.dart'; -class _FavoriteIcon extends StatelessWidget { +class _FavoriteIcon extends StatefulWidget { final bool isFavorite; final VoidCallback _onFavorite; @@ -9,13 +9,32 @@ class _FavoriteIcon extends StatelessWidget { required VoidCallback onFavorite, }) : _onFavorite = onFavorite; + @override + State<_FavoriteIcon> createState() => _FavoriteIconState(); +} + +class _FavoriteIconState extends State<_FavoriteIcon> { + late bool _isFavorite; + + @override + void initState() { + _isFavorite = widget.isFavorite; + super.initState(); + } + + void _onTap() { + widget._onFavorite(); + _isFavorite = !_isFavorite; + setState(() {}); + } + @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 16), child: GestureDetector( - onDoubleTap: _onFavorite, - child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border), + onTap: _onTap, + child: Icon(_isFavorite ? Icons.favorite : Icons.favorite_border), ), ); } diff --git a/lib/ui/pages/restaurant_detail/widgets/review_item.dart b/lib/ui/pages/restaurant_detail/widgets/review_item.dart index 4bbe9c5..712392f 100644 --- a/lib/ui/pages/restaurant_detail/widgets/review_item.dart +++ b/lib/ui/pages/restaurant_detail/widgets/review_item.dart @@ -7,6 +7,7 @@ class _ReviewItem extends StatelessWidget { @override Widget build(BuildContext context) { + final userImage = _review.user.imageUrl; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -19,11 +20,13 @@ class _ReviewItem extends StatelessWidget { const SizedBox(height: 8), Row( children: [ - SizedBox( - height: 40, - width: 40, - child: CircleAvatar( - child: Image.network(_review.user.imageUrl), + ClipRRect( + borderRadius: BorderRadius.circular(32), + child: Container( + height: 40, + width: 40, + decoration: const BoxDecoration(shape: BoxShape.circle), + child: userImage.isEmpty ? const Icon(Icons.person) : Image.network(_review.user.imageUrl, fit: BoxFit.fill), ), ), const SizedBox(width: 8), diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index 15ebf16..ca9ea65 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -36,7 +36,6 @@ class _RestaurantTourPageState extends State with SingleTick } void _dispose() { - _bloc.dispose(); _tabController.dispose(); } diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart index 9a65fae..f82cf37 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_presenter.dart @@ -5,5 +5,6 @@ abstract class RestaurantTourPresenter { Future getAllRestaurants(); List get favoriteRestaurantList; Future getFavoriteRestaurants(); - Future addFavoriteRestaurants(RestaurantEntity restaurant); + Future addFavoriteRestaurant(RestaurantEntity restaurant); + Future removeFavoriteRestaurant(FavoriteRestaurantEntity favoriteRestaurant); } diff --git a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart index 590f4ec..24abfd2 100644 --- a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart @@ -5,19 +5,25 @@ class _FavoriteRestaurantList extends StatelessWidget { @override Widget build(BuildContext context) { - final favoriteRestaurantList = context.read().favoriteRestaurantList; - if (favoriteRestaurantList.isEmpty) { - return const _MessageContent('No favorites.'); - } else { - return ListView.separated( - itemCount: favoriteRestaurantList.length, - padding: const EdgeInsets.all(16), - itemBuilder: (context, index) { - final restaurant = favoriteRestaurantList[index]; - return _RestaurantItem(restaurant); - }, - separatorBuilder: (_, __) => const SizedBox(height: 12), - ); - } + final cubit = context.read(); + return BlocBuilder( + bloc: cubit, + builder: (context, state) { + final favoriteRestaurantList = cubit.favoriteRestaurantList; + if (favoriteRestaurantList.isEmpty) { + return const _MessageContent('No favorites.'); + } else { + return ListView.separated( + itemCount: favoriteRestaurantList.length, + padding: const EdgeInsets.all(16), + itemBuilder: (context, index) { + final restaurant = favoriteRestaurantList[index]; + return _RestaurantItem(restaurant); + }, + separatorBuilder: (_, __) => const SizedBox(height: 12), + ); + } + }, + ); } } diff --git a/lib/ui/pages/restaurant_tour/widgets/image.dart b/lib/ui/pages/restaurant_tour/widgets/image.dart index 08cc67a..877ad01 100644 --- a/lib/ui/pages/restaurant_tour/widgets/image.dart +++ b/lib/ui/pages/restaurant_tour/widgets/image.dart @@ -7,14 +7,14 @@ class _Image extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 88, - width: 88, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(8), + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + height: 88, + width: 88, + decoration: BoxDecoration(color: Colors.black12, borderRadius: BorderRadius.circular(8)), + child: _url.isEmpty ? const NoImage(style: AppTextStyles.openRegularText) : Image.network(_url, fit: BoxFit.cover), ), - child: Text(_url), ); } } diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart index 49a2a07..be8eb45 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart @@ -6,14 +6,21 @@ class _RestaurantItem extends StatelessWidget { const _RestaurantItem(this._restaurant); void _onItemTapped(BuildContext context) { + void onFavorite() { + final cubit = context.read(); + if (_restaurant is FavoriteRestaurantEntity) { + cubit.removeFavoriteRestaurant(_restaurant as FavoriteRestaurantEntity); + } else { + cubit.addFavoriteRestaurant(_restaurant); + } + } + Navigator.push( context, MaterialPageRoute( - builder: (context) => RestaurantDetailPage( + builder: (_) => RestaurantDetailPage( restaurant: _restaurant, - onFavorite: () { - context.read().addFavoriteRestaurants(_restaurant); - }, + onFavorite: onFavorite, ), ), ); @@ -39,7 +46,7 @@ class _RestaurantItem extends StatelessWidget { ), child: Row( children: [ - _Image(_restaurant.heroImage), + _Image(_restaurant.photos.isEmpty ? '' : _restaurant.photos.first), const SizedBox(width: 12), Flexible( child: Column( @@ -47,12 +54,12 @@ class _RestaurantItem extends StatelessWidget { children: [ Text(_restaurant.name, style: AppTextStyles.loraRegularTitle), const SizedBox(height: 4), - const Text('\$\$\$\$ Italian', style: AppTextStyles.openRegularText), + Text(_restaurant.price, style: AppTextStyles.openRegularText), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Stars(_restaurant.rating), + Stars(_restaurant.rating.round()), Status(_restaurant.isOpen), ], ), diff --git a/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart b/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart index 6f65a82..8c0a37e 100644 --- a/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart +++ b/lib/ui/pages/restaurant_tour/widgets/snack_bar.dart @@ -4,7 +4,7 @@ void showSnackBar(BuildContext context, {required RestaurantErrorState state}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( elevation: 0, - backgroundColor: Colors.red.withOpacity(.8), + backgroundColor: Colors.red, duration: const Duration(milliseconds: 2000), content: Text( state.message, diff --git a/lib/ui/widgets/no_image.dart b/lib/ui/widgets/no_image.dart index 57a5ef1..afb10b2 100644 --- a/lib/ui/widgets/no_image.dart +++ b/lib/ui/widgets/no_image.dart @@ -12,7 +12,7 @@ class NoImage extends StatelessWidget { return Center( child: Text( 'No image', - style: style ?? AppTextStyles.openRegularText, + style: style ?? AppTextStyles.openRegularHeadline, ), ); } From 808736a780718582535b11a12d122c72b6c77f9c Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:16:16 -0300 Subject: [PATCH 62/91] refact: data layer improvements --- lib/data/models/remote_restaurant_model.dart | 5 +---- lib/data/models/user_model.dart | 2 +- lib/data/usecases/local_get_favorite_restaurants.dart | 2 +- lib/data/usecases/local_save_favorite_restaurants.dart | 7 ++----- lib/data/usecases/remote_get_restaurants.dart | 7 ++----- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/data/models/remote_restaurant_model.dart b/lib/data/models/remote_restaurant_model.dart index bc4a6ad..7e761ee 100644 --- a/lib/data/models/remote_restaurant_model.dart +++ b/lib/data/models/remote_restaurant_model.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - import '../../domain/entities/entities.dart'; import '../../domain/helpers/helpers.dart'; import 'models.dart'; @@ -40,8 +38,7 @@ class RemoteRestaurantModel { rating: json['rating'] ?? 5.0, reviews: (json['reviews'] as List).map(((item) => ReviewModel.fromJson(item))).toList(), ); - } catch (e, s) { - debugPrintStack(label: 'Error $e', stackTrace: s); + } catch (_) { throw DomainError.unexpected; } } diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 6381032..0c86365 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -35,7 +35,7 @@ class UserModel { Map toJson() { return { 'id': id, - 'imageUrl': imageUrl, + 'image_url': imageUrl, 'name': name, }; } diff --git a/lib/data/usecases/local_get_favorite_restaurants.dart b/lib/data/usecases/local_get_favorite_restaurants.dart index 3f4692c..39bd254 100644 --- a/lib/data/usecases/local_get_favorite_restaurants.dart +++ b/lib/data/usecases/local_get_favorite_restaurants.dart @@ -19,7 +19,7 @@ class LocalGetFavoriteRestaurants implements GetFavoriteRestaurants { final result = await cache.fetch('favorite_restaurants'); if (result == null) return []; final jsonDecoded = jsonDecode(result); - return (jsonDecoded as List).map((article) => RestaurantModel.fromJson(article).toFavoriteEntity()).toList(); + return (jsonDecoded as List).map((article) => LocalRestaurantModel.fromJson(article).toEntity()).toList(); } catch (_) { throw DomainError.unexpected; } diff --git a/lib/data/usecases/local_save_favorite_restaurants.dart b/lib/data/usecases/local_save_favorite_restaurants.dart index 2492a3f..670d308 100644 --- a/lib/data/usecases/local_save_favorite_restaurants.dart +++ b/lib/data/usecases/local_save_favorite_restaurants.dart @@ -14,11 +14,8 @@ class LocalSaveFavoriteRestaurants implements SaveFavoriteRestaurants { @override Future call(List restaurants) async { try { - final value = jsonEncode(restaurants.map((restaurant) => RestaurantModel.fromEntity(restaurant).toJson()).toList()); - return await cache.save( - key: 'favorite_restaurants', - value: value, - ); + final value = jsonEncode(restaurants.map((restaurant) => LocalRestaurantModel.fromEntity(restaurant).toJson()).toList()); + return await cache.save(key: 'favorite_restaurants', value: value); } catch (_) { throw DomainError.unexpected; } diff --git a/lib/data/usecases/remote_get_restaurants.dart b/lib/data/usecases/remote_get_restaurants.dart index fe1d33f..ac78236 100644 --- a/lib/data/usecases/remote_get_restaurants.dart +++ b/lib/data/usecases/remote_get_restaurants.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import '../../domain/entities/entities.dart'; import '../../domain/helpers/helpers.dart'; import '../../domain/usecases/usecases.dart'; @@ -55,9 +53,8 @@ class RemoteGetRestaurants implements GetRestaurants { Future> call() async { try { final response = await _client.request(url: _url, method: 'post', data: query); - return (response['data']['search']['business'] as List).map((item) => RestaurantModel.fromJson(item).toEntity()).toList(); - } catch (e, s) { - debugPrintStack(label: 'Error $e', stackTrace: s); + return (response['data']['search']['business'] as List).map((item) => RemoteRestaurantModel.fromJson(item).toEntity()).toList(); + } catch (_) { throw DomainError.unexpected; } } From 0d76b30666228693767b283f7cf9a75461eb3737 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:16:32 -0300 Subject: [PATCH 63/91] feat: cubit adjustments --- .../cubit_restaurant_tour_presenter.dart | 83 ++++++++++++++++--- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 69af5e6..61990b8 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -36,6 +36,56 @@ class CubitRestaurantTourPresenter extends Cubit implements Res } } + void _updateRestaurantAsFavorite( + RestaurantEntity restaurant, + FavoriteRestaurantEntity favoriteRestaurant, + ) { + final index = _restaurantList.indexOf(restaurant); + _restaurantList[index] = favoriteRestaurant; + _favoriteRestaurantList.add(favoriteRestaurant); + } + + void _removeFavoriteRestaurant( + RestaurantEntity restaurant, + FavoriteRestaurantEntity favoriteRestaurant, + ) { + final index = _restaurantList.indexOf(favoriteRestaurant); + if (index != -1) _restaurantList[index] = restaurant; + _favoriteRestaurantList.remove(favoriteRestaurant); + } + + RestaurantEntity _makeRestaurantFromFavoriteRestaurant( + FavoriteRestaurantEntity favoriteRestaurant, + ) { + return RestaurantEntity( + id: favoriteRestaurant.id, + categories: favoriteRestaurant.categories, + isOpen: favoriteRestaurant.isOpen, + address: favoriteRestaurant.address, + name: favoriteRestaurant.name, + photos: favoriteRestaurant.photos, + price: favoriteRestaurant.price, + rating: favoriteRestaurant.rating, + reviews: favoriteRestaurant.reviews, + ); + } + + FavoriteRestaurantEntity _makeFavoriteRestaurantFromRestaurant( + RestaurantEntity restaurant, + ) { + return FavoriteRestaurantEntity( + id: restaurant.id, + categories: restaurant.categories, + isOpen: restaurant.isOpen, + address: restaurant.address, + name: restaurant.name, + photos: restaurant.photos, + price: restaurant.price, + rating: restaurant.rating, + reviews: restaurant.reviews, + ); + } + final _restaurantList = []; final _favoriteRestaurantList = []; @@ -51,9 +101,8 @@ class CubitRestaurantTourPresenter extends Cubit implements Res emit(RestaurantLoadingState()); _favoriteRestaurantList.addAll(await _getFavoriteRestaurants()); emit(RestaurantSuccessState()); - } catch (e, s) { - debugPrintStack(label: 'Error - getFavoriteRestaurants - $e', stackTrace: s); - emit(RestaurantErrorState('An error occurred when get favorite restaurants')); + } catch (_) { + emit(RestaurantErrorState('Error loading favorite restaurants. Please try again later.')); } } @@ -63,23 +112,31 @@ class CubitRestaurantTourPresenter extends Cubit implements Res emit(RestaurantLoadingState()); _restaurantList.addAll(await _getRestaurants()); emit(RestaurantSuccessState()); - } catch (e, s) { - debugPrintStack(label: 'Error - getAllRestaurants - $e', stackTrace: s); - emit(RestaurantErrorState('An error occurred when searching for the restaurants')); + } catch (_) { + emit(RestaurantErrorState('Oops! We had trouble finding the restaurants.')); } } @override - Future addFavoriteRestaurants() async { + Future addFavoriteRestaurant(RestaurantEntity restaurant) async { try { - _saveFavoriteRestaurants(_favoriteRestaurantList); - } catch (e, s) { - debugPrintStack(label: 'Error - addFavoriteRestaurants - $e', stackTrace: s); + final favoriteRestaurant = _makeFavoriteRestaurantFromRestaurant(restaurant); + _updateRestaurantAsFavorite(restaurant, favoriteRestaurant); + await _saveFavoriteRestaurants(_favoriteRestaurantList); + } catch (_) { + emit(RestaurantErrorState('Error fetching restaurants. Try again later.')); } } - void dispose() { - _restaurantList.clear(); - _favoriteRestaurantList.clear(); + @override + Future removeFavoriteRestaurant(FavoriteRestaurantEntity favoriteRestaurant) async { + try { + final restaurant = _makeRestaurantFromFavoriteRestaurant(favoriteRestaurant); + _removeFavoriteRestaurant(restaurant, favoriteRestaurant); + await _saveFavoriteRestaurants(_favoriteRestaurantList); + emit(RestaurantSuccessState()); + } catch (_) { + emit(RestaurantErrorState('Oops! There was an issue removing the restaurant from your favorites.')); + } } } From fb452d9babe0ab43d79506b024d11772354bd2e2 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:16:48 -0300 Subject: [PATCH 64/91] chore: change authorization token --- lib/factories/infra/http/http_client_factory.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/factories/infra/http/http_client_factory.dart b/lib/factories/infra/http/http_client_factory.dart index ddcce6a..118ef03 100644 --- a/lib/factories/infra/http/http_client_factory.dart +++ b/lib/factories/infra/http/http_client_factory.dart @@ -7,7 +7,7 @@ HttpAdapter makeHttpAdapter() { final headers = { 'Content-Type': 'application/graphql', 'Authorization': - 'Bearer 1hH9pW24WpBHxamMbgShaygY4eqRSW5NZq3e6eVImPkhFU8RR4KHIZ59pJ0D5YL4GCizkxjdxegx1f5igGGQuVEMER8gR-Jo8JpT1V6D3OUjUcucwL8uF5pxNqzlZnYx', + 'Bearer Ea0b8FWXPxS9mv3nTITKruISxVsKVPqW02EP4egupETca6psxfSgIjgAxKBYxOCu0LSluKYwBJWI15YPJXfL6aVRdxCGhP8uhQXcTpBVMn8NPIN8zU7_K3iTJuzoZnYx', }; return HttpAdapter(client: client, headers: headers); } From 7c39b4491be35daa632469efe96cff3074b12b8b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:27:05 -0300 Subject: [PATCH 65/91] refactor: removed unnused restaurant model --- lib/models/restaurant.dart | 157 ----------------------------------- lib/models/restaurant.g.dart | 109 ------------------------ 2 files changed, 266 deletions(-) delete mode 100644 lib/models/restaurant.dart delete mode 100644 lib/models/restaurant.g.dart diff --git a/lib/models/restaurant.dart b/lib/models/restaurant.dart deleted file mode 100644 index 1c7ad2f..0000000 --- a/lib/models/restaurant.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'restaurant.g.dart'; - -@JsonSerializable() -class Category { - final String? alias; - final String? title; - - Category({ - this.alias, - this.title, - }); - - factory Category.fromJson(Map json) => - _$CategoryFromJson(json); - - Map toJson() => _$CategoryToJson(this); -} - -@JsonSerializable() -class Hours { - @JsonKey(name: 'is_open_now') - final bool? isOpenNow; - - const Hours({ - this.isOpenNow, - }); - - factory Hours.fromJson(Map json) => _$HoursFromJson(json); - - Map toJson() => _$HoursToJson(this); -} - -@JsonSerializable() -class User { - final String? id; - @JsonKey(name: 'image_url') - final String? imageUrl; - final String? name; - - const User({ - this.id, - this.imageUrl, - this.name, - }); - - factory User.fromJson(Map json) => _$UserFromJson(json); - - Map toJson() => _$UserToJson(this); -} - -@JsonSerializable() -class Review { - final String? id; - final int? rating; - final String? text; - final User? user; - - const Review({ - this.id, - this.rating, - this.user, - this.text, - }); - - factory Review.fromJson(Map json) => _$ReviewFromJson(json); - - Map toJson() => _$ReviewToJson(this); -} - -@JsonSerializable() -class Location { - @JsonKey(name: 'formatted_address') - final String? formattedAddress; - - Location({ - this.formattedAddress, - }); - - factory Location.fromJson(Map json) => - _$LocationFromJson(json); - - Map toJson() => _$LocationToJson(this); -} - -@JsonSerializable() -class Restaurant { - final String? id; - final String? name; - final String? price; - final double? rating; - final List? photos; - final List? categories; - final List? hours; - final List? reviews; - final Location? location; - - const Restaurant({ - this.id, - this.name, - this.price, - this.rating, - this.photos, - this.categories, - this.hours, - this.reviews, - this.location, - }); - - factory Restaurant.fromJson(Map json) => - _$RestaurantFromJson(json); - - Map toJson() => _$RestaurantToJson(this); - - /// Use the first category for the category shown to the user - String get displayCategory { - if (categories != null && categories!.isNotEmpty) { - return categories!.first.title ?? ''; - } - return ''; - } - - /// Use the first image as the image shown to the user - String get heroImage { - if (photos != null && photos!.isNotEmpty) { - return photos!.first; - } - return ''; - } - - /// This logic is probably not correct in all cases but it is ok - /// for this application - bool get isOpen { - if (hours != null && hours!.isNotEmpty) { - return hours!.first.isOpenNow ?? false; - } - return false; - } -} - -@JsonSerializable() -class RestaurantQueryResult { - final int? total; - @JsonKey(name: 'business') - final List? restaurants; - - const RestaurantQueryResult({ - this.total, - this.restaurants, - }); - - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); - - Map toJson() => _$RestaurantQueryResultToJson(this); -} diff --git a/lib/models/restaurant.g.dart b/lib/models/restaurant.g.dart deleted file mode 100644 index 3ed33f9..0000000 --- a/lib/models/restaurant.g.dart +++ /dev/null @@ -1,109 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'restaurant.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Category _$CategoryFromJson(Map json) => Category( - alias: json['alias'] as String?, - title: json['title'] as String?, - ); - -Map _$CategoryToJson(Category instance) => { - 'alias': instance.alias, - 'title': instance.title, - }; - -Hours _$HoursFromJson(Map json) => Hours( - isOpenNow: json['is_open_now'] as bool?, - ); - -Map _$HoursToJson(Hours instance) => { - 'is_open_now': instance.isOpenNow, - }; - -User _$UserFromJson(Map json) => User( - id: json['id'] as String?, - imageUrl: json['image_url'] as String?, - name: json['name'] as String?, - ); - -Map _$UserToJson(User instance) => { - 'id': instance.id, - 'image_url': instance.imageUrl, - 'name': instance.name, - }; - -Review _$ReviewFromJson(Map json) => Review( - id: json['id'] as String?, - rating: json['rating'] as int?, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - ); - -Map _$ReviewToJson(Review instance) => { - 'id': instance.id, - 'rating': instance.rating, - 'user': instance.user, - }; - -Location _$LocationFromJson(Map json) => Location( - formattedAddress: json['formatted_address'] as String?, - ); - -Map _$LocationToJson(Location instance) => { - 'formatted_address': instance.formattedAddress, - }; - -Restaurant _$RestaurantFromJson(Map json) => Restaurant( - id: json['id'] as String?, - name: json['name'] as String?, - price: json['price'] as String?, - rating: (json['rating'] as num?)?.toDouble(), - photos: - (json['photos'] as List?)?.map((e) => e as String).toList(), - categories: (json['categories'] as List?) - ?.map((e) => Category.fromJson(e as Map)) - .toList(), - hours: (json['hours'] as List?) - ?.map((e) => Hours.fromJson(e as Map)) - .toList(), - reviews: (json['reviews'] as List?) - ?.map((e) => Review.fromJson(e as Map)) - .toList(), - location: json['location'] == null - ? null - : Location.fromJson(json['location'] as Map), - ); - -Map _$RestaurantToJson(Restaurant instance) => - { - 'id': instance.id, - 'name': instance.name, - 'price': instance.price, - 'rating': instance.rating, - 'photos': instance.photos, - 'categories': instance.categories, - 'hours': instance.hours, - 'reviews': instance.reviews, - 'location': instance.location, - }; - -RestaurantQueryResult _$RestaurantQueryResultFromJson( - Map json) => - RestaurantQueryResult( - total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) - .toList(), - ); - -Map _$RestaurantQueryResultToJson( - RestaurantQueryResult instance) => - { - 'total': instance.total, - 'business': instance.restaurants, - }; From dfbd71b0af04760aa099172c4fdc3e574a628c9e Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:27:48 -0300 Subject: [PATCH 66/91] refactor: move typography to core folder --- lib/core/core.dart | 1 + lib/{ => core/typography}/typography.dart | 0 lib/ui/pages/restaurant_detail/restaurant_detail_page.dart | 2 +- lib/ui/pages/restaurant_tour/restaurant_tour_page.dart | 2 +- lib/ui/widgets/no_image.dart | 2 +- lib/ui/widgets/status.dart | 2 +- 6 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 lib/core/core.dart rename lib/{ => core/typography}/typography.dart (100%) diff --git a/lib/core/core.dart b/lib/core/core.dart new file mode 100644 index 0000000..0f37626 --- /dev/null +++ b/lib/core/core.dart @@ -0,0 +1 @@ +export 'typography/typography.dart'; diff --git a/lib/typography.dart b/lib/core/typography/typography.dart similarity index 100% rename from lib/typography.dart rename to lib/core/typography/typography.dart diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index 9fdabd7..7fd624f 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import '../../../core/core.dart'; import '../../../domain/entities/entities.dart'; -import '../../../typography.dart'; import '../../widgets/widgets.dart'; part 'widgets/arrow_back_icon.dart'; diff --git a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart index ca9ea65..4cebd65 100644 --- a/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart +++ b/lib/ui/pages/restaurant_tour/restaurant_tour_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../core/core.dart'; import '../../../domain/entities/entities.dart'; import '../../../presentation/presentation.dart'; -import '../../../typography.dart'; import '../../widgets/widgets.dart'; import '../pages.dart'; diff --git a/lib/ui/widgets/no_image.dart b/lib/ui/widgets/no_image.dart index afb10b2..30a6a21 100644 --- a/lib/ui/widgets/no_image.dart +++ b/lib/ui/widgets/no_image.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../typography.dart'; +import '../../core/core.dart'; class NoImage extends StatelessWidget { final TextStyle? style; diff --git a/lib/ui/widgets/status.dart b/lib/ui/widgets/status.dart index 4346615..b1c2b3a 100644 --- a/lib/ui/widgets/status.dart +++ b/lib/ui/widgets/status.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../typography.dart'; +import '../../core/core.dart'; class Status extends StatelessWidget { final bool _isOpen; From 772717906b2c8f793d185ba31c431f92f20dfcd0 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:35:08 -0300 Subject: [PATCH 67/91] refactor: move image code from detail page to your own file --- .../restaurant_detail_page.dart | 12 +++--------- .../pages/restaurant_detail/widgets/image.dart | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 lib/ui/pages/restaurant_detail/widgets/image.dart diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index 7fd624f..5e8ae1b 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -6,6 +6,7 @@ import '../../widgets/widgets.dart'; part 'widgets/arrow_back_icon.dart'; part 'widgets/divider.dart'; +part 'widgets/image.dart'; part 'widgets/favorite_icon.dart'; part 'widgets/rating.dart'; part 'widgets/review_item.dart'; @@ -33,18 +34,11 @@ class RestaurantDetailPage extends StatelessWidget { onFavorite: _onFavorite, ), ], - title: Text( - _restaurant.name, - style: AppTextStyles.loraRegularHeadline, - ), + title: Text(_restaurant.name, style: AppTextStyles.loraRegularHeadline), ), body: ListView( children: [ - Container( - height: 360, - color: Colors.black12, - child: _restaurant.photos.isEmpty ? const NoImage() : Image.network(_restaurant.photos.first, fit: BoxFit.cover), - ), + _Image(photos: _restaurant.photos), Padding( padding: const EdgeInsets.all(24), child: Column( diff --git a/lib/ui/pages/restaurant_detail/widgets/image.dart b/lib/ui/pages/restaurant_detail/widgets/image.dart new file mode 100644 index 0000000..0a9f0f2 --- /dev/null +++ b/lib/ui/pages/restaurant_detail/widgets/image.dart @@ -0,0 +1,18 @@ +part of '../restaurant_detail_page.dart'; + +class _Image extends StatelessWidget { + final List _photos; + + const _Image({ + required List photos, + }) : _photos = photos; + + @override + Widget build(BuildContext context) { + return Container( + height: 360, + color: Colors.black12, + child: _photos.isEmpty ? const NoImage() : Image.network(_photos.first, fit: BoxFit.cover), + ); + } +} From 2a9139cc7d34f4926e192934abf88325751f2d9b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:45:49 -0300 Subject: [PATCH 68/91] chore: add .env config to hide confidencial data --- .gitignore | 4 +- lib/core/config/application_config.dart | 11 + lib/core/config/config.dart | 2 + lib/core/config/env.dart | 7 + lib/core/core.dart | 1 + lib/factories/infra/http/api_url_factory.dart | 4 +- .../infra/http/http_client_factory.dart | 4 +- lib/main.dart | 4 +- pubspec.lock | 336 +----------------- pubspec.yaml | 9 +- 10 files changed, 53 insertions(+), 329 deletions(-) create mode 100644 lib/core/config/application_config.dart create mode 100644 lib/core/config/config.dart create mode 100644 lib/core/config/env.dart diff --git a/.gitignore b/.gitignore index 7040cb0..8c6c143 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ app.*.map.json # fvm # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ + +.env \ No newline at end of file diff --git a/lib/core/config/application_config.dart b/lib/core/config/application_config.dart new file mode 100644 index 0000000..59b78fc --- /dev/null +++ b/lib/core/config/application_config.dart @@ -0,0 +1,11 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +abstract class ApplicationConfig { + static Future setUp() async { + await _loadEnv(); + } + + static Future _loadEnv() async { + await dotenv.load(); + } +} diff --git a/lib/core/config/config.dart b/lib/core/config/config.dart new file mode 100644 index 0000000..4e9eeab --- /dev/null +++ b/lib/core/config/config.dart @@ -0,0 +1,2 @@ +export 'application_config.dart'; +export 'env.dart'; diff --git a/lib/core/config/env.dart b/lib/core/config/env.dart new file mode 100644 index 0000000..ae28a0b --- /dev/null +++ b/lib/core/config/env.dart @@ -0,0 +1,7 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +abstract class ENV { + static final env = dotenv.env; + static String apiKey = env['API_KEY'] ?? ''; + static String baseUrl = env['BASE_URL'] ?? ''; +} diff --git a/lib/core/core.dart b/lib/core/core.dart index 0f37626..b8ea6ab 100644 --- a/lib/core/core.dart +++ b/lib/core/core.dart @@ -1 +1,2 @@ +export 'config/config.dart'; export 'typography/typography.dart'; diff --git a/lib/factories/infra/http/api_url_factory.dart b/lib/factories/infra/http/api_url_factory.dart index b450b69..0d45fb5 100644 --- a/lib/factories/infra/http/api_url_factory.dart +++ b/lib/factories/infra/http/api_url_factory.dart @@ -1 +1,3 @@ -String makeApiUrl() => 'https://api.yelp.com/v3/graphql'; +import '../../../core/core.dart'; + +String makeApiUrl() => ENV.baseUrl; diff --git a/lib/factories/infra/http/http_client_factory.dart b/lib/factories/infra/http/http_client_factory.dart index 118ef03..0d50889 100644 --- a/lib/factories/infra/http/http_client_factory.dart +++ b/lib/factories/infra/http/http_client_factory.dart @@ -1,13 +1,13 @@ import 'package:http/http.dart'; import '../../../../infra/http/http.dart'; +import '../../../core/core.dart'; HttpAdapter makeHttpAdapter() { final client = Client(); final headers = { 'Content-Type': 'application/graphql', - 'Authorization': - 'Bearer Ea0b8FWXPxS9mv3nTITKruISxVsKVPqW02EP4egupETca6psxfSgIjgAxKBYxOCu0LSluKYwBJWI15YPJXfL6aVRdxCGhP8uhQXcTpBVMn8NPIN8zU7_K3iTJuzoZnYx', + 'Authorization': ENV.apiKey, }; return HttpAdapter(client: client, headers: headers); } diff --git a/lib/main.dart b/lib/main.dart index a819cbf..64ae30e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'core/core.dart'; import 'restaurant_tour.dart'; -void main() { +void main() async { + await ApplicationConfig.setUp(); runApp(const RestaurantTour()); } diff --git a/pubspec.lock b/pubspec.lock index 70dd6f0..e736060 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,30 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" - url: "https://pub.dev" - source: hosted - version: "67.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - args: - dependency: transitive - description: - name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" async: dependency: transitive description: @@ -49,70 +25,6 @@ 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: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" - url: "https://pub.dev" - source: hosted - version: "2.4.11" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe - url: "https://pub.dev" - source: hosted - version: "7.3.1" - 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: @@ -121,14 +33,6 @@ 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" clock: dependency: transitive description: @@ -137,14 +41,6 @@ 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: @@ -153,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" crypto: dependency: transitive description: @@ -169,14 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" - url: "https://pub.dev" - source: hosted - version: "2.3.6" fake_async: dependency: transitive description: @@ -185,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + faker: + dependency: "direct dev" + description: + name: faker + sha256: "544c34e9e1d322824156d5a8d451bc1bb778263b892aded24ec7ba77b0706624" + url: "https://pub.dev" + source: hosted + version: "2.2.0" ffi: dependency: transitive description: @@ -201,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -222,6 +102,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -240,30 +128,6 @@ packages: 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: "direct main" description: @@ -272,14 +136,6 @@ packages: 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: @@ -288,38 +144,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - 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: @@ -352,14 +176,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" matcher: dependency: transitive description: @@ -384,14 +200,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.0" - mime: - dependency: transitive - description: - name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" - url: "https://pub.dev" - source: hosted - version: "1.0.6" nested: dependency: transitive description: @@ -400,14 +208,6 @@ packages: 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: @@ -456,14 +256,6 @@ packages: 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" provider: dependency: transitive description: @@ -472,22 +264,6 @@ packages: 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: @@ -544,43 +320,11 @@ packages: 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: @@ -605,14 +349,6 @@ 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: @@ -637,14 +373,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" typed_data: dependency: transitive description: @@ -669,14 +397,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.1" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" web: dependency: transitive description: @@ -685,22 +405,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.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: @@ -709,14 +413,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index f51121e..d70e1e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,16 +14,15 @@ dependencies: flutter: sdk: flutter http: ^1.2.2 - json_annotation: ^4.9.0 flutter_bloc: ^8.1.6 shared_preferences: ^2.3.2 + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^4.0.0 - build_runner: ^2.4.10 - json_serializable: ^6.8.0 + faker: ^2.2.0 flutter: generate: true @@ -47,4 +46,6 @@ flutter: style: italic - asset: assets/fonts/OpenSans/OpenSans-SemiBold.ttf weight: 600 - + + assets: + - .env From dedac72380b569c6f5f5e2ef7a2bc12f72fc99b9 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:48:32 -0300 Subject: [PATCH 69/91] refactor: app adjustments --- lib/restaurant_tour.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/restaurant_tour.dart b/lib/restaurant_tour.dart index a5cc804..e617038 100644 --- a/lib/restaurant_tour.dart +++ b/lib/restaurant_tour.dart @@ -10,15 +10,8 @@ class RestaurantTour extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Restaurant Tour', - initialRoute: '/', - routes: { - '/': (_) => makeRestaurantTourPage(), - }, - theme: ThemeData.from( - colorScheme: ColorScheme.fromSwatch( - backgroundColor: const Color(0xFFFAFAFA), - ), - ), + home: makeRestaurantTourPage(), + theme: ThemeData.from(colorScheme: ColorScheme.fromSwatch(backgroundColor: const Color(0xFFFAFAFA))), ); } } From 86d3adbe047e1e217e145b2b18c4632c3babe965 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 00:53:06 -0300 Subject: [PATCH 70/91] feat: add force material transparency on detail page --- lib/ui/pages/restaurant_detail/restaurant_detail_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart index 5e8ae1b..644bdbb 100644 --- a/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart +++ b/lib/ui/pages/restaurant_detail/restaurant_detail_page.dart @@ -27,6 +27,7 @@ class RestaurantDetailPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + forceMaterialTransparency: true, leading: const _ArrowBackIcon(), actions: [ _FavoriteIcon( From 46f13be3b454979e99b3ad1d33d33aef3f5c18d6 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 12:23:11 -0300 Subject: [PATCH 71/91] chore: add mocktail as dependency --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index e736060..5b2c447 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -200,6 +200,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d70e1e3..448e1f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^4.0.0 faker: ^2.2.0 + mocktail: ^1.0.4 flutter: generate: true From 33e0efe5358891af3f1fc280eff5077836c27291 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 14:01:01 -0300 Subject: [PATCH 72/91] test: add factories on tests --- .../infra/http/http_client_factory.dart | 2 +- test/core/factories.dart | 161 ++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 test/core/factories.dart diff --git a/lib/factories/infra/http/http_client_factory.dart b/lib/factories/infra/http/http_client_factory.dart index 0d50889..63058ca 100644 --- a/lib/factories/infra/http/http_client_factory.dart +++ b/lib/factories/infra/http/http_client_factory.dart @@ -7,7 +7,7 @@ HttpAdapter makeHttpAdapter() { final client = Client(); final headers = { 'Content-Type': 'application/graphql', - 'Authorization': ENV.apiKey, + 'Authorization': 'Bearer ${ENV.apiKey}', }; return HttpAdapter(client: client, headers: headers); } diff --git a/test/core/factories.dart b/test/core/factories.dart new file mode 100644 index 0000000..55b91f9 --- /dev/null +++ b/test/core/factories.dart @@ -0,0 +1,161 @@ +import 'package:faker/faker.dart'; +import 'package:restaurant_tour/data/models/category_model.dart'; +import 'package:restaurant_tour/data/models/local_restaurant_model.dart'; +import 'package:restaurant_tour/data/models/remote_restaurant_model.dart'; +import 'package:restaurant_tour/data/models/review_model.dart'; +import 'package:restaurant_tour/data/models/user_model.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; + +RemoteRestaurantModel makeRemoteRestaurantModel() { + return RemoteRestaurantModel( + id: faker.guid.guid(), + categories: makeListCategoryModel(), + isOpen: faker.randomGenerator.boolean(), + address: faker.address.streetAddress(), + name: faker.person.name(), + photos: [], + price: faker.randomGenerator.decimal(scale: 15, min: 5).toString(), + rating: faker.randomGenerator.decimal(min: 1), + reviews: makeListReviewModel(), + ); +} + +LocalRestaurantModel makeLocalResturantModel() { + return LocalRestaurantModel( + id: faker.guid.guid(), + categories: makeListCategoryModel(), + isOpen: faker.randomGenerator.boolean(), + address: faker.address.streetAddress(), + name: faker.person.name(), + photos: [], + price: faker.randomGenerator.decimal(scale: 15, min: 5).toString(), + rating: faker.randomGenerator.decimal(min: 1), + reviews: makeListReviewModel(), + ); +} + +List makeListCategoryModel() { + return List.generate(10, (_) => makeCategoryModel()); +} + +CategoryModel makeCategoryModel() { + return CategoryModel(title: faker.lorem.sentence(), alias: faker.lorem.word()); +} + +List makeListReviewModel() { + return List.generate(5, (_) => makeReviewModel()); +} + +ReviewModel makeReviewModel() { + return ReviewModel( + id: faker.guid.guid(), + rating: faker.randomGenerator.integer(5, min: 1), + text: faker.lorem.word(), + user: makeUserModel(), + ); +} + +UserModel makeUserModel() { + return UserModel( + id: faker.guid.guid(), + imageUrl: '', + name: faker.person.name(), + ); +} + +List makeListRestaurantEntity() { + return List.generate(4, (_) => makeRestaurantEntity()); +} + +RestaurantEntity makeRestaurantEntity() { + return RestaurantEntity( + id: faker.guid.guid(), + categories: makeListCategoryEntity(), + isOpen: faker.randomGenerator.boolean(), + address: faker.address.streetAddress(), + name: faker.person.name(), + photos: [], + price: faker.randomGenerator.decimal(scale: 15, min: 5).toString(), + rating: faker.randomGenerator.decimal(min: 1), + reviews: makeListReviewEntity(), + ); +} + +List makeListFavoriteRestaurantEntity() { + return List.generate(4, (_) => makeFavoriteRestaurantEntity()); +} + +FavoriteRestaurantEntity makeFavoriteRestaurantEntity() { + return FavoriteRestaurantEntity( + id: faker.guid.guid(), + categories: makeListCategoryEntity(), + isOpen: faker.randomGenerator.boolean(), + address: faker.address.streetAddress(), + name: faker.person.name(), + photos: [], + price: faker.randomGenerator.decimal(scale: 15, min: 5).toString(), + rating: faker.randomGenerator.decimal(min: 1), + reviews: makeListReviewEntity(), + ); +} + +List makeListCategoryEntity() { + return List.generate(10, (_) => makeCategoryEntity()); +} + +CategoryEntity makeCategoryEntity() { + return CategoryEntity(title: faker.lorem.sentence(), alias: faker.lorem.word()); +} + +List makeListReviewEntity() { + return List.generate(5, (_) => makeReviewEntity()); +} + +ReviewEntity makeReviewEntity() { + return ReviewEntity( + id: faker.guid.guid(), + rating: faker.randomGenerator.integer(5, min: 1), + text: faker.lorem.word(), + user: makeUserEntity(), + ); +} + +UserEntity makeUserEntity() { + return UserEntity( + id: faker.guid.guid(), + imageUrl: '', + name: faker.person.name(), + ); +} + +List makeRemoteRestaurantsJson() { + return List.generate( + 4, + (_) => { + 'id': faker.guid.guid(), + 'categories': List.generate(4, (_) => makeCategoryModel().toJson()), + 'hours': [ + {'is_open_now': faker.randomGenerator.boolean()}, + ], + 'location': {'formatted_address': faker.address.streetAddress()}, + 'name': faker.person.name(), + 'photos': [], + 'price': '\$\$\$', + 'rating': faker.randomGenerator.decimal(min: 1), + 'reviews': List.generate(3, (_) => makeReviewModel().toJson()), + }, + ); +} + +List makeLocalRestaurantsJson() { + return List.generate(5, (_) => makeLocalResturantModel().toJson()); +} + +Map makeInvalidJson() { + return { + 'data': { + 'id': faker.guid.guid(), + 'categories': null, + }, + }; +} From 92e321ce1bf580a36f9bd69916a8c4d58a11a907 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 14:01:26 -0300 Subject: [PATCH 73/91] test: add local get favorite restaurants tests --- .../local_get_favorite_restaurants_test.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/data/usecases/local_get_favorite_restaurants_test.dart diff --git a/test/data/usecases/local_get_favorite_restaurants_test.dart b/test/data/usecases/local_get_favorite_restaurants_test.dart new file mode 100644 index 0000000..f5e3623 --- /dev/null +++ b/test/data/usecases/local_get_favorite_restaurants_test.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/usecases/usecases.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; +import 'package:restaurant_tour/infra/infra.dart'; + +import '../../core/factories.dart'; + +class FetchCacheSpy extends Mock implements FetchCache {} + +void main() { + late LocalGetFavoriteRestaurants sut; + late FetchCacheSpy fetchCache; + + When mockFetchCall() => when(() => fetchCache.fetch(any())); + + void mockFetch() { + mockFetchCall().thenAnswer((_) async => jsonEncode(makeLocalRestaurantsJson())); + } + + void mockFetchError() { + mockFetchCall().thenThrow(Exception()); + } + + setUp(() { + fetchCache = FetchCacheSpy(); + sut = LocalGetFavoriteRestaurants(cache: fetchCache); + mockFetch(); + }); + + test('Should call fetchCache with correct value', () async { + await sut(); + verify(() => fetchCache.fetch(any())); + }); + + test('Should return an List', () async { + final result = await sut(); + expect(result, isA>()); + }); + + test('Should throw UnexpectedError if fetchCache throws', () async { + mockFetchError(); + final future = sut(); + expect(future, throwsA(DomainError.unexpected)); + }); +} From 78b244463261ba35d543b07ea2c3f0ab82d72dd7 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 14:01:44 -0300 Subject: [PATCH 74/91] test: add local save favorite restaurant tests --- .../local_save_favorite_restaurants_test.dart | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/data/usecases/local_save_favorite_restaurants_test.dart diff --git a/test/data/usecases/local_save_favorite_restaurants_test.dart b/test/data/usecases/local_save_favorite_restaurants_test.dart new file mode 100644 index 0000000..fb6ea94 --- /dev/null +++ b/test/data/usecases/local_save_favorite_restaurants_test.dart @@ -0,0 +1,48 @@ +import 'package:faker/faker.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/usecases/usecases.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; +import 'package:restaurant_tour/infra/infra.dart'; + +import '../../core/factories.dart'; + +class SaveCacheSpy extends Mock implements SaveCache {} + +void main() { + late SaveCacheSpy saveCache; + late LocalSaveFavoriteRestaurants sut; + late List restaurantList; + + When mockSaveCall() { + return when(() => saveCache.save(key: any(named: 'key'), value: any(named: 'value'))); + } + + void mockSave() { + mockSaveCall().thenAnswer((_) async => faker.randomGenerator.integer(10)); + } + + void mockSaveError() { + mockSaveCall().thenThrow(Exception()); + } + + setUp(() { + saveCache = SaveCacheSpy(); + sut = LocalSaveFavoriteRestaurants(cache: saveCache); + restaurantList = makeListRestaurantEntity(); + mockSave(); + }); + + test('Should call SaveCache with correct value', () async { + await sut(restaurantList); + verify(() => saveCache.save(key: any(named: 'key'), value: any(named: 'value'))); + }); + + test('Should throw UnexpectedError if saveCache throws', () async { + mockSaveError(); + final future = sut(restaurantList); + expect(future, throwsA(DomainError.unexpected)); + }); +} From 33a43205bfcc5ec627fe4dd690e036627d9527ac Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 14:02:04 -0300 Subject: [PATCH 75/91] test: add remote get restaurant tests --- .../usecases/remote_get_restaurants_test.dart | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/data/usecases/remote_get_restaurants_test.dart diff --git a/test/data/usecases/remote_get_restaurants_test.dart b/test/data/usecases/remote_get_restaurants_test.dart new file mode 100644 index 0000000..ceabdee --- /dev/null +++ b/test/data/usecases/remote_get_restaurants_test.dart @@ -0,0 +1,79 @@ +import 'package:faker/faker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/usecases/usecases.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; +import 'package:restaurant_tour/infra/infra.dart'; + +import '../../core/factories.dart'; + +class HttpClientSpy extends Mock implements HttpClient {} + +void main() { + late RemoteGetRestaurants sut; + late HttpClientSpy httpClient; + late String url; + late Map data; + + When mockRequest() { + return when( + () => httpClient.request( + url: any(named: 'url'), + method: any(named: 'method'), + data: any(named: 'data') + ), + ); + } + + void mockHttpData(Map mockData) { + data = mockData; + mockRequest().thenAnswer((_) async => data); + } + + void mockHttpError(HttpError error) { + mockRequest().thenThrow(error); + } + + setUp(() { + url = faker.internet.httpUrl(); + httpClient = HttpClientSpy(); + sut = RemoteGetRestaurants(url: url, client: httpClient); + mockHttpData({ + 'data': { + 'search': { + 'business': makeRemoteRestaurantsJson(), + }, + }, + }); + }); + + test('Should call HttpClient with correct values', () async { + await sut(); + verify(() => httpClient.request(url: any(named: 'url'), method: any(named: 'method'), data: any(named: 'data'))); + }); + + test('Should return restaurants on 200', () async { + final articles = await sut(); + expect(articles, isA>()); + expect(articles, isNotEmpty); + }); + + test('Should throw UnexpectedError if HttpClient returns 200 with invalid data', () async { + mockHttpData({'invalid_key': 'invalid_value'}); + final future = sut(); + expect(future, throwsA(DomainError.unexpected)); + }); + + test('Should throw UnexpectedError if HttpClient returns 400', () async { + mockHttpError(HttpError.badRequest); + final future = sut(); + expect(future, throwsA(DomainError.unexpected)); + }); + + test('Should throw UnexpectedError if HttpClient returns 500', () async { + mockHttpError(HttpError.serverError); + final future = sut(); + expect(future, throwsA(DomainError.unexpected)); + }); +} From 7bb08c6da42f51315d7a65888632ef46c1639653 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 16:47:39 -0300 Subject: [PATCH 76/91] test: add http adapter tests --- lib/infra/http/http_adapter.dart | 6 +- test/infra/http/http_adapter_test.dart | 120 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 test/infra/http/http_adapter_test.dart diff --git a/lib/infra/http/http_adapter.dart b/lib/infra/http/http_adapter.dart index 952e0d7..f058048 100644 --- a/lib/infra/http/http_adapter.dart +++ b/lib/infra/http/http_adapter.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart'; import 'http.dart'; -class HttpAdapter implements HttpClient { +class HttpAdapter implements HttpClient { final Client _client; final Map _headers; @@ -15,7 +15,7 @@ class HttpAdapter implements HttpClient { _headers = headers; @override - Future request({required String url, required String method, String? data}) async { + Future request({required String url, required String method, String? data}) async { final uri = Uri.parse(url); var response = Response('', 500); @@ -30,7 +30,7 @@ class HttpAdapter implements HttpClient { return _handleResponse(response); } - Map _handleResponse(Response response) { + Map? _handleResponse(Response response) { if (response.statusCode == 200) { return response.body.isEmpty ? null : jsonDecode(response.body); } else if (response.statusCode == 400) { diff --git a/test/infra/http/http_adapter_test.dart b/test/infra/http/http_adapter_test.dart new file mode 100644 index 0000000..1ce0a27 --- /dev/null +++ b/test/infra/http/http_adapter_test.dart @@ -0,0 +1,120 @@ +import 'package:faker/faker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/infra/infra.dart'; + +class ClientSpy extends Mock implements Client {} + +class UriFake extends Fake implements Uri {} + +void main() { + late HttpAdapter sut; + late ClientSpy client; + late String url; + late Uri uri; + late String data; + + setUp(() { + client = ClientSpy(); + sut = HttpAdapter(client: client, headers: {'content-type': 'application/json', 'accept': 'application/json'}); + url = faker.internet.httpUrl(); + uri = Uri.parse(url); + data = '{"any_key":"any_value"}'; + }); + + setUpAll(() { + registerFallbackValue(UriFake()); + }); + + group('shared', () { + test('Should throw ServeError if invalid method is provided', () async { + final future = sut.request(url: url, method: 'invalid_method'); + expect(future, throwsA(HttpError.serverError)); + }); + }); + + group('post', () { + When mockRequest() { + return when( + () => client.post(any(), body: any(named: 'body'), headers: any(named: 'headers')), + ); + } + + void mockError() { + mockRequest().thenThrow(Exception()); + } + + void mockResponse(int statusCode, {String body = '{"any_key":"any_value"}'}) { + mockRequest().thenAnswer( + (_) async => Response(body, statusCode), + ); + } + + setUp(() { + mockResponse(200); + }); + + test('Should call post with correct values', () async { + await sut.request( + url: url, + method: 'post', + data: '{"any_key":"any_value"}', + ); + + verify( + () => client.post( + uri, + headers: {'content-type': 'application/json', 'accept': 'application/json'}, + body: data, + ), + ); + }); + + test('Should call post without body', () async { + await sut.request(url: url, method: 'post'); + verify(() => client.post(any(), headers: any(named: 'headers'))); + }); + + test('Should return data if post returns 200', () async { + final response = await sut.request(url: url, method: 'post', data: data); + expect(response, {'any_key': 'any_value'}); + }); + + test('Should return null if post returns 200 with no data', () async { + mockResponse(200, body: ''); + final response = await sut.request(url: url, method: 'post', data: data); + expect(response, null); + }); + + test('Should return BadRequestError if post returns 400', () async { + mockResponse(400, body: ''); + final future = sut.request(url: url, method: 'post', data: data); + expect(future, throwsA(HttpError.badRequest)); + }); + + test('Should return BadRequestError if post returns 400', () async { + mockResponse(400); + final future = sut.request(url: url, method: 'post', data: data); + expect(future, throwsA(HttpError.badRequest),); + }); + + test('Should return UnathorizedError if post returns 401', () async { + mockResponse(401); + final future = sut.request(url: url, method: 'post', data: data); + expect(future, throwsA(HttpError.unauthorized)); + }); + + test('Should return ServerError if post returns 500', () async { + mockResponse(500); + final future = sut.request(url: url, method: 'post', data: data); + expect(future, throwsA(HttpError.serverError)); + }); + + test('Should return ServerError if post throws', () async { + mockError(); + final future = sut.request(url: url, method: 'post', data: data); + expect(future, throwsA(HttpError.serverError)); + }); + }); +} From 00f1087b39da0aa843c3783e5406ecfd3c8d3c0f Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 16:49:11 -0300 Subject: [PATCH 77/91] feat: get restaurants adjustments --- lib/data/usecases/remote_get_restaurants.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/data/usecases/remote_get_restaurants.dart b/lib/data/usecases/remote_get_restaurants.dart index ac78236..f6306b4 100644 --- a/lib/data/usecases/remote_get_restaurants.dart +++ b/lib/data/usecases/remote_get_restaurants.dart @@ -5,11 +5,11 @@ import '../../infra/http/http.dart'; import '../models/models.dart'; class RemoteGetRestaurants implements GetRestaurants { - final HttpClient _client; + final HttpClient _client; final String _url; const RemoteGetRestaurants({ - required HttpClient client, + required HttpClient client, required String url, }) : _client = client, _url = url; @@ -53,7 +53,7 @@ class RemoteGetRestaurants implements GetRestaurants { Future> call() async { try { final response = await _client.request(url: _url, method: 'post', data: query); - return (response['data']['search']['business'] as List).map((item) => RemoteRestaurantModel.fromJson(item).toEntity()).toList(); + return (response?['data']['search']['business'] as List).map((item) => RemoteRestaurantModel.fromJson(item).toEntity()).toList(); } catch (_) { throw DomainError.unexpected; } From 9cccbbf312bd0e20bb8c1ca21cfa41f3b66a8b1f Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:25:08 -0300 Subject: [PATCH 78/91] test: add factories json --- test/core/factories.dart | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/core/factories.dart b/test/core/factories.dart index 55b91f9..d2d3613 100644 --- a/test/core/factories.dart +++ b/test/core/factories.dart @@ -20,7 +20,7 @@ RemoteRestaurantModel makeRemoteRestaurantModel() { ); } -LocalRestaurantModel makeLocalResturantModel() { +LocalRestaurantModel makeLocalRestaurantModel() { return LocalRestaurantModel( id: faker.guid.guid(), categories: makeListCategoryModel(), @@ -148,7 +148,35 @@ List makeRemoteRestaurantsJson() { } List makeLocalRestaurantsJson() { - return List.generate(5, (_) => makeLocalResturantModel().toJson()); + return List.generate(5, (_) => makeLocalRestaurantModel().toJson()); +} + +Map makeCategoryJson() { + return { + 'title': faker.lorem.word(), + 'alias': faker.lorem.word(), + }; +} + +Map makeReviewJson() { + return { + 'id': faker.guid.guid(), + 'rating': faker.randomGenerator.integer(5, min: 1), + 'text': faker.lorem.sentence(), + 'user': { + 'id': faker.guid.guid(), + 'image_url': '', + 'name': faker.person.name(), + }, + }; +} + +Map makeUserJson() { + return { + 'id': faker.guid.guid(), + 'image_url': '', + 'name': faker.person.name(), + }; } Map makeInvalidJson() { From f78618f3efd8b7daf842915e9290d3111c94c939 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:25:21 -0300 Subject: [PATCH 79/91] test: add category modal tests --- test/data/models/category_model_test.dart | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/data/models/category_model_test.dart diff --git a/test/data/models/category_model_test.dart b/test/data/models/category_model_test.dart new file mode 100644 index 0000000..2743c8e --- /dev/null +++ b/test/data/models/category_model_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/data/models/models.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; + +import '../../core/factories.dart'; + +void main() { + late Map categoryJson; + late CategoryModel categoryModel; + late CategoryEntity categoryEntity; + + setUp(() { + categoryJson = makeCategoryJson(); + categoryModel = makeCategoryModel(); + categoryEntity = makeCategoryEntity(); + }); + + test('Should create CategoryModel object when json is valid', () async { + final json = CategoryModel.fromJson(categoryJson); + expect(json, isA()); + }); + + test('Should create CategoryEntity object from CategoryModel object', () async { + final json = CategoryModel.fromEntity(categoryEntity); + expect(json, isA()); + }); + + test('Should throw a unexpected error when json is not valid', () async { + expect(() => CategoryModel.fromJson({'alias': 33}), throwsA(DomainError.unexpected)); + }); + + test('Should create a CategoryEntity object', () async { + final result = categoryModel.toEntity(); + expect(result, isA()); + }); +} From 18c5494475f9bb4659caf30091c599464f8e003a Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:25:33 -0300 Subject: [PATCH 80/91] test: add user model tests --- test/data/models/user_model_test.dart | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/data/models/user_model_test.dart diff --git a/test/data/models/user_model_test.dart b/test/data/models/user_model_test.dart new file mode 100644 index 0000000..31d108d --- /dev/null +++ b/test/data/models/user_model_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/data/models/models.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; + +import '../../core/factories.dart'; + +void main() { + late Map userJson; + late UserModel userModel; + late UserEntity userEntity; + + setUp(() { + userJson = makeUserJson(); + userModel = makeUserModel(); + userEntity = makeUserEntity(); + }); + + test('Should create UserModel object when json is valid', () async { + final json = UserModel.fromJson(userJson); + expect(json, isA()); + }); + + test('Should create UserEntity object from UserModel object', () async { + final json = UserModel.fromEntity(userEntity); + expect(json, isA()); + }); + + test('Should throw a unexpected error when json is not valid', () async { + expect(() => UserModel.fromJson({'id': 33}), throwsA(DomainError.unexpected)); + }); + + test('Should create a UserEntity object', () async { + final result = userModel.toEntity(); + expect(result, isA()); + }); +} From d5e3f550804637575d2f436eccbe132447b5fa87 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:25:44 -0300 Subject: [PATCH 81/91] test: add review model tests --- test/data/models/review_model_test.dart | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/data/models/review_model_test.dart diff --git a/test/data/models/review_model_test.dart b/test/data/models/review_model_test.dart new file mode 100644 index 0000000..c0ff44c --- /dev/null +++ b/test/data/models/review_model_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/data/models/models.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; + +import '../../core/factories.dart'; + +void main() { + late Map reviewJson; + late ReviewModel reviewModel; + late ReviewEntity reviewEntity; + + setUp(() { + reviewJson = makeReviewJson(); + reviewModel = makeReviewModel(); + reviewEntity = makeReviewEntity(); + }); + + test('Should create ReviewModel object when json is valid', () async { + final json = ReviewModel.fromJson(reviewJson); + expect(json, isA()); + }); + + test('Should create ReviewEntity object from ReviewModel object', () async { + final json = ReviewModel.fromEntity(reviewEntity); + expect(json, isA()); + }); + + test('Should throw a unexpected error when json is not valid', () async { + expect(() => ReviewModel.fromJson(makeInvalidJson()), throwsA(DomainError.unexpected)); + }); + + test('Should create a ReviewEntity object', () async { + final result = reviewModel.toEntity(); + expect(result, isA()); + }); +} From a22f2f24890a54bb6802bd9f20b5abc232c2f713 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:25:59 -0300 Subject: [PATCH 82/91] test: add local restaurant model tests --- .../models/local_restaurant_model_test.dart | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/data/models/local_restaurant_model_test.dart diff --git a/test/data/models/local_restaurant_model_test.dart b/test/data/models/local_restaurant_model_test.dart new file mode 100644 index 0000000..5bb0025 --- /dev/null +++ b/test/data/models/local_restaurant_model_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/data/models/models.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; + +import '../../core/factories.dart'; + +void main() { + late Map restaurantJson; + late LocalRestaurantModel localRestaurantModel; + late RestaurantEntity restaurantEntity; + + setUp(() { + restaurantJson = makeLocalRestaurantsJson().first; + localRestaurantModel = makeLocalRestaurantModel(); + restaurantEntity = makeRestaurantEntity(); + }); + + test('Should create LocalRestaurantModel object when json is valid', () async { + final json = LocalRestaurantModel.fromJson(restaurantJson); + expect(json, isA()); + }); + + test('Should create RestaurantEntity object from LocalRestaurantModel object', () async { + final json = LocalRestaurantModel.fromEntity(restaurantEntity); + expect(json, isA()); + }); + + test('Should throw a unexpected error when json is not valid', () async { + expect(() => LocalRestaurantModel.fromJson(makeInvalidJson()), throwsA(DomainError.unexpected)); + }); + + test('Should create a FavoriteRestaurantEntity object', () async { + final result = localRestaurantModel.toEntity(); + expect(result, isA()); + }); +} From abf6600dc0d31b46cc5aca450c88c00b183940a4 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:26:15 -0300 Subject: [PATCH 83/91] test: add remote restaurant model tests --- .../models/remote_restaurant_model_test.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/data/models/remote_restaurant_model_test.dart diff --git a/test/data/models/remote_restaurant_model_test.dart b/test/data/models/remote_restaurant_model_test.dart new file mode 100644 index 0000000..6afb192 --- /dev/null +++ b/test/data/models/remote_restaurant_model_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/data/models/models.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; + +import '../../core/factories.dart'; + +void main() { + late Map restaurantJson; + late RemoteRestaurantModel remoteRestaurantModel; + + setUp(() { + restaurantJson = makeRemoteRestaurantsJson().first; + remoteRestaurantModel = makeRemoteRestaurantModel(); + }); + + test('Should create RemoteRestaurantModel object when json is valid', () async { + final json = RemoteRestaurantModel.fromJson(restaurantJson); + expect(json, isA()); + }); + + test('Should throw a unexpected error when json is not valid', () async { + expect(() => RemoteRestaurantModel.fromJson(makeInvalidJson()), throwsA(DomainError.unexpected)); + }); + + test('Should create a RestaurantEntity object', () async { + final result = remoteRestaurantModel.toEntity(); + expect(result, isA()); + }); +} From 52ee67074a591eef63c9b8f327c8868523a28106 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 17:53:24 -0300 Subject: [PATCH 84/91] test: add local storage adapter tests --- .../cache/local_storage_adapter_test.dart | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/infra/cache/local_storage_adapter_test.dart diff --git a/test/infra/cache/local_storage_adapter_test.dart b/test/infra/cache/local_storage_adapter_test.dart new file mode 100644 index 0000000..a96ebe5 --- /dev/null +++ b/test/infra/cache/local_storage_adapter_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/infra/cache/cache.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late LocalStorageAdapter sut; + late String key; + late String value; + + setUp(() { + key = 'any_key'; + value = 'any_value'; + SharedPreferences.setMockInitialValues({key: value}); + sut = LocalStorageAdapter(); + }); + + group('fetch', () { + test('Should return the value from SharedPreferences', () async { + final result = await sut.fetch(key); + expect(result, value); + }); + + test('Should return null if the key is not found', () async { + final result = await sut.fetch('test'); + expect(result, isNull); + }); + }); + + group('save', () { + test('Should save the value in SharedPreferences', () async { + const key = 'test_key'; + const value = 'test_value'; + await sut.save(key: key, value: value); + final result = await sut.fetch(key); + expect(result, value); + }); + }); +} From 21390ad5f119cc724b64c94dfbde4dba7860e5c9 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 22:17:12 -0300 Subject: [PATCH 85/91] test: add restaurant tour presenter tests --- .../widgets/review_item.dart | 2 +- .../usecases/remote_get_restaurants_test.dart | 2 +- .../cubit_restaurant_tour_presenter_test.dart | 164 ++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart diff --git a/lib/ui/pages/restaurant_detail/widgets/review_item.dart b/lib/ui/pages/restaurant_detail/widgets/review_item.dart index 712392f..da55d8e 100644 --- a/lib/ui/pages/restaurant_detail/widgets/review_item.dart +++ b/lib/ui/pages/restaurant_detail/widgets/review_item.dart @@ -25,7 +25,7 @@ class _ReviewItem extends StatelessWidget { child: Container( height: 40, width: 40, - decoration: const BoxDecoration(shape: BoxShape.circle), + decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.black12), child: userImage.isEmpty ? const Icon(Icons.person) : Image.network(_review.user.imageUrl, fit: BoxFit.fill), ), ), diff --git a/test/data/usecases/remote_get_restaurants_test.dart b/test/data/usecases/remote_get_restaurants_test.dart index ceabdee..2c839b2 100644 --- a/test/data/usecases/remote_get_restaurants_test.dart +++ b/test/data/usecases/remote_get_restaurants_test.dart @@ -21,7 +21,7 @@ void main() { () => httpClient.request( url: any(named: 'url'), method: any(named: 'method'), - data: any(named: 'data') + data: any(named: 'data'), ), ); } diff --git a/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart b/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart new file mode 100644 index 0000000..882307f --- /dev/null +++ b/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/domain/helpers/helpers.dart'; +import 'package:restaurant_tour/domain/usecases/usecases.dart'; +import 'package:restaurant_tour/presentation/presentation.dart'; + +import '../../core/factories.dart'; + +class SpyGetRestaurants extends Mock implements GetRestaurants {} + +class SpyGetFavoriteRestaurants extends Mock implements GetFavoriteRestaurants {} + +class SpySaveFavoriteRestaurants extends Mock implements SaveFavoriteRestaurants {} + +class FakeArticleEntity extends Fake implements RestaurantEntity {} + +class FakefavoriteRestaurantEntity extends Fake implements FavoriteRestaurantEntity {} + +void main() { + late GetRestaurants getRestaurants; + late GetFavoriteRestaurants getFavoriteRestaurants; + late SaveFavoriteRestaurants saveFavoriteRestaurants; + late CubitRestaurantTourPresenter sut; + late List restaurants; + late RestaurantEntity restaurantEntity; + late List favoriteRestaurants; + + When mockGetRestaurantsCall() { + return when(() => getRestaurants()); + } + + void mockGetRestaurantsSuccess() { + mockGetRestaurantsCall().thenAnswer((_) => Future.value(restaurants)); + } + + void mockGetRestaurantsError() { + mockGetRestaurantsCall().thenThrow(DomainError.unexpected); + } + + When mockGetFavoriteRestaurantsCall() { + return when(() => getFavoriteRestaurants()); + } + + void mockGetFavoriteRestaurantsSuccess() { + mockGetFavoriteRestaurantsCall().thenAnswer((_) => Future.value(favoriteRestaurants)); + } + + void mockGetFavoriteRestaurantsError() { + mockGetFavoriteRestaurantsCall().thenThrow(DomainError.unexpected); + } + + When mockSaveFavoriteRestaurantsCall() { + return when(() => saveFavoriteRestaurants(any())); + } + + void mockSaveFavoriteRestaurantsSuccess() { + mockSaveFavoriteRestaurantsCall().thenAnswer((_) => Future.value()); + } + + void mockSaveFavoriteRestaurantsError() { + mockSaveFavoriteRestaurantsCall().thenThrow(DomainError.unexpected); + } + + void expectSuccessFlowEventsEmitted() { + expectLater( + sut.stream, + emitsInOrder([isA(), isA()]), + ); + } + + void expectErrorFlowEventsEmitted() { + expectLater( + sut.stream, + emitsInOrder([isA(), isA()]), + ); + } + + void expectErrorEmitted() { + expectLater(sut.stream, emits(isA())); + } + + setUp(() { + getRestaurants = SpyGetRestaurants(); + getFavoriteRestaurants = SpyGetFavoriteRestaurants(); + saveFavoriteRestaurants = SpySaveFavoriteRestaurants(); + + sut = CubitRestaurantTourPresenter( + getRestaurants: getRestaurants, + getFavoriteRestaurants: getFavoriteRestaurants, + saveFavoriteRestaurants: saveFavoriteRestaurants, + ); + + restaurants = makeListRestaurantEntity(); + restaurantEntity = restaurants[0]; + favoriteRestaurants = makeListFavoriteRestaurantEntity(); + + mockGetRestaurantsSuccess(); + mockGetFavoriteRestaurantsSuccess(); + mockSaveFavoriteRestaurantsSuccess(); + }); + + setUpAll(() { + registerFallbackValue(FakeArticleEntity()); + registerFallbackValue(FakefavoriteRestaurantEntity()); + }); + + group('getAllRestaurants', () { + test('Should emits [RestaurantLoadingState, RestaurantSuccessState] when getAllRestaurants was success', () async { + expectSuccessFlowEventsEmitted(); + await sut.getAllRestaurants(); + }); + + test('Should restaurantList is not empty when getAllRestaurants was success', () async { + sut.restaurantList.clear(); + expect(sut.restaurantList.isEmpty, isTrue); + await sut.getAllRestaurants(); + expect(sut.restaurantList.isNotEmpty, isTrue); + }); + + test('Should emits [RestaurantLoadingState, RestaurantErrorState] when getAllRestaurants fails', () async { + mockGetRestaurantsError(); + expectErrorFlowEventsEmitted(); + await sut.getAllRestaurants(); + }); + }); + + group('getFavoriteRestaurants', () { + test('Should emits [RestaurantLoadingState, RestaurantSuccessState] when getFavoriteRestaurants was success', () async { + expectSuccessFlowEventsEmitted(); + await sut.getFavoriteRestaurants(); + }); + + test('Should groupedFavoriteNews is not empty when getFavoriteRestaurants was success', () async { + expect(sut.favoriteRestaurantList.isEmpty, isTrue); + await sut.getFavoriteRestaurants(); + expect(sut.favoriteRestaurantList.isNotEmpty, isTrue); + }); + + test('Should emits [RestaurantLoadingState, RestaurantErrorState] when getFavoriteRestaurants fails', () async { + mockGetFavoriteRestaurantsError(); + expectErrorFlowEventsEmitted(); + await sut.getFavoriteRestaurants(); + }); + }); + + group('addFavoriteRestaurant', () { + setUp(() { + sut.getAllRestaurants(); + }); + + test('Should restaurantList have restaurant added as favorite when addFavoriteRestaurant was success', () async { + await sut.addFavoriteRestaurant(restaurantEntity); + final result = sut.restaurantList.firstWhere((item) => item.id == restaurantEntity.id); + expect(result, isA()); + }); + + test('Should emits [RestaurantErrorState] when addFavoriteRestaurant fails', () async { + mockSaveFavoriteRestaurantsError(); + expectErrorEmitted(); + await sut.addFavoriteRestaurant(restaurantEntity); + }); + }); +} From 6e4d6e600ccfa6516796724ffc9c3418cf068ebd Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 22:58:36 -0300 Subject: [PATCH 86/91] chore: add bloc_test as dev dependency --- pubspec.lock | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 2 files changed, 241 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 5b2c447..15ff885 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -17,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -49,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -57,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" fake_async: dependency: transitive description: @@ -128,6 +184,22 @@ packages: 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" http: dependency: "direct main" description: @@ -136,6 +208,14 @@ packages: 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: @@ -144,6 +224,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + 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" leak_tracker: dependency: transitive description: @@ -176,6 +272,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: @@ -200,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" mocktail: dependency: "direct dev" description: @@ -216,6 +328,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -264,6 +392,14 @@ packages: 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" provider: dependency: transitive description: @@ -272,6 +408,14 @@ packages: 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" shared_preferences: dependency: "direct main" description: @@ -328,11 +472,59 @@ packages: 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_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -373,6 +565,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -381,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" typed_data: dependency: transitive description: @@ -405,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: @@ -413,6 +629,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" xdg_directories: dependency: transitive description: @@ -421,6 +653,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 448e1f5..1946da7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dev_dependencies: flutter_lints: ^4.0.0 faker: ^2.2.0 mocktail: ^1.0.4 + bloc_test: ^9.1.7 flutter: generate: true From 0b2989d7f69daed4e358b5ad8c3e6b83442ce64c Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 22:58:53 -0300 Subject: [PATCH 87/91] test: add restaurant tour page tests --- .../widgets/favorite_restaurant_list.dart | 3 + .../widgets/restaurant_item.dart | 3 + .../widgets/restaurant_list.dart | 3 + .../restaurant_tour_page_test.dart | 153 ++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 test/ui/pages/restaurant_tour/restaurant_tour_page_test.dart diff --git a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart index 24abfd2..a2467f2 100644 --- a/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/favorite_restaurant_list.dart @@ -1,5 +1,7 @@ part of '../restaurant_tour_page.dart'; +const favoriteRestaurantListKey = Key('_FavoriteRestaurantList'); + class _FavoriteRestaurantList extends StatelessWidget { const _FavoriteRestaurantList(); @@ -14,6 +16,7 @@ class _FavoriteRestaurantList extends StatelessWidget { return const _MessageContent('No favorites.'); } else { return ListView.separated( + key: favoriteRestaurantListKey, itemCount: favoriteRestaurantList.length, padding: const EdgeInsets.all(16), itemBuilder: (context, index) { diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart index be8eb45..8d8a19f 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_item.dart @@ -1,5 +1,7 @@ part of '../restaurant_tour_page.dart'; +Key restaurantItemKey(String id) => Key('_RestaurantItem::$id'); + class _RestaurantItem extends StatelessWidget { final RestaurantEntity _restaurant; @@ -29,6 +31,7 @@ class _RestaurantItem extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( + key: restaurantItemKey(_restaurant.id), onTap: () => _onItemTapped(context), child: Container( padding: const EdgeInsets.all(8), diff --git a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart index 3a223f4..3ca53b3 100644 --- a/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart +++ b/lib/ui/pages/restaurant_tour/widgets/restaurant_list.dart @@ -1,5 +1,7 @@ part of '../restaurant_tour_page.dart'; +const restaurantListKey = Key('_RestaurantList'); + class _RestaurantList extends StatelessWidget { const _RestaurantList(); @@ -10,6 +12,7 @@ class _RestaurantList extends StatelessWidget { return const _MessageContent('No restaurants.'); } else { return ListView.separated( + key: restaurantListKey, padding: const EdgeInsets.all(16), itemCount: restaurantList.length, itemBuilder: (context, index) { diff --git a/test/ui/pages/restaurant_tour/restaurant_tour_page_test.dart b/test/ui/pages/restaurant_tour/restaurant_tour_page_test.dart new file mode 100644 index 0000000..f6eb411 --- /dev/null +++ b/test/ui/pages/restaurant_tour/restaurant_tour_page_test.dart @@ -0,0 +1,153 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/entities/entities.dart'; +import 'package:restaurant_tour/presentation/presentation.dart'; +import 'package:restaurant_tour/ui/pages/pages.dart'; +import 'package:restaurant_tour/ui/widgets/widgets.dart'; + +import '../../../core/factories.dart'; + +class SpyCubitRestaurantTourPresenter extends MockCubit implements CubitRestaurantTourPresenter {} + +class SpyNavigatorObserver extends Mock implements NavigatorObserver {} + +class FakeRestaurantEntity extends Fake implements RestaurantEntity {} + +class FakeFavoriteRestaurantEntity extends Fake implements FavoriteRestaurantEntity {} + +class FakeRoute extends Fake implements Route {} + +void main() { + late CubitRestaurantTourPresenter presenter; + late NavigatorObserver navigatorObserver; + late RestaurantEntity restaurantEntity; + late FavoriteRestaurantEntity favoriteRestaurantEntity; + + void mockErrorState() { + whenListen( + presenter, + Stream.fromIterable([RestaurantErrorState('any_message')]), + initialState: RestaurantInitialState(), + ); + } + + void mockLoadingState() { + whenListen( + presenter, + Stream.fromIterable([RestaurantLoadingState()]), + initialState: RestaurantInitialState(), + ); + } + + void mockSuccessState() { + whenListen( + presenter, + Stream.fromIterable([RestaurantSuccessState()]), + initialState: RestaurantInitialState(), + ); + } + + void mockGetAllRestaurants() { + when(() => presenter.getAllRestaurants()).thenAnswer((_) => Future.value()); + } + + void mockGetFavoriteRestaurants() { + when(() => presenter.getFavoriteRestaurants()).thenAnswer((_) => Future.value()); + } + + void mockRestaurantList({restaurantList = const []}) { + when(() => presenter.restaurantList).thenReturn(restaurantList); + } + + void mockFavoriteRestaurantList({favoriteRestaurantList = const []}) { + when(() => presenter.favoriteRestaurantList).thenReturn(favoriteRestaurantList); + } + + setUp(() { + presenter = SpyCubitRestaurantTourPresenter(); + navigatorObserver = SpyNavigatorObserver(); + restaurantEntity = makeRestaurantEntity(); + favoriteRestaurantEntity = makeFavoriteRestaurantEntity(); + mockSuccessState(); + mockGetAllRestaurants(); + mockGetFavoriteRestaurants(); + mockRestaurantList(); + mockFavoriteRestaurantList(); + }); + + setUpAll(() { + registerFallbackValue(FakeRestaurantEntity()); + registerFallbackValue(FakeFavoriteRestaurantEntity()); + registerFallbackValue(FakeRoute()); + }); + + Future loadPage(WidgetTester tester) async { + final page = BlocProvider.value( + value: presenter, + child: MaterialApp( + home: RestaurantTourPage(presenter: presenter), + navigatorObservers: [navigatorObserver], + ), + ); + await tester.pumpWidget(page); + } + + testWidgets('Should load the page with correct widgets', (tester) async { + await loadPage(tester); + expect(find.byType(TabBar), findsOneWidget); + expect(find.byType(TabBarView), findsOneWidget); + expect(find.text('No restaurants.'), findsOneWidget); + expect(find.byKey(restaurantListKey), findsNothing); + }); + + testWidgets('Should show a list of restaurants when there is a restaurant', (tester) async { + mockRestaurantList(restaurantList: [restaurantEntity]); + await loadPage(tester); + expect(find.byKey(restaurantListKey), findsOneWidget); + }); + + testWidgets('Should favorites content when favorite tab was tapped', (tester) async { + await loadPage(tester); + final favoriteTab = find.widgetWithText(Tab, 'My Favorites'); + await tester.tap(favoriteTab); + await tester.pumpAndSettle(); + expect(find.text('No favorites.'), findsOneWidget); + }); + + testWidgets('Should show a list of favorite restaurants when there is a favorite restaurant', (tester) async { + mockFavoriteRestaurantList(favoriteRestaurantList: [favoriteRestaurantEntity]); + await loadPage(tester); + final favoriteTab = find.widgetWithText(Tab, 'My Favorites'); + await tester.tap(favoriteTab); + await tester.pumpAndSettle(); + expect(find.text('No favorites.'), findsNothing); + expect(find.byKey(favoriteRestaurantListKey), findsOneWidget); + }); + + testWidgets('Should show a SnackBar when [RestaurantErrorState] was emitted', (tester) async { + mockErrorState(); + await loadPage(tester); + await tester.pumpAndSettle(); + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('any_message'), findsOneWidget); + }); + + testWidgets('Should show a CircularLoading when [RestaurantLoadingState] was emitted', (tester) async { + mockLoadingState(); + await loadPage(tester); + await tester.pump(const Duration(milliseconds: 200)); + expect(find.byType(CircularLoading), findsOneWidget); + }); + + testWidgets('Should navigate when RestaurantItem was tapped', (tester) async { + mockRestaurantList(restaurantList: [restaurantEntity]); + await loadPage(tester); + final restaurantItem = find.byKey(restaurantItemKey(restaurantEntity.id)); + await tester.tap(restaurantItem); + await tester.pumpAndSettle(); + verify(() => navigatorObserver.didPush(any(), any())); + }); +} From 192db154f061bd0a14f924962d156fa8257ed120 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Tue, 17 Sep 2024 23:44:35 -0300 Subject: [PATCH 88/91] feat: update README --- README.md | 209 +++++++++++++----------------------------------------- 1 file changed, 48 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 412d444..d7758c6 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,49 @@ # Restaurant Tour -Welcome to Superformula's Coding challenge, we are excited to see what you can build! +## Overview -This take home test aims to evaluate your skills in building a Flutter application. We are looking for a well-structured and well-tested application that demonstrates your knowledge of Flutter and the Dart language. +This project is a solution to the **Superformula's Coding Challenge**, designed to demonstrate skills in Flutter development and architecture. The focus of the project is on creating a scalable, well-structured, and well-tested Flutter application that allows users to explore restaurants using the Yelp GraphQL API. -We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter application. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask. +## Key Features -Things we'll be looking on your submission: -- App structure for scalability -- Error and optional (?) handling -- Widget tree optimization -- State management -- Test coverage +- **Restaurant Tour Page**: Displays a list of restaurants, allowing users to view details and add favorites. The app also shows restaurants by categories such as price, name, and rating. +- **Restaurant Detail Page**: Provides detailed information about the selected restaurant, including user reviews, restaurant category, and other essential details. +- **Favorites Feature**: Users can favorite businesses, with favorites being stored locally using **SharedPreferences**. -Think of the app you'll be building as the final product, do not over engineer it for possible future features, but do not under engineer it either. We are looking for a balance. We want that the functionalities that you implement are well thought out and implemented. +## Architecture and State Management -As an example, for the favorites feature you can simply use SharedPreferences, you don't need to use a complex database solution, but we're looking for a solid shared preferences implementation. +This application follows the principles of **Clean Architecture** to ensure scalability, separation of concerns, and maintainability. The following layers were implemented: +1. **Domain Layer**: Contains the business logic, including the use of usecases and entities. +2. **Data Layer**: Responsible for data fetching and caching, using the Yelp GraphQL API and storing API keys using the `flutter_dotenv` package for secure management. +3. **Presentation Layer**: Flutter widgets and state management using **flutter_bloc** for managing UI states, and reactions. +I utilized several design patterns to create a solid structure, such as: +- **Factory Pattern**: For creating complex objects and handling dependencies dynamically. +- **Adapter Pattern**: To allow integration of different API responses with internal models. +- **Strategy Pattern**: For handling various business logic strategies. + +In addition, I applied **SOLID** principles where appropriate, ensuring that classes and components are modular, extensible, and maintainable. -Be sure to read **all** of this document carefully, and follow the guidelines within. +### State Management + +The state management in this project is handled using the **flutter_bloc** library. + +### API Key Management + +The **flutter_dotenv** package is used to securely store and manage the API Key needed for communicating with the Yelp GraphQL API. The API key is stored in an environment file, which is then loaded into the application at runtime. + +## Testing + +A comprehensive test strategy was implemented to cover different parts of the application. The project includes: +- **Unit Tests**: For testing business logic and data manipulation within usecases and adapters. +- **Widget Tests**: For testing the UI components. + +## Steps to run the application -## Vendorized Flutter +## 1. Install fvm -3. We use [fvm](https://fvm.app/) for managing the flutter version within the project. Using terminal, while being on the test repository, install the tools dependencies by running the following commands: +1. [fvm](https://fvm.app/) for managing the flutter version within the project. Using terminal, while being on the test repository, install the tools dependencies by running the following commands: ```sh dart pub global activate fvm @@ -35,168 +55,35 @@ Be sure to read **all** of this document carefully, and follow the guidelines wi export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables ``` -4. Install the project's flutter version using `fvm`. +2. Install the project's flutter version using `fvm`. ```sh fvm use ``` -5. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. +3. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. ```sh fvm flutter pub get ``` -More information on the approach can be found here: - -> hhttps://fvm.app/docs/getting_started/installation - -From the root directory: - - -### IDE Setup - -
-Use with VSCode -

- -If you're a VScode user link the new Flutter SDK path in your settings -`$projectRoot/.vscode/settings.json` (create if it doesn't exist yet) - -```json -{ - "dart.flutterSdkPath": ".fvm/flutter_sdk" -} -``` - - -

-
- -
-Use with IntelliJ / Android Studio -

- -Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK path to `$projectRoot/.fvm/flutter_sdk` - -IntelliJ Settings - -

-
- -## Requirements - -### App Structure - -#### Restaurant List Page - -- Tab Bar - - List of favorites (stored client side) - - List of businesses - - Hero image - - Name - - Price - - Category - - Rating (rounded to the nearest value) - - Open/Closed - -#### Restaurant Detail View - -- Ability to favorite a business -- Name -- Hero image -- Price and category -- Address -- Rating -- Total reviews -- List of reviews - - User name - - Rating - - User image - - Review Text (These are just snippets of the full review, usually like 3-4 lines long) - -#### Misc. - -- Clear documentation on the structure and architecture of your application. -- Clear and logical commit messages. - - We suggest following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - -## Test Coverage - -To demonstrate your experience writing different types of tests in Flutter please do the following: - -- We are looking to see how you write tests in Flutter. We are not looking for 100% coverage but we are looking for a good mix of unit and widget tests. -- We are specially looking for you to cover at least one file for each domain layer (interface, application, repositories, etc). - -Feel free to add more tests as you see fit but the above is the minimum requirement. - -## Design - -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. - -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) - -## API - -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: - -1. Please go to https://www.yelp.com/signup and sign up for a developer account. -1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. -1. Create a new app by filling out the required information. -1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. - -### Solid testing approach +### 2. Create the environment file -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. +1. Create a .env file in the root of your project with the following content: -## Q&A + ```bash + API_KEY=API_KEY + BASE_URL=BASE_URL -> Where should I send back the result when I'm done? +### 3. Run on iOS simulator -Please fork this repo and then send us a pull request to our repo when you think you are done. There is no deadline for this task unless otherwise noted to you directly. +1. Open the iOS simulator via terminal: -> What if I have a question? + ```bash + open -a Simulator -Just create a new issue in this repo and we will respond and get back to you quickly. +2. With the iOS simulator running, go back to the Flutter project root directory and run: -## Review + ```bash + flutter run -The coding challenge is a take-home test upon which we'll be conducting a thorough code review once complete. The review will consist of meeting some more of our mobile engineers and giving a review of the solution you have designed. Please be prepared to share your screen and run/demo the application to the group. During this process, the engineers will be asking questions. From b9a36837581217ff98c1fcbd6bb7a2509abfcf52 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Wed, 18 Sep 2024 00:03:20 -0300 Subject: [PATCH 89/91] refactor: duplicated code to a helper method on restaurant tour presenter --- .../cubit_restaurant_tour_presenter.dart | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 61990b8..6fb5f00 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -86,6 +86,18 @@ class CubitRestaurantTourPresenter extends Cubit implements Res ); } + void _setLoading() { + emit(RestaurantLoadingState()); + } + + void _setSuccess() { + emit(RestaurantSuccessState()); + } + + void _setError(String message) { + emit(RestaurantErrorState(message)); + } + final _restaurantList = []; final _favoriteRestaurantList = []; @@ -98,22 +110,22 @@ class CubitRestaurantTourPresenter extends Cubit implements Res @override Future getFavoriteRestaurants() async { try { - emit(RestaurantLoadingState()); + _setLoading(); _favoriteRestaurantList.addAll(await _getFavoriteRestaurants()); - emit(RestaurantSuccessState()); + _setSuccess(); } catch (_) { - emit(RestaurantErrorState('Error loading favorite restaurants. Please try again later.')); + _setError('Error loading favorite restaurants. Please try again later.'); } } @override Future getAllRestaurants() async { try { - emit(RestaurantLoadingState()); + _setLoading(); _restaurantList.addAll(await _getRestaurants()); - emit(RestaurantSuccessState()); + _setSuccess(); } catch (_) { - emit(RestaurantErrorState('Oops! We had trouble finding the restaurants.')); + _setError('Oops! We had trouble finding the restaurants.'); } } @@ -124,7 +136,7 @@ class CubitRestaurantTourPresenter extends Cubit implements Res _updateRestaurantAsFavorite(restaurant, favoriteRestaurant); await _saveFavoriteRestaurants(_favoriteRestaurantList); } catch (_) { - emit(RestaurantErrorState('Error fetching restaurants. Try again later.')); + _setError('Error fetching restaurants. Try again later.'); } } @@ -134,9 +146,9 @@ class CubitRestaurantTourPresenter extends Cubit implements Res final restaurant = _makeRestaurantFromFavoriteRestaurant(favoriteRestaurant); _removeFavoriteRestaurant(restaurant, favoriteRestaurant); await _saveFavoriteRestaurants(_favoriteRestaurantList); - emit(RestaurantSuccessState()); + _setSuccess(); } catch (_) { - emit(RestaurantErrorState('Oops! There was an issue removing the restaurant from your favorites.')); + _setError('Oops! There was an issue removing the restaurant from your favorites.'); } } } From f2cba7397a7a91066b17f577d9d4d731de8439b8 Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Wed, 18 Sep 2024 00:05:13 -0300 Subject: [PATCH 90/91] refactor: improve add favorite restaurant error message --- .../restaurant_tour/cubit_restaurant_tour_presenter.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart index 6fb5f00..32ffc79 100644 --- a/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart +++ b/lib/presentation/restaurant_tour/cubit_restaurant_tour_presenter.dart @@ -136,7 +136,7 @@ class CubitRestaurantTourPresenter extends Cubit implements Res _updateRestaurantAsFavorite(restaurant, favoriteRestaurant); await _saveFavoriteRestaurants(_favoriteRestaurantList); } catch (_) { - _setError('Error fetching restaurants. Try again later.'); + _setError('Error favoriting restaurant. Try again later.'); } } From 5bc62736eb62a24826bbdec089c26c99c302f84b Mon Sep 17 00:00:00 2001 From: Alex Oliveira Date: Wed, 18 Sep 2024 10:45:30 -0300 Subject: [PATCH 91/91] test: improve presentation function names --- .../cubit_restaurant_tour_presenter_test.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart b/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart index 882307f..95f5dec 100644 --- a/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart +++ b/test/presentation/restaurant_tour/cubit_restaurant_tour_presenter_test.dart @@ -62,14 +62,14 @@ void main() { mockSaveFavoriteRestaurantsCall().thenThrow(DomainError.unexpected); } - void expectSuccessFlowEventsEmitted() { + void expectSuccessFlowStatesEmitted() { expectLater( sut.stream, emitsInOrder([isA(), isA()]), ); } - void expectErrorFlowEventsEmitted() { + void expectErrorFlowStatesEmitted() { expectLater( sut.stream, emitsInOrder([isA(), isA()]), @@ -107,7 +107,7 @@ void main() { group('getAllRestaurants', () { test('Should emits [RestaurantLoadingState, RestaurantSuccessState] when getAllRestaurants was success', () async { - expectSuccessFlowEventsEmitted(); + expectSuccessFlowStatesEmitted(); await sut.getAllRestaurants(); }); @@ -120,14 +120,14 @@ void main() { test('Should emits [RestaurantLoadingState, RestaurantErrorState] when getAllRestaurants fails', () async { mockGetRestaurantsError(); - expectErrorFlowEventsEmitted(); + expectErrorFlowStatesEmitted(); await sut.getAllRestaurants(); }); }); group('getFavoriteRestaurants', () { test('Should emits [RestaurantLoadingState, RestaurantSuccessState] when getFavoriteRestaurants was success', () async { - expectSuccessFlowEventsEmitted(); + expectSuccessFlowStatesEmitted(); await sut.getFavoriteRestaurants(); }); @@ -139,7 +139,7 @@ void main() { test('Should emits [RestaurantLoadingState, RestaurantErrorState] when getFavoriteRestaurants fails', () async { mockGetFavoriteRestaurantsError(); - expectErrorFlowEventsEmitted(); + expectErrorFlowStatesEmitted(); await sut.getFavoriteRestaurants(); }); });