From f02cfb858be9aab47671cf06a8b48f3b4e14d54b Mon Sep 17 00:00:00 2001 From: Helder Date: Fri, 13 Sep 2024 17:37:47 -0300 Subject: [PATCH 1/7] Initial version of the app calling the api and the pages in the early stage of development --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 +++ lib/app.dart | 56 ++++ .../data/datasources/home_data_source.dart | 59 ++++ .../data/repositories/home_repository.dart | 16 + .../home_repository_interface.dart | 7 + .../domain/usecases/get_all_restaurants.dart | 13 + lib/home/failures/error_handler_failure.dart | 36 +++ lib/home/failures/failures.dart | 96 ++++++ lib/home/presentation/bloc/home_bloc.dart | 28 ++ lib/home/presentation/bloc/home_event.dart | 5 + lib/home/presentation/bloc/home_state.dart | 18 ++ lib/home/presentation/page/home.dart | 75 +++++ .../presentation/page/restaurant_details.dart | 110 +++++++ .../tabs/all_restaurants_tab.dart | 45 +++ .../presentation/tabs/my_favorities_tab.dart | 26 ++ lib/home/presentation/tabs/tab_item.dart | 25 ++ lib/home/presentation/widgets/header.dart | 35 +++ lib/home/presentation/widgets/loading.dart | 16 + .../presentation/widgets/restaurant_card.dart | 79 +++++ lib/main.dart | 12 +- lib/navigator.dart | 52 ++++ lib/routes.dart | 30 ++ pubspec.lock | 277 ++++++++++++++++-- pubspec.yaml | 6 + 26 files changed, 1146 insertions(+), 22 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/app.dart create mode 100644 lib/home/data/datasources/home_data_source.dart create mode 100644 lib/home/data/repositories/home_repository.dart create mode 100644 lib/home/domain/repositories/home_repository_interface.dart create mode 100644 lib/home/domain/usecases/get_all_restaurants.dart create mode 100644 lib/home/failures/error_handler_failure.dart create mode 100644 lib/home/failures/failures.dart create mode 100644 lib/home/presentation/bloc/home_bloc.dart create mode 100644 lib/home/presentation/bloc/home_event.dart create mode 100644 lib/home/presentation/bloc/home_state.dart create mode 100644 lib/home/presentation/page/home.dart create mode 100644 lib/home/presentation/page/restaurant_details.dart create mode 100644 lib/home/presentation/tabs/all_restaurants_tab.dart create mode 100644 lib/home/presentation/tabs/my_favorities_tab.dart create mode 100644 lib/home/presentation/tabs/tab_item.dart create mode 100644 lib/home/presentation/widgets/header.dart create mode 100644 lib/home/presentation/widgets/loading.dart create mode 100644 lib/home/presentation/widgets/restaurant_card.dart create mode 100644 lib/navigator.dart create mode 100644 lib/routes.dart 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/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..85e90a1 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/home/domain/usecases/get_all_restaurants.dart'; +import 'package:restaurant_tour/routes.dart'; + +import 'home/data/datasources/home_data_source.dart'; +import 'home/data/repositories/home_repository.dart'; +import 'home/domain/repositories/home_repository_interface.dart'; +import 'home/presentation/bloc/home_bloc.dart'; + +/// The Widget that configures your application. +class MyApp extends StatelessWidget { + MyApp({ + super.key, + }); + + final _scaffoldMessengerKey = GlobalKey(); + + List get listRepositoryProvider => [ + RepositoryProvider( + create: (context) => HomeRepository( + HomeDataSource(), + ), + ), + ]; + + List get listBlocProvider => [ + BlocProvider( + create: (context) { + final repository = context.read(); + + return RestaurantsBloc( + GetAllRestaurants(repository), + )..add(GetAllRestaurantsEvent()); + }, + ), + ]; + + @override + Widget build(BuildContext context) { + return MultiRepositoryProvider( + providers: listRepositoryProvider, + child: MultiBlocProvider( + providers: listBlocProvider, + child: MaterialApp.router( + scaffoldMessengerKey: _scaffoldMessengerKey, + theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + routerConfig: Routes.router, + debugShowCheckedModeBanner: false, + ), + ), + ); + } +} diff --git a/lib/home/data/datasources/home_data_source.dart b/lib/home/data/datasources/home_data_source.dart new file mode 100644 index 0000000..946c909 --- /dev/null +++ b/lib/home/data/datasources/home_data_source.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; + +import '../../../models/restaurant.dart'; +import '../../../query.dart'; +import '../../failures/failures.dart'; +import 'package:http/http.dart' as http; + +abstract class HomeDataSourceInterface { + Future> getAllRestaurants(); +} + +class HomeDataSource implements HomeDataSourceInterface { + HomeDataSource(); + + final _apiKey = + 'yT1sJ3lRDxaU8jTfgmRuGnvd0Pj-OAU9KqV8VmQ9rK0L-M8sID9ZN0UmBfmPiMTFTHO11ziGqcb6gILaUj7zdkYUfZoUHshf7HsBUd2LEU5nIASGbkfYLaaUPZ_kZnYx'; + final _baseUrl = 'https://api.yelp.com/v3/graphql'; + + @override + Future> getAllRestaurants({ + 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 right( + RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ), + ); + } else { + return left( + MovieFailure( + message: 'Failed to load restaurants: ${response.statusCode}', + ), + ); + } + } catch (e) { + print('Error fetching restaurants: $e'); + return left( + MovieFailure( + message: 'Error fetching restaurants: $e', + ), + ); + } + } +} diff --git a/lib/home/data/repositories/home_repository.dart b/lib/home/data/repositories/home_repository.dart new file mode 100644 index 0000000..aa6fd98 --- /dev/null +++ b/lib/home/data/repositories/home_repository.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; + +import '../../../models/restaurant.dart'; +import '../../domain/repositories/home_repository_interface.dart'; +import '../datasources/home_data_source.dart'; + +class HomeRepository implements HomeRepositoryInterface { + final HomeDataSourceInterface dataSource; + + const HomeRepository(this.dataSource); + + @override + Future> + getAllRestaurants() => dataSource.getAllRestaurants(); +} diff --git a/lib/home/domain/repositories/home_repository_interface.dart b/lib/home/domain/repositories/home_repository_interface.dart new file mode 100644 index 0000000..1f682d4 --- /dev/null +++ b/lib/home/domain/repositories/home_repository_interface.dart @@ -0,0 +1,7 @@ +import 'package:dartz/dartz.dart'; +import '../../../models/restaurant.dart'; +import '../../failures/failures.dart'; + +abstract class HomeRepositoryInterface { + Future> getAllRestaurants(); +} diff --git a/lib/home/domain/usecases/get_all_restaurants.dart b/lib/home/domain/usecases/get_all_restaurants.dart new file mode 100644 index 0000000..87bcc64 --- /dev/null +++ b/lib/home/domain/usecases/get_all_restaurants.dart @@ -0,0 +1,13 @@ +import 'package:dartz/dartz.dart'; +import '../../../models/restaurant.dart'; +import '../../failures/failures.dart'; +import '../repositories/home_repository_interface.dart'; + +class GetAllRestaurants { + final HomeRepositoryInterface repository; + + const GetAllRestaurants(this.repository); + + Future> call() => + repository.getAllRestaurants(); +} diff --git a/lib/home/failures/error_handler_failure.dart b/lib/home/failures/error_handler_failure.dart new file mode 100644 index 0000000..c084f95 --- /dev/null +++ b/lib/home/failures/error_handler_failure.dart @@ -0,0 +1,36 @@ +enum FailureType { + info, + alert, + warning, + error, +} + +enum FailureDisplayType { + dialog, + toast, + toastBanner, +} + +class ErrorHandlerFailure { + final String? title; + final String message; + final FailureType type; + final FailureDisplayType displayType; + final void Function()? onPress; + + ErrorHandlerFailure({ + this.title, + required this.message, + required this.type, + required this.displayType, + this.onPress, + }); + + factory ErrorHandlerFailure.fromJson(Map json) => + ErrorHandlerFailure( + title: json['title'], + message: json['message'], + type: FailureType.values[json['type']], + displayType: FailureDisplayType.values[json['displayType']], + ); +} diff --git a/lib/home/failures/failures.dart b/lib/home/failures/failures.dart new file mode 100644 index 0000000..8a64f1c --- /dev/null +++ b/lib/home/failures/failures.dart @@ -0,0 +1,96 @@ +import 'error_handler_failure.dart'; + +abstract class RestaurantsFailure implements ErrorHandlerFailure {} + +class APIRestaurantsFailure extends RestaurantsFailure { + @override + final String message; + @override + final FailureType type; + @override + final FailureDisplayType displayType; + @override + final String? title; + @override + final void Function()? onPress; + + APIRestaurantsFailure({ + required this.message, + required this.type, + required this.displayType, + this.title, + this.onPress, + }) : super(); +} + +class GetCurrentLocationFailure extends RestaurantsFailure { + @override + final String message; + @override + FailureType get type => FailureType.error; + @override + FailureDisplayType get displayType => FailureDisplayType.toast; + @override + final String? title; + @override + final void Function()? onPress; + + GetCurrentLocationFailure({ + required this.message, + this.title, + this.onPress, + }) : super(); +} + +class MovieFailure extends RestaurantsFailure { + @override + final String message; + @override + FailureType get type => FailureType.error; + @override + FailureDisplayType get displayType => FailureDisplayType.dialog; + @override + final String? title; + @override + final void Function()? onPress; + + MovieFailure({ + required this.message, + this.title, + this.onPress, + }) : super(); +} + +class NetworkRestaurantsFailure extends RestaurantsFailure { + @override + FailureDisplayType get displayType => FailureDisplayType.toast; + @override + final String message; + @override + void Function()? get onPress => null; + @override + String? get title => null; + @override + FailureType get type => FailureType.warning; + + NetworkRestaurantsFailure({required this.message}) : super(); +} + +class SearchAddressesFailure extends RestaurantsFailure { + @override + final String message; + @override + FailureType get type => FailureType.error; + @override + FailureDisplayType get displayType => FailureDisplayType.toast; + @override + final String? title; + @override + final void Function()? onPress; + + SearchAddressesFailure({ + required this.message, + this.title, + this.onPress, + }) : super(); +} diff --git a/lib/home/presentation/bloc/home_bloc.dart b/lib/home/presentation/bloc/home_bloc.dart new file mode 100644 index 0000000..a25fc38 --- /dev/null +++ b/lib/home/presentation/bloc/home_bloc.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../models/restaurant.dart'; +import '../../domain/usecases/get_all_restaurants.dart'; +import '../../failures/failures.dart'; +part 'home_event.dart'; +part 'home_state.dart'; + +class RestaurantsBloc extends Bloc { + final GetAllRestaurants getAllRestaurants; + + RestaurantsBloc(this.getAllRestaurants) : super(RestaurantsInitial()) { + on(_getAllRestaurants); + } + + FutureOr _getAllRestaurants( + RestaurantsEvent event, + Emitter emit, + ) async { + emit(LoadingRestaurantsState()); + final result = await getAllRestaurants(); + result.fold( + (failure) => emit(ErrorRestaurantsState(failure)), + (movie) => emit(ResultRestaurantsState(movie)), + ); + } +} diff --git a/lib/home/presentation/bloc/home_event.dart b/lib/home/presentation/bloc/home_event.dart new file mode 100644 index 0000000..bfa93dc --- /dev/null +++ b/lib/home/presentation/bloc/home_event.dart @@ -0,0 +1,5 @@ +part of 'home_bloc.dart'; + +abstract class RestaurantsEvent {} + +class GetAllRestaurantsEvent extends RestaurantsEvent {} diff --git a/lib/home/presentation/bloc/home_state.dart b/lib/home/presentation/bloc/home_state.dart new file mode 100644 index 0000000..99d4dda --- /dev/null +++ b/lib/home/presentation/bloc/home_state.dart @@ -0,0 +1,18 @@ +part of 'home_bloc.dart'; + +abstract class RestaurantsState {} + +class RestaurantsInitial extends RestaurantsState {} + +class LoadingRestaurantsState extends RestaurantsState {} + +class ResultRestaurantsState extends RestaurantsState { + final RestaurantQueryResult listRestaurants; + ResultRestaurantsState(this.listRestaurants); +} + +class ErrorRestaurantsState extends RestaurantsState { + final RestaurantsFailure error; + + ErrorRestaurantsState(this.error); +} diff --git a/lib/home/presentation/page/home.dart b/lib/home/presentation/page/home.dart new file mode 100644 index 0000000..162b3d5 --- /dev/null +++ b/lib/home/presentation/page/home.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/home/presentation/widgets/loading.dart'; + +import '../bloc/home_bloc.dart'; +import '../tabs/all_restaurants_tab.dart'; +import '../tabs/my_favorities_tab.dart'; +import '../tabs/tab_item.dart'; +import '../widgets/header.dart'; + +class HomePage extends StatelessWidget { + const HomePage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const HeaderHome( + text: 'Restaurant Tour', + ), + centerTitle: false, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Container( + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 10), + child: const TabBar( + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: Colors.black, + tabs: [ + TabItem( + title: 'All Restaurants', + ), + TabItem( + title: 'My Favorities', + ), + ], + ), + ), + ), + ), + ), + body: BlocBuilder( + builder: (context, state) { + if (state is LoadingRestaurantsState) { + return const Loading(); + } else if (state is ResultRestaurantsState) { + return TabBarView( + children: [ + AllRestaurantsTab( + allRestaurants: state.listRestaurants.restaurants ?? [], + ), + const MyFavoritiesTab(), + ], + ); + } else if (state is ErrorRestaurantsState) { + return Center(child: Text(state.error.message)); + } else { + return const Loading(); + } + }, + ), + ), + ), + ); + } +} diff --git a/lib/home/presentation/page/restaurant_details.dart b/lib/home/presentation/page/restaurant_details.dart new file mode 100644 index 0000000..1c19564 --- /dev/null +++ b/lib/home/presentation/page/restaurant_details.dart @@ -0,0 +1,110 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating/flutter_rating.dart'; + +import '../widgets/header.dart'; + +class RestaurantDetails extends StatelessWidget { + const RestaurantDetails({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const HeaderHome( + text: 'Restaurant Name', + ), + centerTitle: false, + ), + body: SingleChildScrollView( + child: Column( + children: [ + CachedNetworkImage( + imageUrl: + 'https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg', + height: 360, + width: MediaQuery.of(context).size.width, + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + StarRating(rating: 3.5), + const Text('Open Now'), + ], + ), + const SizedBox( + height: 16, + ), + const Divider( + thickness: 1, + ), + const SizedBox( + height: 16, + ), + // const Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + const Text('Address'), + const SizedBox( + height: 16, + ), + const Text('102 Lakerside Ave'), + const Text('Seattle, WA 98122'), + const SizedBox( + height: 16, + ), + const Divider( + thickness: 1, + ), + const SizedBox( + height: 16, + ), + const Text('Overall Rating'), + const SizedBox( + height: 16, + ), + const Row( + children: [ + Text('4.6'), + Icon( + Icons.star, + ), + ], + ), + const SizedBox( + height: 16, + ), + const Divider( + thickness: 1, + ), + const SizedBox( + height: 16, + ), + const Text('42 reviews'), + const SizedBox( + height: 16, + ), + StarRating(rating: 3.5), + const Text('Open Now'), + const Row( + children: [ + CircleAvatar( + backgroundImage: NetworkImage( + 'https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg', + ), + radius: 24, + ), + Text('Helder'), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/all_restaurants_tab.dart b/lib/home/presentation/tabs/all_restaurants_tab.dart new file mode 100644 index 0000000..1745fdf --- /dev/null +++ b/lib/home/presentation/tabs/all_restaurants_tab.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/home/presentation/widgets/restaurant_card.dart'; + +import '../../../models/restaurant.dart'; + +class AllRestaurantsTab extends StatefulWidget { + final List allRestaurants; + const AllRestaurantsTab({ + super.key, + required this.allRestaurants, + }); + + @override + State createState() => _AllRestaurantsTabState(); +} + +DateTime selectedDate = DateTime.now(); + +class _AllRestaurantsTabState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.allRestaurants.length, + itemBuilder: (_, index) => RestaurantCard( + photo: widget.allRestaurants[index].photos?.first ?? '', + name: widget.allRestaurants[index].name!, + price: widget.allRestaurants[index].price!, + rating: widget.allRestaurants[index].rating!, + isOpenNow: widget.allRestaurants[index].isOpen, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/my_favorities_tab.dart b/lib/home/presentation/tabs/my_favorities_tab.dart new file mode 100644 index 0000000..fdfc411 --- /dev/null +++ b/lib/home/presentation/tabs/my_favorities_tab.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class MyFavoritiesTab extends StatefulWidget { + const MyFavoritiesTab({ + super.key, + }); + + @override + State createState() => _MyFavoritiesTabState(); +} + +DateTime selectedDate = DateTime.now(); + +class _MyFavoritiesTabState extends State { + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [], + ), + ), + ); + } +} diff --git a/lib/home/presentation/tabs/tab_item.dart b/lib/home/presentation/tabs/tab_item.dart new file mode 100644 index 0000000..2d1ed5b --- /dev/null +++ b/lib/home/presentation/tabs/tab_item.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class TabItem extends StatelessWidget { + final String title; + + const TabItem({ + super.key, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/lib/home/presentation/widgets/header.dart b/lib/home/presentation/widgets/header.dart new file mode 100644 index 0000000..aa75f03 --- /dev/null +++ b/lib/home/presentation/widgets/header.dart @@ -0,0 +1,35 @@ +// ignore_for_file: require_trailing_commas + +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/typography.dart'; + +class HeaderHome extends StatelessWidget { + final String text; + final bool isDetailsPage; + const HeaderHome({ + required this.text, + this.isDetailsPage = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(text, style: AppTextStyles.loraRegularHeadline), + ], + ), + ), + if (isDetailsPage) ...[ + const Icon( + Icons.favorite, + ), + ] + ], + ); + } +} diff --git a/lib/home/presentation/widgets/loading.dart b/lib/home/presentation/widgets/loading.dart new file mode 100644 index 0000000..42ce632 --- /dev/null +++ b/lib/home/presentation/widgets/loading.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class Loading extends StatelessWidget { + const Loading({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator( + color: Colors.black, + ), + ); + } +} diff --git a/lib/home/presentation/widgets/restaurant_card.dart b/lib/home/presentation/widgets/restaurant_card.dart new file mode 100644 index 0000000..1ea7ea4 --- /dev/null +++ b/lib/home/presentation/widgets/restaurant_card.dart @@ -0,0 +1,79 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_rating/flutter_rating.dart'; + +import '../../../navigator.dart'; +import '../../../typography.dart'; + +class RestaurantCard extends StatelessWidget { + final String photo; + final String name; + final String price; + final num rating; + final bool isOpenNow; + + const RestaurantCard({ + super.key, + required this.photo, + required this.name, + required this.price, + required this.rating, + required this.isOpenNow, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + color: Colors.white, + elevation: 1, + child: InkWell( + onTap: () => { + NavAdapter.goToNamed( + '/restaurant_details', + context, + ), + }, + child: Row( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: CachedNetworkImage( + imageUrl: photo, + height: 100, + width: 100, + fit: BoxFit.cover, + ), + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + name, + style: AppTextStyles.loraRegularHeadline, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + textDirection: TextDirection.ltr, + ), + Text(price), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + StarRating(rating: rating.toDouble()), + Text(isOpenNow ? 'Open Now' : 'Closed'), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index ae7012a..deace4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,14 +2,22 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:restaurant_tour/app.dart'; import 'package:restaurant_tour/models/restaurant.dart'; import 'package:restaurant_tour/query.dart'; -const _apiKey = ''; +const _apiKey = + 'nR4hTLTG9yrxOefBEzGgaEn7pZwmXsyigjye-VSHOed-JNqkKKdOVEmwjv6Z0J54PziaI6XVwDPt0rcgIbknCEiYbWFQW_vx4Hss6qGrg_HaQWxUiIJOYY4mtDbkZnYx'; const _baseUrl = 'https://api.yelp.com/v3/graphql'; void main() { - runApp(const RestaurantTour()); + WidgetsFlutterBinding.ensureInitialized(); + // runApp(const RestaurantTour()); + runApp( + MaterialApp( + home: MyApp(), + ), + ); } class RestaurantTour extends StatelessWidget { diff --git a/lib/navigator.dart b/lib/navigator.dart new file mode 100644 index 0000000..f6df6d9 --- /dev/null +++ b/lib/navigator.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +class NavAdapter { + static Future goToNamed( + String path, + BuildContext context, { + Object? data, + }) async { + data ??= {}; + context.push( + path, + extra: data, + ); + } + + static Future push( + Widget page, + BuildContext context, + ) async { + await Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (_, __, ___) => page, + transitionDuration: const Duration(milliseconds: 200), + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return SlideTransition( + position: Tween( + begin: const Offset(0.0, 1.0), + end: Offset.zero, + ).animate(animation), + child: child, + ); + }, + ), + ); + } + + static void pop(BuildContext context) { + context.pop(); + } + + static bool canPop(BuildContext context) { + return context.canPop(); + } +} diff --git a/lib/routes.dart b/lib/routes.dart new file mode 100644 index 0000000..be416de --- /dev/null +++ b/lib/routes.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:restaurant_tour/home/presentation/page/restaurant_details.dart'; + +import 'home/presentation/page/home.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); + +class Routes { + static final GoRouter router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomePage(); + }, + ), + GoRoute( + path: '/restaurant_details', + builder: (BuildContext context, GoRouterState state) { + return const RestaurantDetails(); + }, + ), + ], + ); +} diff --git a/pubspec.lock b/pubspec.lock index f95a63e..02757ea 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: @@ -105,6 +113,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -177,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -185,27 +233,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: name: file - sha256: 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 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_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -214,11 +286,24 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_rating: + dependency: "direct main" + description: + name: flutter_rating + sha256: "207bcd4a276585b8a0771a5ac03c0f3cdb27490e79a609f9a483d9794fe630b7" + url: "https://pub.dev" + source: hosted + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -231,10 +316,18 @@ packages: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + url: "https://pub.dev" + source: hosted + version: "14.2.7" graphs: dependency: transitive description: @@ -303,18 +396,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -351,18 +444,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -371,6 +464,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -387,6 +496,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.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: @@ -395,6 +568,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: @@ -411,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: @@ -456,6 +645,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + url: "https://pub.dev" + source: hosted + version: "2.5.4+2" stack_trace: dependency: transitive description: @@ -488,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: @@ -500,10 +721,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -520,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_math: dependency: transitive description: @@ -532,10 +761,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -560,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.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: @@ -569,5 +806,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..b2674eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,12 @@ dependencies: sdk: flutter http: ^1.2.2 json_annotation: ^4.9.0 + go_router: ^14.1.1 + cached_network_image: 3.4.1 + flutter_rating: ^2.0.2 + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 + dartz: ^0.10.1 dev_dependencies: flutter_test: From 7dbed7c24e9a5b090bd95098a754c9f7efa4fb5e Mon Sep 17 00:00:00 2001 From: Helder Date: Sat, 14 Sep 2024 15:26:12 -0300 Subject: [PATCH 2/7] add tests --- pubspec.lock | 144 ++++++++++++++++++-- pubspec.yaml | 8 ++ test/data/home_data_source_test.dart | 36 +++++ test/presentation/restaurant_card_test.dart | 39 ++++++ 4 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 test/data/home_data_source_test.dart create mode 100644 test/presentation/restaurant_card_test.dart diff --git a/pubspec.lock b/pubspec.lock index 02757ea..a4c43a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -10,7 +10,7 @@ packages: source: hosted version: "61.0.0" analyzer: - dependency: transitive + dependency: "direct dev" description: name: analyzer sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 @@ -41,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: @@ -193,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -217,6 +233,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + dio: + dependency: "direct dev" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" equatable: dependency: "direct main" description: @@ -305,13 +345,13 @@ packages: source: sdk version: "0.0.0" frontend_server_client: - dependency: transitive + dependency: "direct dev" 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: @@ -372,10 +412,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: @@ -464,6 +504,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -472,6 +528,14 @@ 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" octo_image: dependency: transitive description: @@ -577,13 +641,13 @@ packages: source: hosted version: "6.1.2" pub_semver: - dependency: transitive + dependency: "direct dev" 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: @@ -608,6 +672,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + 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: @@ -637,6 +717,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + 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: @@ -717,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" test_api: dependency: transitive description: @@ -725,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" timing: dependency: transitive description: @@ -766,13 +878,13 @@ packages: source: hosted version: "14.2.5" watcher: - dependency: transitive + dependency: "direct dev" 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: @@ -789,6 +901,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.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: diff --git a/pubspec.yaml b/pubspec.yaml index b2674eb..770a3fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,14 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 + watcher: ^1.1.0 + pub_semver: ^2.1.4 + mockito: ^5.4.4 + mocktail: ^1.0.4 + frontend_server_client: ^4.0.0 + analyzer: ^5.13.0 + bloc_test: ^9.1.7 + dio: ^5.7.0 flutter: generate: true diff --git a/test/data/home_data_source_test.dart b/test/data/home_data_source_test.dart new file mode 100644 index 0000000..75f0caf --- /dev/null +++ b/test/data/home_data_source_test.dart @@ -0,0 +1,36 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; +import 'package:restaurant_tour/models/restaurant.dart'; + +class MockDio extends Mock implements Dio {} + +class MockResponse extends Mock implements Response {} + +void main() { + final dio = MockDio(); + final mockResponse = MockResponse(); + late HomeDataSource datasource; + + setUpAll(() { + datasource = HomeDataSource(); + }); + + test("Should return a valid response when success", () async { + when(() => mockResponse.statusCode).thenReturn(200); + when(() => mockResponse.data).thenReturn({}); + when( + () => dio.post( + any(), + data: any(named: "data"), + ), + ).thenAnswer((_) async => mockResponse); + + final result = await datasource.getAllRestaurants(); + + expect(result, isA>()); + }); +} diff --git a/test/presentation/restaurant_card_test.dart b/test/presentation/restaurant_card_test.dart new file mode 100644 index 0000000..63d1d94 --- /dev/null +++ b/test/presentation/restaurant_card_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/home/presentation/widgets/restaurant_card.dart'; + +void main() { + group('RestaurantCard', () { + testWidgets('should display the restaurant details correctly', + (WidgetTester tester) async { + const name = 'Chefe Ramsy'; + const price = '\$\$\$ Italian'; + const rating = 4.4; + const isOpenNow = true; + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: RestaurantCard( + photo: '', + name: name, + price: price, + rating: rating, + isOpenNow: isOpenNow, + ), + ), + ), + ); + + expect(find.text(name), findsOneWidget); + expect(find.text(price), findsOneWidget); + expect(find.text('Open Now'), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => widget is Icon && widget.color == Colors.green, + ), + findsOneWidget, + ); + }); + }); +} From 3e74ccaa092f24581f947d44cf6c881fde5ef24e Mon Sep 17 00:00:00 2001 From: Helder Date: Mon, 16 Sep 2024 10:52:32 -0300 Subject: [PATCH 3/7] ajust layout for details page --- .fvm/fvm_config.json | 4 +- assets/images/user.png | Bin 0 -> 1820 bytes assets/json/graphql_response.json | 1241 +++++++++++++++++ .../data/datasources/home_data_source.dart | 17 +- lib/home/failures/error_handler_failure.dart | 36 - lib/home/failures/failures.dart | 92 +- lib/home/presentation/bloc/home_bloc.dart | 2 +- lib/home/presentation/page/home.dart | 38 +- .../presentation/page/restaurant_details.dart | 165 ++- .../tabs/all_restaurants_tab.dart | 5 +- lib/home/presentation/widgets/comment.dart | 76 + .../presentation/widgets/custom_divider.dart | 17 + lib/home/presentation/widgets/header.dart | 4 +- .../presentation/widgets/restaurant_card.dart | 77 +- lib/home/utils/utils.dart | 24 + lib/models/restaurant.g.dart | 1 + lib/routes.dart | 4 +- pubspec.lock | 2 +- pubspec.yaml | 6 + test/presentation/restaurant_card_test.dart | 2 + 20 files changed, 1580 insertions(+), 233 deletions(-) create mode 100644 assets/images/user.png create mode 100644 assets/json/graphql_response.json delete mode 100644 lib/home/failures/error_handler_failure.dart create mode 100644 lib/home/presentation/widgets/comment.dart create mode 100644 lib/home/presentation/widgets/custom_divider.dart create mode 100644 lib/home/utils/utils.dart diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..4d748a0 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.22.3", + "flutterSdkVersion": "3.24.3", "flavors": {} -} \ No newline at end of file +} diff --git a/assets/images/user.png b/assets/images/user.png new file mode 100644 index 0000000000000000000000000000000000000000..999abd3317707e21c24a7edd6ff080537fdf43ca GIT binary patch literal 1820 zcma)&XHe4#8pi)gNTCP>L5c|-LlYN_E}cX$1O!46gaEq(90=qD5)njYm9l~g!O%6- zB{@PwilG-p$|+K$Mw(P9p(uJQ3qi~>H#f6$Gxy=1PtUyXGrxD9S2Q7-2V@-y4g>%M z0>Iv2ZvYTY1GWGH27|+(2sj)rE{>3pl0PCPDJi8acT`3mt)ixiRzacEwT&?9nub~^ zlXnU$%DrO|1uiKr8hL;$Y=Aqa#7MBM-iIK+b?AmA^-5l|QiECvxx z0f@unhbr#)Yy000Y@WKe_lbvz0sw^XQ467YPwhbdCIG;s!8{G=eKrmNG@deV%UDr( zcK`tY?8FSpOY(3i7SA0BsOX6~q)FF@bZ%bGte&*&0060A>e~n!;tBc58q_J zc=RCvoC#Gc!msACh5%4PRSZQPKKMM+Jrn@>hZH75*yMTufX3J0pug0h?i?cjr*0p% zwUq@x5HJJ+l|qQY|D6JXA!1M%07uGc7$3#q-53Z(bxo~+Kx#s1?XbMU4<_f_J+oz_ zUM^^#re*#%vi+Z@fNX&8A#=)II3B<0N^zxr*i6u~Y6bY{CHp6I)d5sr8%wW6D)v!m zAz0mhYLw5b{=K7tSMj+|kvm@~)Q@u5oa+3THpi1OaCOEzVKPQ0WwZ{uQX&jzM8H2h z(Gln#G*%#?p?OZ+Fl$vnY6~c5#w79)$ja7A55Yr-b+ zVGfIplP~N>a!g-*A*a)7Hs4q{k8)cSzzJn8MQl(P-#G8b1CLW5708XI*W|Z9ov>{S z(%se?wwXha{cvQWp6i%wEOPLaWJ2ptV;nEkQ2v%&a&b4WS*tGXZ#|sWe{MaB_QF5x zzZ6H(Ycm%%rr+wy2r<2G_Bp8DvvJ@RvIK)u^=3sM7a!MSNgSkH(uPaPH%m>NSP?{-^xeWL*pYHGfL1tmO%At9rxxr8H}G~{n=|l0@)d(y3FehRpvJ`UUl|Ac=W{UHRr7X=<8V@`kC2s zc`UjhKK~=Fq{)vvb4q*jp=Mp#$h9fA-K#0Gd9Ex&s17bs1}tqPM0@C7J@R;5M_<_f zPLeUc>qwdn`+5IvJ}=VTQ$Vm=zgm_Jz{@zhTLzqrjrogQSAi_L(At)|ge_z@EewGk zJYV8oR`_&`K?nG)Sir4_sn3w#SU%Jmob;tYYrlp)AwQ>Ad5I=#AjH)=AFh zO5Rppl-KD_6u%;)o#e|H?@L}!E>*b_e#2C)ZeZG|{bIi`n2_W2i>*;> getAllRestaurants(); } @@ -41,16 +43,21 @@ class HomeDataSource implements HomeDataSourceInterface { ), ); } else { - return left( - MovieFailure( - message: 'Failed to load restaurants: ${response.statusCode}', + String resp = await Utils.readJson(); + print( + RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + ); + return right( + RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], ), ); } } catch (e) { - print('Error fetching restaurants: $e'); return left( - MovieFailure( + RestaurantFailure( message: 'Error fetching restaurants: $e', ), ); diff --git a/lib/home/failures/error_handler_failure.dart b/lib/home/failures/error_handler_failure.dart deleted file mode 100644 index c084f95..0000000 --- a/lib/home/failures/error_handler_failure.dart +++ /dev/null @@ -1,36 +0,0 @@ -enum FailureType { - info, - alert, - warning, - error, -} - -enum FailureDisplayType { - dialog, - toast, - toastBanner, -} - -class ErrorHandlerFailure { - final String? title; - final String message; - final FailureType type; - final FailureDisplayType displayType; - final void Function()? onPress; - - ErrorHandlerFailure({ - this.title, - required this.message, - required this.type, - required this.displayType, - this.onPress, - }); - - factory ErrorHandlerFailure.fromJson(Map json) => - ErrorHandlerFailure( - title: json['title'], - message: json['message'], - type: FailureType.values[json['type']], - displayType: FailureDisplayType.values[json['displayType']], - ); -} diff --git a/lib/home/failures/failures.dart b/lib/home/failures/failures.dart index 8a64f1c..e2ea742 100644 --- a/lib/home/failures/failures.dart +++ b/lib/home/failures/failures.dart @@ -1,96 +1,14 @@ -import 'error_handler_failure.dart'; - -abstract class RestaurantsFailure implements ErrorHandlerFailure {} - -class APIRestaurantsFailure extends RestaurantsFailure { - @override +abstract class RestaurantsFailure { final String message; - @override - final FailureType type; - @override - final FailureDisplayType displayType; - @override - final String? title; - @override - final void Function()? onPress; - APIRestaurantsFailure({ - required this.message, - required this.type, - required this.displayType, - this.title, - this.onPress, - }) : super(); + RestaurantsFailure({required this.message}); } -class GetCurrentLocationFailure extends RestaurantsFailure { +class RestaurantFailure extends RestaurantsFailure { @override final String message; - @override - FailureType get type => FailureType.error; - @override - FailureDisplayType get displayType => FailureDisplayType.toast; - @override - final String? title; - @override - final void Function()? onPress; - - GetCurrentLocationFailure({ - required this.message, - this.title, - this.onPress, - }) : super(); -} - -class MovieFailure extends RestaurantsFailure { - @override - final String message; - @override - FailureType get type => FailureType.error; - @override - FailureDisplayType get displayType => FailureDisplayType.dialog; - @override - final String? title; - @override - final void Function()? onPress; - - MovieFailure({ - required this.message, - this.title, - this.onPress, - }) : super(); -} - -class NetworkRestaurantsFailure extends RestaurantsFailure { - @override - FailureDisplayType get displayType => FailureDisplayType.toast; - @override - final String message; - @override - void Function()? get onPress => null; - @override - String? get title => null; - @override - FailureType get type => FailureType.warning; - - NetworkRestaurantsFailure({required this.message}) : super(); -} - -class SearchAddressesFailure extends RestaurantsFailure { - @override - final String message; - @override - FailureType get type => FailureType.error; - @override - FailureDisplayType get displayType => FailureDisplayType.toast; - @override - final String? title; - @override - final void Function()? onPress; - SearchAddressesFailure({ + RestaurantFailure({ required this.message, - this.title, - this.onPress, - }) : super(); + }) : super(message: message); } diff --git a/lib/home/presentation/bloc/home_bloc.dart b/lib/home/presentation/bloc/home_bloc.dart index a25fc38..25e9547 100644 --- a/lib/home/presentation/bloc/home_bloc.dart +++ b/lib/home/presentation/bloc/home_bloc.dart @@ -22,7 +22,7 @@ class RestaurantsBloc extends Bloc { final result = await getAllRestaurants(); result.fold( (failure) => emit(ErrorRestaurantsState(failure)), - (movie) => emit(ResultRestaurantsState(movie)), + (restaurantList) => emit(ResultRestaurantsState(restaurantList)), ); } } diff --git a/lib/home/presentation/page/home.dart b/lib/home/presentation/page/home.dart index 162b3d5..1b8af84 100644 --- a/lib/home/presentation/page/home.dart +++ b/lib/home/presentation/page/home.dart @@ -16,34 +16,36 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + color: Colors.white, home: DefaultTabController( length: 2, child: Scaffold( + backgroundColor: Colors.white, appBar: AppBar( + backgroundColor: Colors.white, title: const HeaderHome( text: 'Restaurant Tour', ), centerTitle: false, bottom: PreferredSize( preferredSize: const Size.fromHeight(50), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: Container( - height: 40, - margin: const EdgeInsets.symmetric(horizontal: 10), - child: const TabBar( - indicatorSize: TabBarIndicatorSize.tab, - dividerColor: Colors.transparent, - labelColor: Colors.black, - tabs: [ - TabItem( - title: 'All Restaurants', - ), - TabItem( - title: 'My Favorities', - ), - ], - ), + child: Container( + color: Colors.white, + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 10), + child: const TabBar( + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + labelColor: Colors.black, + indicatorColor: Colors.black, + tabs: [ + TabItem( + title: 'All Restaurants', + ), + TabItem( + title: 'My Favorities', + ), + ], ), ), ), diff --git a/lib/home/presentation/page/restaurant_details.dart b/lib/home/presentation/page/restaurant_details.dart index 1c19564..dc3db45 100644 --- a/lib/home/presentation/page/restaurant_details.dart +++ b/lib/home/presentation/page/restaurant_details.dart @@ -1,106 +1,159 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_rating/flutter_rating.dart'; +import 'package:restaurant_tour/home/presentation/widgets/comment.dart'; +import 'package:restaurant_tour/models/restaurant.dart'; +import '../../../typography.dart'; +import '../widgets/custom_divider.dart'; import '../widgets/header.dart'; class RestaurantDetails extends StatelessWidget { + final Restaurant restaurant; const RestaurantDetails({ + required this.restaurant, super.key, }); @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, appBar: AppBar( - title: const HeaderHome( - text: 'Restaurant Name', + backgroundColor: Colors.white, + shadowColor: Colors.white, + title: HeaderHome( + text: restaurant.name!, + isDetailsPage: true, ), centerTitle: false, ), body: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CachedNetworkImage( - imageUrl: - 'https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg', + imageUrl: restaurant.photos!.first, height: 360, width: MediaQuery.of(context).size.width, + fit: BoxFit.cover, ), const SizedBox( height: 16, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - StarRating(rating: 3.5), - const Text('Open Now'), - ], + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${restaurant.price} ${restaurant.categories?.first.title!}', + style: AppTextStyles.openRegularText, + ), + const Spacer(), + Text( + restaurant.isOpen ? 'Open Now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 8), + Icon( + Icons.circle, + color: restaurant.isOpen ? Colors.green : Colors.red, + size: 8, + ), + ], + ), ), + const CustomDivider(), const SizedBox( - height: 16, + height: 8, ), - const Divider( - thickness: 1, + const Padding( + padding: EdgeInsets.only(left: 16.0), + child: Text( + 'Address', + style: AppTextStyles.openRegularTitle, + ), ), const SizedBox( - height: 16, + height: 8, ), - // const Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - const Text('Address'), - const SizedBox( - height: 16, + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + restaurant.location!.formattedAddress!, + style: AppTextStyles.openRegularTitleSemiBold, + ), ), - const Text('102 Lakerside Ave'), - const Text('Seattle, WA 98122'), const SizedBox( - height: 16, - ), - const Divider( - thickness: 1, + height: 8, ), + const CustomDivider(), const SizedBox( - height: 16, + height: 8, ), - const Text('Overall Rating'), - const SizedBox( - height: 16, - ), - const Row( - children: [ - Text('4.6'), - Icon( - Icons.star, - ), - ], + const Padding( + padding: EdgeInsets.only(left: 16.0), + child: Text( + 'Overall Rating', + style: AppTextStyles.openRegularTitle, + ), ), const SizedBox( - height: 16, - ), - const Divider( - thickness: 1, + height: 8, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Row( + children: [ + Text( + restaurant.rating.toString(), + style: AppTextStyles.loraRegularHeadline.copyWith( + fontSize: 32, + ), + ), + const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ], + ), ), + const CustomDivider(), const SizedBox( - height: 16, + height: 8, + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + '${restaurant.reviews!.length} reviews', + style: AppTextStyles.openRegularTitle, + ), ), - const Text('42 reviews'), const SizedBox( height: 16, ), - StarRating(rating: 3.5), - const Text('Open Now'), - const Row( - children: [ - CircleAvatar( - backgroundImage: NetworkImage( - 'https://s3-media2.fl.yelpcdn.com/photo/rEWek1sYL0F35KZ0zRt3sw/o.jpg', - ), - radius: 24, + Padding( + padding: const EdgeInsets.only( + left: 16.0, + bottom: 16, + ), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: restaurant.reviews!.length, + itemBuilder: (_, index) => Comment( + photo: restaurant.reviews![index].user!.imageUrl ?? '', + userName: restaurant.reviews![index].user!.name ?? '', + text: restaurant.reviews![index].text ?? '', + rating: restaurant.reviews![index].rating ?? 4, ), - Text('Helder'), - ], + ), ), ], ), diff --git a/lib/home/presentation/tabs/all_restaurants_tab.dart b/lib/home/presentation/tabs/all_restaurants_tab.dart index 1745fdf..33e7fd5 100644 --- a/lib/home/presentation/tabs/all_restaurants_tab.dart +++ b/lib/home/presentation/tabs/all_restaurants_tab.dart @@ -32,9 +32,12 @@ class _AllRestaurantsTabState extends State { itemBuilder: (_, index) => RestaurantCard( photo: widget.allRestaurants[index].photos?.first ?? '', name: widget.allRestaurants[index].name!, - price: widget.allRestaurants[index].price!, + price: widget.allRestaurants[index].price ?? '', rating: widget.allRestaurants[index].rating!, isOpenNow: widget.allRestaurants[index].isOpen, + category: + widget.allRestaurants[index].categories?.first.title! ?? '', + data: widget.allRestaurants[index], ), ), ], diff --git a/lib/home/presentation/widgets/comment.dart b/lib/home/presentation/widgets/comment.dart new file mode 100644 index 0000000..94f4b16 --- /dev/null +++ b/lib/home/presentation/widgets/comment.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating/flutter_rating.dart'; +import 'package:restaurant_tour/typography.dart'; + +import '../../utils/utils.dart'; + +class Comment extends StatelessWidget { + final String photo; + final String userName; + final int rating; + final String text; + const Comment({ + super.key, + required this.photo, + required this.userName, + required this.rating, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + StarRating( + rating: rating.toDouble(), + color: Colors.amber, + size: 14, + ), + ], + ), + const SizedBox( + height: 8, + ), + Text( + text, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: AppTextStyles.openRegularTitle, + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + SizedBox( + height: 32, + width: 32, + child: CircleAvatar( + backgroundImage: Utils.getImageProvider(photo), + radius: 24, + ), + ), + const SizedBox( + width: 8, + ), + Text( + userName, + style: AppTextStyles.openRegularTitle, + ), + ], + ), + const Padding( + padding: EdgeInsets.only( + top: 8.0, + right: 16, + bottom: 8, + ), + child: Divider(), + ), + ], + ); + } +} diff --git a/lib/home/presentation/widgets/custom_divider.dart b/lib/home/presentation/widgets/custom_divider.dart new file mode 100644 index 0000000..dc7a577 --- /dev/null +++ b/lib/home/presentation/widgets/custom_divider.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class CustomDivider extends StatelessWidget { + const CustomDivider({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Divider( + thickness: 1, + ), + ); + } +} diff --git a/lib/home/presentation/widgets/header.dart b/lib/home/presentation/widgets/header.dart index aa75f03..48c2e84 100644 --- a/lib/home/presentation/widgets/header.dart +++ b/lib/home/presentation/widgets/header.dart @@ -1,5 +1,3 @@ -// ignore_for_file: require_trailing_commas - import 'package:flutter/material.dart'; import 'package:restaurant_tour/typography.dart'; @@ -28,7 +26,7 @@ class HeaderHome extends StatelessWidget { const Icon( Icons.favorite, ), - ] + ], ], ); } diff --git a/lib/home/presentation/widgets/restaurant_card.dart b/lib/home/presentation/widgets/restaurant_card.dart index 1ea7ea4..be15d3a 100644 --- a/lib/home/presentation/widgets/restaurant_card.dart +++ b/lib/home/presentation/widgets/restaurant_card.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating/flutter_rating.dart'; +import 'package:restaurant_tour/models/restaurant.dart'; import '../../../navigator.dart'; import '../../../typography.dart'; @@ -11,6 +12,8 @@ class RestaurantCard extends StatelessWidget { final String price; final num rating; final bool isOpenNow; + final String category; + final Restaurant? data; const RestaurantCard({ super.key, @@ -19,31 +22,39 @@ class RestaurantCard extends StatelessWidget { required this.price, required this.rating, required this.isOpenNow, + required this.category, + this.data, }); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - color: Colors.white, - elevation: 1, + return Card( + color: Colors.white, + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () => { NavAdapter.goToNamed( '/restaurant_details', context, + data: { + 'restaurant': data, + }, ), }, child: Row( children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(10)), - child: CachedNetworkImage( - imageUrl: photo, - height: 100, - width: 100, - fit: BoxFit.cover, + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: CachedNetworkImage( + imageUrl: photo, + height: 100, + width: 100, + fit: BoxFit.cover, + ), ), ), Flexible( @@ -51,20 +62,42 @@ class RestaurantCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ + Container( + constraints: const BoxConstraints(minHeight: 48), + child: Text( + name, + style: AppTextStyles.loraRegularHeadline, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), Text( - name, - style: AppTextStyles.loraRegularHeadline, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - textDirection: TextDirection.ltr, + '$price $category', + style: AppTextStyles.openRegularText, + ), + const SizedBox( + height: 4, ), - Text(price), Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, children: [ - StarRating(rating: rating.toDouble()), - Text(isOpenNow ? 'Open Now' : 'Closed'), + StarRating( + rating: rating.toDouble(), + size: 18, + color: Colors.amber, + ), + const Spacer(), + Text( + isOpenNow ? 'Open Now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 8), + Icon( + Icons.circle, + color: isOpenNow ? Colors.green : Colors.red, + size: 8, + ), ], ), ], diff --git a/lib/home/utils/utils.dart b/lib/home/utils/utils.dart new file mode 100644 index 0000000..9c2e70d --- /dev/null +++ b/lib/home/utils/utils.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class Utils { + static Future readJson() async { + String response = + await rootBundle.loadString('assets/json/graphql_response.json'); + return response; + } + + static ImageProvider? getImageProvider(String avatar) { + final defaultImage = Image.asset( + 'assets/images/user.png', + ).image; + + final avatarUrl = Uri.parse(avatar); + + if (!avatarUrl.hasAbsolutePath) { + return defaultImage; + } + + return avatar != '' ? NetworkImage(avatar) : defaultImage; + } +} diff --git a/lib/models/restaurant.g.dart b/lib/models/restaurant.g.dart index 3ed33f9..0693ae5 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/models/restaurant.g.dart @@ -39,6 +39,7 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, rating: json['rating'] as int?, + text: json['text'] as String?, user: json['user'] == null ? null : User.fromJson(json['user'] as Map), diff --git a/lib/routes.dart b/lib/routes.dart index be416de..1aee03e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -22,7 +22,9 @@ class Routes { GoRoute( path: '/restaurant_details', builder: (BuildContext context, GoRouterState state) { - return const RestaurantDetails(); + return RestaurantDetails( + restaurant: (state.extra as Map)['restaurant'] ?? [], + ); }, ), ], diff --git a/pubspec.lock b/pubspec.lock index a4c43a2..7abe556 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -561,7 +561,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct dev" description: name: path_provider sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 diff --git a/pubspec.yaml b/pubspec.yaml index 770a3fa..784c898 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dev_dependencies: analyzer: ^5.13.0 bloc_test: ^9.1.7 dio: ^5.7.0 + path_provider: ^2.0.2 flutter: generate: true @@ -59,4 +60,9 @@ flutter: style: italic - asset: assets/fonts/OpenSans/OpenSans-SemiBold.ttf weight: 600 + assets: + # Add assets from the images directory to the application. + - assets/json/ + - assets/images/ + - assets/ diff --git a/test/presentation/restaurant_card_test.dart b/test/presentation/restaurant_card_test.dart index 63d1d94..a9f6a75 100644 --- a/test/presentation/restaurant_card_test.dart +++ b/test/presentation/restaurant_card_test.dart @@ -10,6 +10,7 @@ void main() { const price = '\$\$\$ Italian'; const rating = 4.4; const isOpenNow = true; + const category = 'Italian'; await tester.pumpWidget( const MaterialApp( @@ -20,6 +21,7 @@ void main() { price: price, rating: rating, isOpenNow: isOpenNow, + category: category, ), ), ), From 0205523e42d5e1b12f53d3827799e5e392d1e44f Mon Sep 17 00:00:00 2001 From: Helder Date: Mon, 16 Sep 2024 14:50:19 -0300 Subject: [PATCH 4/7] favorite action --- .../data/datasources/home_data_source.dart | 54 ++++-- .../data/repositories/home_repository.dart | 4 +- .../home_repository_interface.dart | 5 +- .../domain/usecases/get_all_restaurants.dart | 6 +- lib/home/failures/app_exception.dart | 8 + lib/{ => home}/models/restaurant.dart | 0 lib/{ => home}/models/restaurant.g.dart | 0 lib/home/presentation/bloc/home_bloc.dart | 2 +- lib/home/presentation/bloc/home_state.dart | 2 +- lib/home/presentation/page/home.dart | 33 +++- .../presentation/page/restaurant_details.dart | 5 +- .../tabs/all_restaurants_tab.dart | 2 +- .../presentation/tabs/my_favorities_tab.dart | 34 +++- lib/home/presentation/widgets/header.dart | 63 +++++- .../presentation/widgets/restaurant_card.dart | 2 +- lib/home/services/module_communication.dart | 182 ++++++++++++++++++ .../module_communication_interface.dart | 26 +++ lib/home/utils/logger.dart | 7 + lib/home/utils/utils.dart | 47 +++++ lib/main.dart | 17 +- pubspec.lock | 10 +- pubspec.yaml | 5 +- test/data/home_data_source_test.dart | 2 +- 23 files changed, 474 insertions(+), 42 deletions(-) create mode 100644 lib/home/failures/app_exception.dart rename lib/{ => home}/models/restaurant.dart (100%) rename lib/{ => home}/models/restaurant.g.dart (100%) create mode 100644 lib/home/services/module_communication.dart create mode 100644 lib/home/services/module_communication_interface.dart create mode 100644 lib/home/utils/logger.dart diff --git a/lib/home/data/datasources/home_data_source.dart b/lib/home/data/datasources/home_data_source.dart index f859191..04a838d 100644 --- a/lib/home/data/datasources/home_data_source.dart +++ b/lib/home/data/datasources/home_data_source.dart @@ -2,15 +2,18 @@ import 'dart:convert'; import 'package:dartz/dartz.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; import '../../../query.dart'; import '../../failures/failures.dart'; import 'package:http/http.dart' as http; +import '../../services/module_communication.dart'; +import '../../services/module_communication_interface.dart'; import '../../utils/utils.dart'; abstract class HomeDataSourceInterface { - Future> getAllRestaurants(); + Future>> + getAllRestaurants(); } class HomeDataSource implements HomeDataSourceInterface { @@ -21,7 +24,8 @@ class HomeDataSource implements HomeDataSourceInterface { final _baseUrl = 'https://api.yelp.com/v3/graphql'; @override - Future> getAllRestaurants({ + Future>> + getAllRestaurants({ int offset = 0, }) async { final headers = { @@ -29,6 +33,9 @@ class HomeDataSource implements HomeDataSourceInterface { 'Content-Type': 'application/graphql', }; + ModuleCommunicationInterface communication = + ModuleCommunication.getInstance(); + try { final response = await http.post( Uri.parse(_baseUrl), @@ -37,22 +44,43 @@ class HomeDataSource implements HomeDataSourceInterface { ); if (response.statusCode == 200) { + RestaurantQueryResult restaurants = RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ); + List list = await Utils.favoritiesRestaurants( + communication, + restaurants.restaurants!, + ); return right( - RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ), + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(response.body)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: list, + ), + }, ); } else { String resp = await Utils.readJson(); - print( - RestaurantQueryResult.fromJson( - jsonDecode(resp)['data']['search'], - ), + RestaurantQueryResult restaurants = RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ); + List list = await Utils.favoritiesRestaurants( + communication, + restaurants.restaurants!, ); return right( - RestaurantQueryResult.fromJson( - jsonDecode(resp)['data']['search'], - ), + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: list, + ), + }, ); } } catch (e) { diff --git a/lib/home/data/repositories/home_repository.dart b/lib/home/data/repositories/home_repository.dart index aa6fd98..e4aa415 100644 --- a/lib/home/data/repositories/home_repository.dart +++ b/lib/home/data/repositories/home_repository.dart @@ -1,7 +1,7 @@ import 'package:dartz/dartz.dart'; import 'package:restaurant_tour/home/failures/failures.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; import '../../domain/repositories/home_repository_interface.dart'; import '../datasources/home_data_source.dart'; @@ -11,6 +11,6 @@ class HomeRepository implements HomeRepositoryInterface { const HomeRepository(this.dataSource); @override - Future> + Future>> getAllRestaurants() => dataSource.getAllRestaurants(); } diff --git a/lib/home/domain/repositories/home_repository_interface.dart b/lib/home/domain/repositories/home_repository_interface.dart index 1f682d4..e87e1dc 100644 --- a/lib/home/domain/repositories/home_repository_interface.dart +++ b/lib/home/domain/repositories/home_repository_interface.dart @@ -1,7 +1,8 @@ import 'package:dartz/dartz.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; import '../../failures/failures.dart'; abstract class HomeRepositoryInterface { - Future> getAllRestaurants(); + Future>> + getAllRestaurants(); } diff --git a/lib/home/domain/usecases/get_all_restaurants.dart b/lib/home/domain/usecases/get_all_restaurants.dart index 87bcc64..df4ff11 100644 --- a/lib/home/domain/usecases/get_all_restaurants.dart +++ b/lib/home/domain/usecases/get_all_restaurants.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; import '../../failures/failures.dart'; import '../repositories/home_repository_interface.dart'; @@ -8,6 +8,6 @@ class GetAllRestaurants { const GetAllRestaurants(this.repository); - Future> call() => - repository.getAllRestaurants(); + Future>> + call() => repository.getAllRestaurants(); } diff --git a/lib/home/failures/app_exception.dart b/lib/home/failures/app_exception.dart new file mode 100644 index 0000000..e19a296 --- /dev/null +++ b/lib/home/failures/app_exception.dart @@ -0,0 +1,8 @@ +class AppException implements Exception { + final String message; + + const AppException(this.message); + + @override + String toString() => message; +} diff --git a/lib/models/restaurant.dart b/lib/home/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/home/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/home/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/home/models/restaurant.g.dart diff --git a/lib/home/presentation/bloc/home_bloc.dart b/lib/home/presentation/bloc/home_bloc.dart index 25e9547..72b0857 100644 --- a/lib/home/presentation/bloc/home_bloc.dart +++ b/lib/home/presentation/bloc/home_bloc.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; import '../../domain/usecases/get_all_restaurants.dart'; import '../../failures/failures.dart'; part 'home_event.dart'; diff --git a/lib/home/presentation/bloc/home_state.dart b/lib/home/presentation/bloc/home_state.dart index 99d4dda..b4e2585 100644 --- a/lib/home/presentation/bloc/home_state.dart +++ b/lib/home/presentation/bloc/home_state.dart @@ -7,7 +7,7 @@ class RestaurantsInitial extends RestaurantsState {} class LoadingRestaurantsState extends RestaurantsState {} class ResultRestaurantsState extends RestaurantsState { - final RestaurantQueryResult listRestaurants; + final Map listRestaurants; ResultRestaurantsState(this.listRestaurants); } diff --git a/lib/home/presentation/page/home.dart b/lib/home/presentation/page/home.dart index 1b8af84..fd1f2d0 100644 --- a/lib/home/presentation/page/home.dart +++ b/lib/home/presentation/page/home.dart @@ -57,10 +57,37 @@ class HomePage extends StatelessWidget { } else if (state is ResultRestaurantsState) { return TabBarView( children: [ - AllRestaurantsTab( - allRestaurants: state.listRestaurants.restaurants ?? [], + RefreshIndicator( + onRefresh: () { + context + .read() + .add(GetAllRestaurantsEvent()); + return Future.delayed( + const Duration(seconds: 0), + ); + }, + child: AllRestaurantsTab( + allRestaurants: state.listRestaurants['allRestaurants'] + ?.restaurants ?? + [], + ), + ), + RefreshIndicator( + onRefresh: () { + context + .read() + .add(GetAllRestaurantsEvent()); + return Future.delayed( + const Duration(seconds: 0), + ); + }, + child: MyFavoritiesTab( + favoritiesRestaurants: state + .listRestaurants['favoritiesRestaurants'] + ?.restaurants ?? + [], + ), ), - const MyFavoritiesTab(), ], ); } else if (state is ErrorRestaurantsState) { diff --git a/lib/home/presentation/page/restaurant_details.dart b/lib/home/presentation/page/restaurant_details.dart index dc3db45..a3156ce 100644 --- a/lib/home/presentation/page/restaurant_details.dart +++ b/lib/home/presentation/page/restaurant_details.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:restaurant_tour/home/presentation/widgets/comment.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; import '../../../typography.dart'; import '../widgets/custom_divider.dart'; @@ -24,6 +24,7 @@ class RestaurantDetails extends StatelessWidget { title: HeaderHome( text: restaurant.name!, isDetailsPage: true, + restaurantId: restaurant.id!, ), centerTitle: false, ), @@ -131,7 +132,7 @@ class RestaurantDetails extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 16.0), child: Text( - '${restaurant.reviews!.length} reviews', + '${restaurant.reviews!.length} reviews', style: AppTextStyles.openRegularTitle, ), ), diff --git a/lib/home/presentation/tabs/all_restaurants_tab.dart b/lib/home/presentation/tabs/all_restaurants_tab.dart index 33e7fd5..78ce6fb 100644 --- a/lib/home/presentation/tabs/all_restaurants_tab.dart +++ b/lib/home/presentation/tabs/all_restaurants_tab.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/home/presentation/widgets/restaurant_card.dart'; -import '../../../models/restaurant.dart'; +import '../../models/restaurant.dart'; class AllRestaurantsTab extends StatefulWidget { final List allRestaurants; diff --git a/lib/home/presentation/tabs/my_favorities_tab.dart b/lib/home/presentation/tabs/my_favorities_tab.dart index fdfc411..6fb95cd 100644 --- a/lib/home/presentation/tabs/my_favorities_tab.dart +++ b/lib/home/presentation/tabs/my_favorities_tab.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; + +import '../widgets/restaurant_card.dart'; class MyFavoritiesTab extends StatefulWidget { + final List favoritiesRestaurants; const MyFavoritiesTab({ super.key, + required this.favoritiesRestaurants, }); @override @@ -14,11 +19,34 @@ DateTime selectedDate = DateTime.now(); class _MyFavoritiesTabState extends State { @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, - children: [], + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.favoritiesRestaurants.isEmpty + ? const Center(child: Text('No favorities found.')) + : ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: widget.favoritiesRestaurants.length, + itemBuilder: (_, index) => RestaurantCard( + photo: + widget.favoritiesRestaurants[index].photos?.first ?? + '', + name: widget.favoritiesRestaurants[index].name!, + price: widget.favoritiesRestaurants[index].price ?? '', + rating: widget.favoritiesRestaurants[index].rating!, + isOpenNow: widget.favoritiesRestaurants[index].isOpen, + category: widget.favoritiesRestaurants[index].categories + ?.first.title! ?? + '', + data: widget.favoritiesRestaurants[index], + ), + ), + ], ), ), ); diff --git a/lib/home/presentation/widgets/header.dart b/lib/home/presentation/widgets/header.dart index 48c2e84..f6b864f 100644 --- a/lib/home/presentation/widgets/header.dart +++ b/lib/home/presentation/widgets/header.dart @@ -1,15 +1,36 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/typography.dart'; -class HeaderHome extends StatelessWidget { +import '../../services/module_communication.dart'; +import '../../services/module_communication_interface.dart'; +import '../../utils/utils.dart'; + +class HeaderHome extends StatefulWidget { final String text; final bool isDetailsPage; + final String? restaurantId; const HeaderHome({ required this.text, this.isDetailsPage = false, + this.restaurantId, super.key, }); + @override + State createState() => _HeaderHomeState(); +} + +class _HeaderHomeState extends State { + ModuleCommunicationInterface communication = + ModuleCommunication.getInstance(); + bool isFavorite = false; + + @override + void initState() { + super.initState(); + checIconFavority(); + } + @override Widget build(BuildContext context) { return Row( @@ -18,16 +39,48 @@ class HeaderHome extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(text, style: AppTextStyles.loraRegularHeadline), + Text(widget.text, style: AppTextStyles.loraRegularHeadline), ], ), ), - if (isDetailsPage) ...[ - const Icon( - Icons.favorite, + if (widget.isDetailsPage) ...[ + IconButton( + icon: isFavorite + ? const Icon( + Icons.favorite, + ) + : const Icon( + Icons.favorite_border, + ), + onPressed: () { + setState(() { + if (!isFavorite) { + Utils.addFavorityRestaurant( + communication, + widget.restaurantId!, + ); + } else { + Utils.removeFavorityRestaurant( + communication, + widget.restaurantId!, + ); + } + isFavorite = !isFavorite; + }); + }, ), ], ], ); } + + Future checIconFavority() async { + isFavorite = await Utils.findFavorityRestaurant( + communication, + widget.restaurantId!, + ); + setState(() { + isFavorite = isFavorite; + }); + } } diff --git a/lib/home/presentation/widgets/restaurant_card.dart b/lib/home/presentation/widgets/restaurant_card.dart index be15d3a..7bd1187 100644 --- a/lib/home/presentation/widgets/restaurant_card.dart +++ b/lib/home/presentation/widgets/restaurant_card.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating/flutter_rating.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; import '../../../navigator.dart'; import '../../../typography.dart'; diff --git a/lib/home/services/module_communication.dart b/lib/home/services/module_communication.dart new file mode 100644 index 0000000..b7a5e97 --- /dev/null +++ b/lib/home/services/module_communication.dart @@ -0,0 +1,182 @@ +import 'dart:io'; + +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import '../failures/app_exception.dart'; +import 'module_communication_interface.dart'; +import '../utils/logger.dart'; + +class ModuleCommunication implements ModuleCommunicationInterface { + static bool hasNoInternetToast = true; + static late LazyBox _collectionInstance; + + static ModuleCommunication? singleInstance; + static ModuleCommunication getInstance() { + singleInstance ??= ModuleCommunication(); + return singleInstance!; + } + + ModuleCommunication(); + + static String step = '0'; + static String errorBeforeClean = ''; + static bool recovered = false; + + static String hardwareId = ''; + + static Future init() async { + final appDocumentDirectory = await getApplicationDocumentsDirectory(); + Hive.init(appDocumentDirectory.path); + try { + _collectionInstance = await Hive.openLazyBox('superformula_app'); + } catch (error) { + recovered = true; + errorBeforeClean = error.toString(); + Hive.deleteBoxFromDisk('superformula_app'); + _collectionInstance = await Hive.openLazyBox('superformula_app'); + } + } + + @override + Future put(KeyStorage key, List value) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.put(key.name, value); + } + } + + @override + Future?> get( + KeyStorage key, { + dynamic defaultValue, + }) async { + if (_collectionInstance.isOpen) { + final List? result = await _collectionInstance.get(key.name); + return result ?? []; + } + return []; + } + + @override + Future delete(KeyStorage key) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.delete(key.name); + } + } + + @override + Future close() async { + if (_collectionInstance.isOpen) { + await _collectionInstance.close(); + } + } + + @override + Stream stream({KeyStorage? key}) => _collectionInstance.watch(key: key); + + @override + Future save(String key, dynamic value) async { + if (_collectionInstance.isOpen) { + await _collectionInstance.put(key, value); + } + } + + @override + Future getBool(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return value; + } + return value as bool; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Future getString(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return null; + } + return value as String; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Future getNum(String key) { + if (_collectionInstance.isOpen) { + return _collectionInstance.get(key).then((value) { + if (value == null) { + return null; + } + return value as num; + }); + } + return Future.error(const AppException('Key not found')); + } + + @override + Stream observe(String key) { + return _collectionInstance.watch(key: key).map((event) => event.value as T); + } + + @override + Future setItem(String key, String value) { + return save(key, value); + } + + static Future get _localPath async { + final directory = await getTemporaryDirectory(); + return directory.path; + } + + static Future hasFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + return file.exists(); + } + + static Future createFile(String filename, String text) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + await file.writeAsString(text); + } + + static Future readFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + return file.readAsString(); + } + + static Future deleteFile(String filename) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + if (file.existsSync()) await file.delete(); + } + + static Future renameFile(String filename, String filenameNew) async { + final String localPath = await _localPath; + final File file = File('$localPath/$filename'); + await file.rename('$localPath/$filenameNew'); + } + + @override + Future> getMap(String key, {defaultValue}) async { + try { + if (_collectionInstance.isOpen) { + final value = + await _collectionInstance.get(key, defaultValue: defaultValue); + if (value == null) return {}; + return Map.from(value); + } + } catch (e, s) { + Logger.log(e.toString()); + Logger.log(s.toString()); + } + throw const AppException('Key not found'); + } +} diff --git a/lib/home/services/module_communication_interface.dart b/lib/home/services/module_communication_interface.dart new file mode 100644 index 0000000..4f64b5c --- /dev/null +++ b/lib/home/services/module_communication_interface.dart @@ -0,0 +1,26 @@ +enum KeyStorage { favorities } + +extension KeyStorageName on KeyStorage { + String get name => toString().split('.')[1]; +} + +abstract class ModuleCommunicationInterface { + Future put(KeyStorage key, List value); + + Stream stream({KeyStorage? key}); + Stream observe(String key); + + Future?> get(KeyStorage key); + Future> getMap(String key, {dynamic defaultValue}); + + Future delete(KeyStorage key); + + Future close(); + + Future save(String element, dynamic value); + Future getString(String key); + Future getBool(String key); + Future getNum(String key); + + Future setItem(String key, String value); +} diff --git a/lib/home/utils/logger.dart b/lib/home/utils/logger.dart new file mode 100644 index 0000000..bccdfbe --- /dev/null +++ b/lib/home/utils/logger.dart @@ -0,0 +1,7 @@ +import 'package:flutter/foundation.dart'; + +class Logger { + static void log(String message) { + debugPrint(message); + } +} diff --git a/lib/home/utils/utils.dart b/lib/home/utils/utils.dart index 9c2e70d..378d491 100644 --- a/lib/home/utils/utils.dart +++ b/lib/home/utils/utils.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../models/restaurant.dart'; +import '../services/module_communication_interface.dart'; + class Utils { static Future readJson() async { String response = @@ -21,4 +24,48 @@ class Utils { return avatar != '' ? NetworkImage(avatar) : defaultImage; } + + static getFavoritiesRestaurants( + ModuleCommunicationInterface communication, + ) async { + return communication.get(KeyStorage.favorities); + } + + static addFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + favorities.add(id); + await communication.put(KeyStorage.favorities, favorities); + } + + static removeFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + favorities.removeWhere((item) => item == id); + await communication.put(KeyStorage.favorities, favorities); + } + + static Future findFavorityRestaurant( + ModuleCommunicationInterface communication, + String id, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + return favorities.where((item) => item == id).isNotEmpty; + } + + static Future> favoritiesRestaurants( + ModuleCommunicationInterface communication, + List restaurants, + ) async { + List favorities = await getFavoritiesRestaurants(communication); + List filterList = restaurants + .toSet() + .where((element) => favorities.toSet().contains(element.id)) + .toList(); + return filterList; + } } diff --git a/lib/main.dart b/lib/main.dart index deace4c..d86a9d1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,16 +3,19 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:restaurant_tour/app.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; import 'package:restaurant_tour/query.dart'; +import 'home/services/module_communication.dart'; + const _apiKey = 'nR4hTLTG9yrxOefBEzGgaEn7pZwmXsyigjye-VSHOed-JNqkKKdOVEmwjv6Z0J54PziaI6XVwDPt0rcgIbknCEiYbWFQW_vx4Hss6qGrg_HaQWxUiIJOYY4mtDbkZnYx'; const _baseUrl = 'https://api.yelp.com/v3/graphql'; -void main() { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); // runApp(const RestaurantTour()); + await moduleCommunicationInit(); runApp( MaterialApp( home: MyApp(), @@ -32,6 +35,16 @@ class RestaurantTour extends StatelessWidget { } } +Future moduleCommunicationInit() async { + bool storageError = false; + try { + await ModuleCommunication.init(); + } catch (error) { + storageError = true; + } + return storageError; +} + // TODO: Architect code // This is just a POC of the API integration class HomePage extends StatelessWidget { diff --git a/pubspec.lock b/pubspec.lock index 7abe556..be12a94 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -376,6 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http: dependency: "direct main" description: @@ -561,7 +569,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: "direct dev" + dependency: "direct main" description: name: path_provider sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 diff --git a/pubspec.yaml b/pubspec.yaml index 784c898..ffad71d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: equatable: ^2.0.5 flutter_bloc: ^8.1.5 dartz: ^0.10.1 + hive: ^2.2.3 + path_provider: ^2.0.2 dev_dependencies: flutter_test: @@ -36,7 +38,8 @@ dev_dependencies: analyzer: ^5.13.0 bloc_test: ^9.1.7 dio: ^5.7.0 - path_provider: ^2.0.2 + + flutter: generate: true diff --git a/test/data/home_data_source_test.dart b/test/data/home_data_source_test.dart index 75f0caf..80cf7e1 100644 --- a/test/data/home_data_source_test.dart +++ b/test/data/home_data_source_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/home/data/datasources/home_data_source.dart'; import 'package:restaurant_tour/home/failures/failures.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; class MockDio extends Mock implements Dio {} From ec8776bef8f2af028d98c99601a078e02f89978e Mon Sep 17 00:00:00 2001 From: Helder Date: Mon, 16 Sep 2024 15:02:38 -0300 Subject: [PATCH 5/7] env for datasource --- lib/env.dart | 13 +++++++++++++ lib/env.g.dart | 13 +++++++++++++ pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 4 files changed, 35 insertions(+) create mode 100644 lib/env.dart create mode 100644 lib/env.g.dart diff --git a/lib/env.dart b/lib/env.dart new file mode 100644 index 0000000..cc24f65 --- /dev/null +++ b/lib/env.dart @@ -0,0 +1,13 @@ +// ignore_for_file: non_constant_identifier_names, constant_identifier_names + +import 'package:envied/envied.dart'; + +part 'env.g.dart'; + +@Envied(path: '.env') +abstract class Env { + @EnviedField() + static const API_KEY = _Env.apiKey; + @EnviedField() + static const BASE_URL = _Env.baseUrl; +} diff --git a/lib/env.g.dart b/lib/env.g.dart new file mode 100644 index 0000000..b25346e --- /dev/null +++ b/lib/env.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'env.dart'; + +// ************************************************************************** +// EnviedGenerator +// ************************************************************************** + +class _Env { + static const apiKey = + 'yT1sJ3lRDxaU8jTfgmRuGnvd0Pj-OAU9KqV8VmQ9rK0L-M8sID9ZN0UmBfmPiMTFTHO11ziGqcb6gILaUj7zdkYUfZoUHshf7HsBUd2LEU5nIASGbkfYLaaUPZ_kZnYx'; + static const baseUrl = 'https://api.yelp.com/v3/graphql'; +} diff --git a/pubspec.lock b/pubspec.lock index be12a94..d65d934 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -257,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + envied: + dependency: "direct main" + description: + name: envied + sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450 + url: "https://pub.dev" + source: hosted + version: "0.5.4+1" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ffad71d..fa36e44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: dartz: ^0.10.1 hive: ^2.2.3 path_provider: ^2.0.2 + envied: ^0.5.4+1 dev_dependencies: flutter_test: From c9823c77ff050ed816e7f0a440e843f9310c55bb Mon Sep 17 00:00:00 2001 From: Helder Date: Mon, 16 Sep 2024 15:02:58 -0300 Subject: [PATCH 6/7] some layout ajusts in restaurant details and project structure --- lib/home/data/datasources/home_data_source.dart | 6 +++--- lib/home/presentation/page/home.dart | 1 + lib/home/presentation/page/restaurant_details.dart | 14 +++++++++----- lib/home/presentation/widgets/comment.dart | 2 +- lib/home/presentation/widgets/header.dart | 2 +- lib/home/presentation/widgets/restaurant_card.dart | 2 +- lib/{ => themes}/typography.dart | 0 7 files changed, 16 insertions(+), 11 deletions(-) rename lib/{ => themes}/typography.dart (100%) diff --git a/lib/home/data/datasources/home_data_source.dart b/lib/home/data/datasources/home_data_source.dart index 04a838d..f3bfe3b 100644 --- a/lib/home/data/datasources/home_data_source.dart +++ b/lib/home/data/datasources/home_data_source.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:dartz/dartz.dart'; +import '../../../env.dart'; import '../../models/restaurant.dart'; import '../../../query.dart'; import '../../failures/failures.dart'; @@ -19,9 +20,8 @@ abstract class HomeDataSourceInterface { class HomeDataSource implements HomeDataSourceInterface { HomeDataSource(); - final _apiKey = - 'yT1sJ3lRDxaU8jTfgmRuGnvd0Pj-OAU9KqV8VmQ9rK0L-M8sID9ZN0UmBfmPiMTFTHO11ziGqcb6gILaUj7zdkYUfZoUHshf7HsBUd2LEU5nIASGbkfYLaaUPZ_kZnYx'; - final _baseUrl = 'https://api.yelp.com/v3/graphql'; + final _apiKey = Env.API_KEY; + final _baseUrl = Env.BASE_URL; @override Future>> diff --git a/lib/home/presentation/page/home.dart b/lib/home/presentation/page/home.dart index fd1f2d0..0ec5f5f 100644 --- a/lib/home/presentation/page/home.dart +++ b/lib/home/presentation/page/home.dart @@ -34,6 +34,7 @@ class HomePage extends StatelessWidget { height: 40, margin: const EdgeInsets.symmetric(horizontal: 10), child: const TabBar( + tabAlignment: TabAlignment.center, indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: Colors.black, diff --git a/lib/home/presentation/page/restaurant_details.dart b/lib/home/presentation/page/restaurant_details.dart index a3156ce..68ae3c3 100644 --- a/lib/home/presentation/page/restaurant_details.dart +++ b/lib/home/presentation/page/restaurant_details.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/home/presentation/widgets/comment.dart'; import 'package:restaurant_tour/home/models/restaurant.dart'; -import '../../../typography.dart'; +import '../../../themes/typography.dart'; import '../widgets/custom_divider.dart'; import '../widgets/header.dart'; @@ -19,6 +19,7 @@ class RestaurantDetails extends StatelessWidget { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( + surfaceTintColor: Colors.transparent, backgroundColor: Colors.white, shadowColor: Colors.white, title: HeaderHome( @@ -117,10 +118,13 @@ class RestaurantDetails extends StatelessWidget { fontSize: 32, ), ), - const Icon( - Icons.star, - color: Colors.amber, - size: 16, + Container( + margin: const EdgeInsets.only(top: 12), + child: const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), ), ], ), diff --git a/lib/home/presentation/widgets/comment.dart b/lib/home/presentation/widgets/comment.dart index 94f4b16..e3227f8 100644 --- a/lib/home/presentation/widgets/comment.dart +++ b/lib/home/presentation/widgets/comment.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_rating/flutter_rating.dart'; -import 'package:restaurant_tour/typography.dart'; +import 'package:restaurant_tour/themes/typography.dart'; import '../../utils/utils.dart'; diff --git a/lib/home/presentation/widgets/header.dart b/lib/home/presentation/widgets/header.dart index f6b864f..a3bcf59 100644 --- a/lib/home/presentation/widgets/header.dart +++ b/lib/home/presentation/widgets/header.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/typography.dart'; +import 'package:restaurant_tour/themes/typography.dart'; import '../../services/module_communication.dart'; import '../../services/module_communication_interface.dart'; diff --git a/lib/home/presentation/widgets/restaurant_card.dart b/lib/home/presentation/widgets/restaurant_card.dart index 7bd1187..b10daf2 100644 --- a/lib/home/presentation/widgets/restaurant_card.dart +++ b/lib/home/presentation/widgets/restaurant_card.dart @@ -4,7 +4,7 @@ import 'package:flutter_rating/flutter_rating.dart'; import 'package:restaurant_tour/home/models/restaurant.dart'; import '../../../navigator.dart'; -import '../../../typography.dart'; +import '../../../themes/typography.dart'; class RestaurantCard extends StatelessWidget { final String photo; diff --git a/lib/typography.dart b/lib/themes/typography.dart similarity index 100% rename from lib/typography.dart rename to lib/themes/typography.dart From 1ea334a67f11eb214402228fd277f5608398bda6 Mon Sep 17 00:00:00 2001 From: Helder Date: Mon, 16 Sep 2024 16:06:19 -0300 Subject: [PATCH 7/7] add more test --- lib/home/models/restaurant.g.dart | 7 +- test/data/home_data_source_test.dart | 5 +- test/domain/domain_test.dart | 61 ++++++++++++++ test/domain/domain_test.mocks.dart | 91 +++++++++++++++++++++ test/presentation/restaurant_card_test.dart | 3 +- 5 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 test/domain/domain_test.dart create mode 100644 test/domain/domain_test.mocks.dart diff --git a/lib/home/models/restaurant.g.dart b/lib/home/models/restaurant.g.dart index 0693ae5..dea6677 100644 --- a/lib/home/models/restaurant.g.dart +++ b/lib/home/models/restaurant.g.dart @@ -38,16 +38,17 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, - rating: json['rating'] as int?, - text: json['text'] as String?, + rating: (json['rating'] as num?)?.toInt(), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, + 'text': instance.text, 'user': instance.user, }; @@ -96,7 +97,7 @@ Map _$RestaurantToJson(Restaurant instance) => RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( - total: json['total'] as int?, + total: (json['total'] as num?)?.toInt(), restaurants: (json['business'] as List?) ?.map((e) => Restaurant.fromJson(e as Map)) .toList(), diff --git a/test/data/home_data_source_test.dart b/test/data/home_data_source_test.dart index 80cf7e1..f0549ec 100644 --- a/test/data/home_data_source_test.dart +++ b/test/data/home_data_source_test.dart @@ -31,6 +31,9 @@ void main() { final result = await datasource.getAllRestaurants(); - expect(result, isA>()); + expect( + result, + isA>>(), + ); }); } diff --git a/test/domain/domain_test.dart b/test/domain/domain_test.dart new file mode 100644 index 0000000..d770965 --- /dev/null +++ b/test/domain/domain_test.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart'; +import 'package:restaurant_tour/home/data/repositories/home_repository.dart'; +import 'package:restaurant_tour/home/failures/failures.dart'; +import 'package:restaurant_tour/home/models/restaurant.dart'; +import 'package:restaurant_tour/home/utils/utils.dart'; + +class MockHomeRepository extends Mock implements HomeRepository {} + +@GenerateMocks([MockHomeRepository]) +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + late MockHomeRepository mockRepo; + late Map expected; + late String resp; + late RestaurantQueryResult restaurants; + + setUp(() async { + mockRepo = MockHomeRepository(); + resp = await Utils.readJson(); + restaurants = RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ); + + expected = { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: [], + ), + }; + }); + + test('getAllRestaurants should return data', () async { + when(mockRepo.getAllRestaurants()).thenAnswer( + (_) async => right( + { + 'allRestaurants': RestaurantQueryResult.fromJson( + jsonDecode(resp)['data']['search'], + ), + 'favoritiesRestaurants': RestaurantQueryResult( + total: restaurants.total, + restaurants: [], + ), + }, + ), + ); + final HomeDataSource articleRepo = HomeDataSource(); + final Either> + response = await articleRepo.getAllRestaurants(); + expect(expected, response); + }); +} diff --git a/test/domain/domain_test.mocks.dart b/test/domain/domain_test.mocks.dart new file mode 100644 index 0000000..e5be56d --- /dev/null +++ b/test/domain/domain_test.mocks.dart @@ -0,0 +1,91 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in restaurant_tour/test/domain/domain_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:dartz/dartz.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:restaurant_tour/home/data/datasources/home_data_source.dart' + as _i2; +import 'package:restaurant_tour/home/failures/failures.dart' as _i6; +import 'package:restaurant_tour/home/models/restaurant.dart' as _i7; + +import 'domain_test.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeHomeDataSourceInterface_0 extends _i1.SmartFake + implements _i2.HomeDataSourceInterface { + _FakeHomeDataSourceInterface_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeEither_1 extends _i1.SmartFake implements _i3.Either { + _FakeEither_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [MockHomeRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMockHomeRepository extends _i1.Mock + implements _i4.MockHomeRepository { + MockMockHomeRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.HomeDataSourceInterface get dataSource => (super.noSuchMethod( + Invocation.getter(#dataSource), + returnValue: _FakeHomeDataSourceInterface_0( + this, + Invocation.getter(#dataSource), + ), + ) as _i2.HomeDataSourceInterface); + + @override + _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>> getAllRestaurants() => + (super.noSuchMethod( + Invocation.method( + #getAllRestaurants, + [], + ), + returnValue: _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>>.value(_FakeEither_1< + _i6.RestaurantsFailure, Map>( + this, + Invocation.method( + #getAllRestaurants, + [], + ), + )), + ) as _i5.Future< + _i3.Either<_i6.RestaurantsFailure, + Map>>); +} diff --git a/test/presentation/restaurant_card_test.dart b/test/presentation/restaurant_card_test.dart index a9f6a75..1574ce0 100644 --- a/test/presentation/restaurant_card_test.dart +++ b/test/presentation/restaurant_card_test.dart @@ -7,7 +7,7 @@ void main() { testWidgets('should display the restaurant details correctly', (WidgetTester tester) async { const name = 'Chefe Ramsy'; - const price = '\$\$\$ Italian'; + const price = '\$\$'; const rating = 4.4; const isOpenNow = true; const category = 'Italian'; @@ -28,7 +28,6 @@ void main() { ); expect(find.text(name), findsOneWidget); - expect(find.text(price), findsOneWidget); expect(find.text('Open Now'), findsOneWidget); expect( find.byWidgetPredicate(