From d6c74c592dbacfb884306b16a2a78f6899ac6ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Thu, 12 Sep 2024 21:20:07 -0300 Subject: [PATCH 01/14] chore: fvm dependencies --- .fvm/fvm_config.json | 3 +-- .fvmrc | 4 ++++ .gitignore | 4 +++- .vscode/settings.json | 14 +++++++------- 4 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 .fvmrc diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..e7b55eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.22.3", - "flavors": {} + "flutterSdkVersion": "3.22.3" } \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..e03e940 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.22.3", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d87..7040cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..c959187 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/versions/3.22.3", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file From cc6cd0318b8c4b67ec4e12f40451442fd2b0c168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Thu, 12 Sep 2024 22:00:24 -0300 Subject: [PATCH 02/14] feat: add architecture structure --- .vscode/launch.json | 3 +- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 +++++ ios/Podfile.lock | 23 +++ ios/Runner.xcodeproj/project.pbxproj | 112 ++++++++++++ .../contents.xcworkspacedata | 3 + lib/core/dependency_injection.dart | 44 +++++ lib/core/utils/storage.dart | 28 +++ lib/{ => core/utils}/typography.dart | 0 lib/{ => data}/models/restaurant.dart | 0 lib/{ => data}/models/restaurant.g.dart | 0 .../repositories/yelp_repository.dart | 5 +- .../yelp_repository_contract.dart | 5 + lib/main.dart | 39 +---- lib/presentation/pages/home_page.dart | 37 ++++ pubspec.lock | 159 +++++++++++++++++- pubspec.yaml | 3 + 18 files changed, 466 insertions(+), 41 deletions(-) create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock create mode 100644 lib/core/dependency_injection.dart create mode 100644 lib/core/utils/storage.dart rename lib/{ => core/utils}/typography.dart (100%) rename lib/{ => data}/models/restaurant.dart (100%) rename lib/{ => data}/models/restaurant.g.dart (100%) rename lib/{ => data}/repositories/yelp_repository.dart (92%) create mode 100644 lib/domain/repositories/yelp_repository_contract.dart create mode 100644 lib/presentation/pages/home_page.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 5d0f1d3..467c4b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,8 @@ { "name": "app", "request": "launch", - "type": "dart" + "type": "dart", + } ] } \ No newline at end of file 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/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..c043b1f --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 182fb57..2d9d9dc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,9 +11,11 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 85C79BC1379B6ECA5B2234C9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC906F28FAE3489FBC83D026 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A48A302D3CCE91394B634008 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85DBD5B0E4B9E5C51A994A79 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,14 +42,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0E7B48D057702E4D32F05D9B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 23739D936EBBE89AB494C04E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 85DBD5B0E4B9E5C51A994A79 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,6 +60,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B0A5FF3660D3DF525F426F09 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B2BEB6E9D5A7F3529423E7F9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B62A153744400B774863F73A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + CE267DD5A9DAF58AE378E4CC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FC906F28FAE3489FBC83D026 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,6 +72,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 85C79BC1379B6ECA5B2234C9 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EDDB347322BEACC8DB1684CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A48A302D3CCE91394B634008 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,15 @@ path = RunnerTests; sourceTree = ""; }; + 75C37D9A5D647899566C804C /* Frameworks */ = { + isa = PBXGroup; + children = ( + FC906F28FAE3489FBC83D026 /* Pods_Runner.framework */, + 85DBD5B0E4B9E5C51A994A79 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +122,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 9F46D7A5476D63249D5C7EC7 /* Pods */, + 75C37D9A5D647899566C804C /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +151,20 @@ path = Runner; sourceTree = ""; }; + 9F46D7A5476D63249D5C7EC7 /* Pods */ = { + isa = PBXGroup; + children = ( + B62A153744400B774863F73A /* Pods-Runner.debug.xcconfig */, + B2BEB6E9D5A7F3529423E7F9 /* Pods-Runner.release.xcconfig */, + CE267DD5A9DAF58AE378E4CC /* Pods-Runner.profile.xcconfig */, + B0A5FF3660D3DF525F426F09 /* Pods-RunnerTests.debug.xcconfig */, + 0E7B48D057702E4D32F05D9B /* Pods-RunnerTests.release.xcconfig */, + 23739D936EBBE89AB494C04E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 28F5A4EE37DD219D58F17275 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + EDDB347322BEACC8DB1684CA /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 2C58858AB72D1B70D2AF6FFA /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 4E6F251814F88DD63A33F380 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 28F5A4EE37DD219D58F17275 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 2C58858AB72D1B70D2AF6FFA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +330,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 4E6F251814F88DD63A33F380 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B0A5FF3660D3DF525F426F09 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0E7B48D057702E4D32F05D9B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 23739D936EBBE89AB494C04E /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/core/dependency_injection.dart b/lib/core/dependency_injection.dart new file mode 100644 index 0000000..4c85fcc --- /dev/null +++ b/lib/core/dependency_injection.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final GetIt getIt = GetIt.instance; + +class DependencyInjection { + static final DependencyInjection _singleton = DependencyInjection._internal(); + + static const String _apiKey = + 'SJWsWG4DEbZIZpnWNxc9K3Es9C2o27Vqpl-v5kNT-ZkYTmKB6ffnjo9Mzg6N0uHgFdJNOYVynd3kWso-tTrVMwpz2gIROLO-BlPdWcZkKKTA7cUr_tiVy5Dry3XjZnYx'; + + factory DependencyInjection() { + return _singleton; + } + + DependencyInjection._internal(); + + void init() async { + //Core + getIt.registerLazySingleton( + () => Storage( + getIt.get(), + ), + ); + + //External + final sharedPreferences = await SharedPreferences.getInstance(); + getIt.registerLazySingleton(() => sharedPreferences); + + getIt.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ), + ); + } +} diff --git a/lib/core/utils/storage.dart b/lib/core/utils/storage.dart new file mode 100644 index 0000000..0902ee0 --- /dev/null +++ b/lib/core/utils/storage.dart @@ -0,0 +1,28 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class StorageInterface { + Future write(String key, String value); + Future read(String key); + Future delete(String key); +} + +class Storage implements StorageInterface { + final SharedPreferences sharedPreferences; + + Storage(this.sharedPreferences); + + @override + Future read(String key) async { + return sharedPreferences.getString(key); + } + + @override + Future write(String key, String value) async { + await sharedPreferences.setString(key, value); + } + + @override + Future delete(String key) async { + await sharedPreferences.remove(key); + } +} diff --git a/lib/typography.dart b/lib/core/utils/typography.dart similarity index 100% rename from lib/typography.dart rename to lib/core/utils/typography.dart diff --git a/lib/models/restaurant.dart b/lib/data/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/data/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/data/models/restaurant.g.dart diff --git a/lib/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart similarity index 92% rename from lib/repositories/yelp_repository.dart rename to lib/data/repositories/yelp_repository.dart index 9eab02a..611de58 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,8 +1,9 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; -const _apiKey = ''; +const _apiKey = + 'SJWsWG4DEbZIZpnWNxc9K3Es9C2o27Vqpl-v5kNT-ZkYTmKB6ffnjo9Mzg6N0uHgFdJNOYVynd3kWso-tTrVMwpz2gIROLO-BlPdWcZkKKTA7cUr_tiVy5Dry3XjZnYx'; class YelpRepository { late Dio dio; diff --git a/lib/domain/repositories/yelp_repository_contract.dart b/lib/domain/repositories/yelp_repository_contract.dart new file mode 100644 index 0000000..6cb109c --- /dev/null +++ b/lib/domain/repositories/yelp_repository_contract.dart @@ -0,0 +1,5 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; + +abstract class YelpRepositoryContract { + Future getRestaurants({int offset = 0}); +} diff --git a/lib/main.dart b/lib/main.dart index 3a4af7d..08ea967 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/presentation/pages/home_page.dart'; void main() { runApp(const RestaurantTour()); } class RestaurantTour extends StatelessWidget { - const RestaurantTour({Key? key}) : super(key: key); + const RestaurantTour({super.key}); @override Widget build(BuildContext context) { @@ -16,38 +16,3 @@ class RestaurantTour extends StatelessWidget { ); } } - -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart new file mode 100644 index 0000000..8c7b3cb --- /dev/null +++ b/lib/presentation/pages/home_page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Restaurant Tour'), + ElevatedButton( + child: const Text('Fetch Restaurants'), + onPressed: () async { + final yelpRepo = YelpRepository(); + + try { + final result = await yelpRepo.getRestaurants(); + if (result != null) { + print('Fetched ${result.restaurants!.length} restaurants'); + } else { + print('No restaurants fetched'); + } + } catch (e) { + print('Failed to fetch restaurants: $e'); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 27b6e40..d7a624c 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: @@ -201,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -222,6 +238,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -243,6 +267,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -251,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" glob: dependency: transitive description: @@ -395,6 +432,14 @@ 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" package_config: dependency: transitive description: @@ -419,6 +464,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -427,6 +496,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + 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: @@ -435,6 +520,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: @@ -451,6 +544,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -624,6 +773,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" xml: dependency: transitive description: @@ -642,4 +799,4 @@ packages: version: "3.1.0" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4018593..28376e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,9 @@ dependencies: dio: ^5.6.0 json_annotation: ^4.9.0 flutter_svg: ^2.0.10 + get_it: ^7.7.0 + flutter_bloc: ^8.1.6 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: From 3fdc778f33a460cfe745248e28e7f40625ce1e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Fri, 13 Sep 2024 21:21:31 -0300 Subject: [PATCH 03/14] feat: add all restaurants page --- .gitignore | 1 + ios/Runner.xcodeproj/project.pbxproj | 30 +-- lib/core/dependency_injection.dart | 47 ++++- lib/core/environment.dart | 10 + .../restaurants_local_storage.dart | 36 ++++ lib/data/repositories/yelp_repository.dart | 22 +-- .../restaurants_local_storage_contract.dart | 6 + .../get_restaurants_usecase_contract.dart | 5 + .../usecases/get_restaurants_usecase.dart | 31 ++++ lib/main.dart | 16 +- .../controllers/cubit/restaurants_cubit.dart | 25 +++ .../cubit/restaurants_cubit_state.dart | 20 ++ lib/presentation/pages/favorites_page.dart | 41 +++++ lib/presentation/pages/home_page.dart | 50 ++--- lib/presentation/pages/restaurants_page.dart | 49 +++++ lib/presentation/widgets/is_open_widget.dart | 29 +++ lib/presentation/widgets/ratings.dart | 20 ++ .../widgets/restaurants_card.dart | 79 ++++++++ pubspec.lock | 172 +++++++++--------- pubspec.yaml | 5 +- 20 files changed, 549 insertions(+), 145 deletions(-) create mode 100644 lib/core/environment.dart create mode 100644 lib/data/local_storages/restaurants_local_storage.dart create mode 100644 lib/domain/local_storages/restaurants_local_storage_contract.dart create mode 100644 lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart create mode 100644 lib/domain/usecases/get_restaurants_usecase.dart create mode 100644 lib/presentation/controllers/cubit/restaurants_cubit.dart create mode 100644 lib/presentation/controllers/cubit/restaurants_cubit_state.dart create mode 100644 lib/presentation/pages/favorites_page.dart create mode 100644 lib/presentation/pages/restaurants_page.dart create mode 100644 lib/presentation/widgets/is_open_widget.dart create mode 100644 lib/presentation/widgets/ratings.dart create mode 100644 lib/presentation/widgets/restaurants_card.dart diff --git a/.gitignore b/.gitignore index 7040cb0..7c68ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +*.env # IntelliJ related *.iml diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2d9d9dc..8b4fe57 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -198,7 +198,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4E6F251814F88DD63A33F380 /* [CP] Embed Pods Frameworks */, + A7F043B40F3703731E5805BA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -330,37 +330,37 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4E6F251814F88DD63A33F380 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + name = "Run Script"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + A7F043B40F3703731E5805BA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/lib/core/dependency_injection.dart b/lib/core/dependency_injection.dart index 4c85fcc..28d61d8 100644 --- a/lib/core/dependency_injection.dart +++ b/lib/core/dependency_injection.dart @@ -1,6 +1,14 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; +import 'package:restaurant_tour/core/environment.dart'; import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/local_storages/restaurants_local_storage.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; +import 'package:restaurant_tour/domain/usecases/get_restaurants_usecase.dart'; +import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; import 'package:shared_preferences/shared_preferences.dart'; final GetIt getIt = GetIt.instance; @@ -8,16 +16,13 @@ final GetIt getIt = GetIt.instance; class DependencyInjection { static final DependencyInjection _singleton = DependencyInjection._internal(); - static const String _apiKey = - 'SJWsWG4DEbZIZpnWNxc9K3Es9C2o27Vqpl-v5kNT-ZkYTmKB6ffnjo9Mzg6N0uHgFdJNOYVynd3kWso-tTrVMwpz2gIROLO-BlPdWcZkKKTA7cUr_tiVy5Dry3XjZnYx'; - factory DependencyInjection() { return _singleton; } DependencyInjection._internal(); - void init() async { + Future init() async { //Core getIt.registerLazySingleton( () => Storage( @@ -25,7 +30,37 @@ class DependencyInjection { ), ); - //External + //Data + getIt.registerLazySingleton( + () => RestaurantsLocalStorage( + localStorage: getIt.get(), + ), + ); + + getIt.registerLazySingleton( + () => YelpRepository( + dio: getIt.get(), + ), + ); + + //Usecases + getIt.registerLazySingleton( + () => GetRestaurantsUsecase( + yelpRepositoryContract: getIt.get(), + restaurantsLocalStorageContract: + getIt.get(), + ), + ); + + //Cubit + getIt.registerLazySingleton( + () => RestaurantsCubit( + getRestaurantsUsecaseContract: + getIt.get(), + ), + ); + + // External final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => sharedPreferences); @@ -34,7 +69,7 @@ class DependencyInjection { BaseOptions( baseUrl: 'https://api.yelp.com', headers: { - 'Authorization': 'Bearer $_apiKey', + 'Authorization': 'Bearer ${Environment.apiKey}', 'Content-Type': 'application/graphql', }, ), diff --git a/lib/core/environment.dart b/lib/core/environment.dart new file mode 100644 index 0000000..3253b08 --- /dev/null +++ b/lib/core/environment.dart @@ -0,0 +1,10 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class Environment { + static String apiKey = ''; + + static Future load() async { + await dotenv.load(); + apiKey = dotenv.env['API_KEY'] ?? ''; + } +} diff --git a/lib/data/local_storages/restaurants_local_storage.dart b/lib/data/local_storages/restaurants_local_storage.dart new file mode 100644 index 0000000..d8053a2 --- /dev/null +++ b/lib/data/local_storages/restaurants_local_storage.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; + +class RestaurantsLocalStorage implements RestaurantsLocalStorageContract { + RestaurantsLocalStorage({ + required StorageInterface localStorage, + }) : _localStorage = localStorage; + + final StorageInterface _localStorage; + + final restaurantsKey = 'restaurants'; + + @override + Future saveRestaurants(List restaurants) async { + final mapList = + restaurants.map((restaurant) => restaurant.toJson()).toList(); + + await _localStorage.write( + restaurantsKey, + jsonEncode(mapList), + ); + } + + @override + Future> getCachedRestaurants() async { + final json = await _localStorage.read(restaurantsKey); + if (json == null) { + return []; + } + final List mapList = jsonDecode(json); + return mapList.map((map) => Restaurant.fromJson(map)).toList(); + } +} diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 611de58..0d6ced4 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,25 +1,16 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; const _apiKey = 'SJWsWG4DEbZIZpnWNxc9K3Es9C2o27Vqpl-v5kNT-ZkYTmKB6ffnjo9Mzg6N0uHgFdJNOYVynd3kWso-tTrVMwpz2gIROLO-BlPdWcZkKKTA7cUr_tiVy5Dry3XjZnYx'; -class YelpRepository { - late Dio dio; - +class YelpRepository extends YelpRepositoryContract { YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); + required this.dio, + }); + + final Dio dio; /// Returns a response in this shape /// { @@ -61,6 +52,7 @@ class YelpRepository { /// } /// } /// + @override Future getRestaurants({int offset = 0}) async { try { final response = await dio.post>( diff --git a/lib/domain/local_storages/restaurants_local_storage_contract.dart b/lib/domain/local_storages/restaurants_local_storage_contract.dart new file mode 100644 index 0000000..2735bef --- /dev/null +++ b/lib/domain/local_storages/restaurants_local_storage_contract.dart @@ -0,0 +1,6 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; + +abstract class RestaurantsLocalStorageContract { + Future saveRestaurants(List restaurants); + Future> getCachedRestaurants(); +} diff --git a/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart new file mode 100644 index 0000000..ddfb8ff --- /dev/null +++ b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart @@ -0,0 +1,5 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; + +abstract class GetRestaurantsUsecaseContract { + Future> getRestaurants({bool forceFetch = false}); +} diff --git a/lib/domain/usecases/get_restaurants_usecase.dart b/lib/domain/usecases/get_restaurants_usecase.dart new file mode 100644 index 0000000..8e5cdd3 --- /dev/null +++ b/lib/domain/usecases/get_restaurants_usecase.dart @@ -0,0 +1,31 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; + +class GetRestaurantsUsecase implements GetRestaurantsUsecaseContract { + GetRestaurantsUsecase({ + required YelpRepositoryContract yelpRepositoryContract, + required RestaurantsLocalStorageContract restaurantsLocalStorageContract, + }) : _yelpRepositoryContract = yelpRepositoryContract, + _restaurantsLocalStorageContract = restaurantsLocalStorageContract; + + final YelpRepositoryContract _yelpRepositoryContract; + final RestaurantsLocalStorageContract _restaurantsLocalStorageContract; + + @override + Future> getRestaurants({bool forceFetch = false}) async { + final cachedRestaurants = + await _restaurantsLocalStorageContract.getCachedRestaurants(); + + if (cachedRestaurants.isNotEmpty && !forceFetch) { + return cachedRestaurants; + } + + final restaurantsQueryResults = + await _yelpRepositoryContract.getRestaurants(); + final restaurants = restaurantsQueryResults?.restaurants; + await _restaurantsLocalStorageContract.saveRestaurants(restaurants ?? []); + return restaurants ?? []; + } +} diff --git a/lib/main.dart b/lib/main.dart index 08ea967..c5483f1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/dependency_injection.dart'; +import 'package:restaurant_tour/core/environment.dart'; +import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; import 'package:restaurant_tour/presentation/pages/home_page.dart'; -void main() { +void main() async { + await Environment.load(); + await DependencyInjection().init(); + runApp(const RestaurantTour()); } @@ -10,9 +17,12 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( title: 'Restaurant Tour', - home: HomePage(), + home: BlocProvider( + create: (context) => getIt.get(), + child: const HomePage(), + ), ); } } diff --git a/lib/presentation/controllers/cubit/restaurants_cubit.dart b/lib/presentation/controllers/cubit/restaurants_cubit.dart new file mode 100644 index 0000000..b9e2168 --- /dev/null +++ b/lib/presentation/controllers/cubit/restaurants_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; + +part 'restaurants_cubit_state.dart'; + +class RestaurantsCubit extends Cubit { + RestaurantsCubit({required this.getRestaurantsUsecaseContract}) + : super(RestaurantsCubitInitial()); + + final GetRestaurantsUsecaseContract getRestaurantsUsecaseContract; + + Future getRestaurants({bool forceFetch = false}) async { + emit(RestaurantsCubitLoading()); + try { + final restaurants = await getRestaurantsUsecaseContract.getRestaurants( + forceFetch: forceFetch, + ); + emit(RestaurantsCubitLoaded(restaurants)); + } catch (e) { + emit(RestaurantsCubitError(e.toString())); + } + } +} diff --git a/lib/presentation/controllers/cubit/restaurants_cubit_state.dart b/lib/presentation/controllers/cubit/restaurants_cubit_state.dart new file mode 100644 index 0000000..b230e72 --- /dev/null +++ b/lib/presentation/controllers/cubit/restaurants_cubit_state.dart @@ -0,0 +1,20 @@ +part of 'restaurants_cubit.dart'; + +@immutable +sealed class RestaurantsCubitState {} + +final class RestaurantsCubitInitial extends RestaurantsCubitState {} + +final class RestaurantsCubitLoading extends RestaurantsCubitState {} + +final class RestaurantsCubitLoaded extends RestaurantsCubitState { + final List restaurants; + + RestaurantsCubitLoaded(this.restaurants); +} + +final class RestaurantsCubitError extends RestaurantsCubitState { + final String message; + + RestaurantsCubitError(this.message); +} diff --git a/lib/presentation/pages/favorites_page.dart b/lib/presentation/pages/favorites_page.dart new file mode 100644 index 0000000..925d915 --- /dev/null +++ b/lib/presentation/pages/favorites_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class FavoritesPage extends StatefulWidget { + const FavoritesPage({super.key}); + + @override + State createState() => _FavoritesPageState(); +} + +class _FavoritesPageState extends State { + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Favorites'), + ElevatedButton( + child: const Text('Favorites'), + onPressed: () async { + // final yelpRepo = YelpRepository(); + + try { + // final result = await yelpRepo.getRestaurants(); + // if (result != null) { + // print( + // 'Fetched ${result.restaurants!.length} restaurants', + // ); + // } else { + // print('No restaurants fetched'); + // } + } catch (e) { + print('Failed to fetch restaurants: $e'); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index 8c7b3cb..143ea98 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -1,34 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/presentation/pages/favorites_page.dart'; +import 'package:restaurant_tour/presentation/pages/restaurants_page.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, + return DefaultTabController( + length: 2, + initialIndex: 0, + child: Scaffold( + appBar: AppBar( + title: const Text('Restaurant Tour'), + bottom: TabBar( + overlayColor: WidgetStateProperty.all(Colors.red[200]), + indicatorColor: Colors.black, + labelStyle: const TextStyle( + fontSize: 14, + color: Colors.black, ), + dividerColor: Colors.transparent, + tabs: const [ + Tab( + text: 'All Restaurants', + ), + Tab(text: 'My Favorites'), + ], + ), + ), + body: const TabBarView( + children: [ + RestaurantsPage(), + FavoritesPage(), ], ), ), diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart new file mode 100644 index 0000000..21e9134 --- /dev/null +++ b/lib/presentation/pages/restaurants_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurants_card.dart'; + +class RestaurantsPage extends StatefulWidget { + const RestaurantsPage({super.key}); + + @override + State createState() => _RestaurantsPageState(); +} + +class _RestaurantsPageState extends State { + @override + void initState() { + super.initState(); + context.read().getRestaurants(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RestaurantsCubitLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (state is RestaurantsCubitLoaded) { + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...state.restaurants.map( + (restaurant) => RestaurantsCard(restaurant: restaurant), + ), + ], + ), + ), + ); + } + return const Center( + child: Text('Failed to fetch restaurants'), + ); + }, + ); + } +} diff --git a/lib/presentation/widgets/is_open_widget.dart b/lib/presentation/widgets/is_open_widget.dart new file mode 100644 index 0000000..e3ed09e --- /dev/null +++ b/lib/presentation/widgets/is_open_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; + +class IsOpenWidget extends StatelessWidget { + const IsOpenWidget({required this.isOpen, super.key}); + + final bool isOpen; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + isOpen ? 'Open now' : 'Closed', + style: AppTextStyles.openRegularItalic, + ), + const SizedBox(width: 4), + Container( + height: 10, + width: 10, + decoration: BoxDecoration( + color: isOpen ? Colors.green : Colors.red, + shape: BoxShape.circle, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/ratings.dart b/lib/presentation/widgets/ratings.dart new file mode 100644 index 0000000..9df4ec2 --- /dev/null +++ b/lib/presentation/widgets/ratings.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class Ratings extends StatelessWidget { + const Ratings({super.key, required this.rating}); + + final double rating; + @override + Widget build(BuildContext context) { + return Row( + children: List.generate( + rating.round(), + (index) => const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/restaurants_card.dart b/lib/presentation/widgets/restaurants_card.dart new file mode 100644 index 0000000..5fd3d0a --- /dev/null +++ b/lib/presentation/widgets/restaurants_card.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/widgets/is_open_widget.dart'; +import 'package:restaurant_tour/presentation/widgets/ratings.dart'; + +class RestaurantsCard extends StatelessWidget { + const RestaurantsCard({required this.restaurant, super.key}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Card( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + restaurant.heroImage, + fit: BoxFit.cover, + width: 100, + height: 100, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8), + child: Expanded( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + restaurant.name ?? '', + maxLines: 2, + style: AppTextStyles.loraRegularTitle, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + '${restaurant.price}', + style: AppTextStyles.openRegularText, + ), + const SizedBox(width: 4), + Text( + restaurant.categories?.first.alias ?? '', + style: AppTextStyles.openRegularText, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Ratings( + rating: restaurant.rating ?? 0, + ), + IsOpenWidget(isOpen: restaurant.isOpen), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index d7a624c..d55b0ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.1" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -121,22 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -165,42 +157,42 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: name: dio - sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.7.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" fake_async: dependency: transitive description: @@ -221,18 +213,18 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -246,6 +238,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -276,10 +276,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" get_it: dependency: "direct main" description: @@ -292,18 +292,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" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http: dependency: transitive description: @@ -316,34 +316,34 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: 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: @@ -396,10 +396,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -428,10 +428,10 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.6" nested: dependency: transitive description: @@ -444,10 +444,10 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" provider: dependency: transitive description: @@ -532,18 +532,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" shared_preferences: dependency: "direct main" description: @@ -604,18 +604,18 @@ packages: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -665,10 +665,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -697,18 +697,18 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_graphics: dependency: transitive description: @@ -753,10 +753,10 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: @@ -765,14 +765,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.1" xdg_directories: dependency: transitive description: @@ -793,10 +801,10 @@ packages: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 28376e0..7301d40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,8 @@ dependencies: flutter_svg: ^2.0.10 get_it: ^7.7.0 flutter_bloc: ^8.1.6 - shared_preferences: ^2.3.2 + shared_preferences: ^2.3.1 + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: @@ -30,6 +31,8 @@ dev_dependencies: flutter: generate: true uses-material-design: true + assets: + - .env fonts: - family: Lora fonts: From 158b6061c3eb424e8fc667b03ff29170355a9782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Fri, 13 Sep 2024 21:54:46 -0300 Subject: [PATCH 04/14] feat: add offset on reach bottom --- .../get_restaurants_usecase_contract.dart | 5 +- .../usecases/get_restaurants_usecase.dart | 7 ++- .../controllers/cubit/restaurants_cubit.dart | 19 +++++++ lib/presentation/pages/restaurants_page.dart | 15 +----- .../widgets/restaurants_list.dart | 50 +++++++++++++++++++ 5 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 lib/presentation/widgets/restaurants_list.dart diff --git a/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart index ddfb8ff..029dd82 100644 --- a/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart +++ b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart @@ -1,5 +1,8 @@ import 'package:restaurant_tour/data/models/restaurant.dart'; abstract class GetRestaurantsUsecaseContract { - Future> getRestaurants({bool forceFetch = false}); + Future> getRestaurants({ + bool forceFetch = false, + int offset = 0, + }); } diff --git a/lib/domain/usecases/get_restaurants_usecase.dart b/lib/domain/usecases/get_restaurants_usecase.dart index 8e5cdd3..29ee39b 100644 --- a/lib/domain/usecases/get_restaurants_usecase.dart +++ b/lib/domain/usecases/get_restaurants_usecase.dart @@ -14,7 +14,10 @@ class GetRestaurantsUsecase implements GetRestaurantsUsecaseContract { final RestaurantsLocalStorageContract _restaurantsLocalStorageContract; @override - Future> getRestaurants({bool forceFetch = false}) async { + Future> getRestaurants({ + bool forceFetch = false, + int offset = 0, + }) async { final cachedRestaurants = await _restaurantsLocalStorageContract.getCachedRestaurants(); @@ -23,7 +26,7 @@ class GetRestaurantsUsecase implements GetRestaurantsUsecaseContract { } final restaurantsQueryResults = - await _yelpRepositoryContract.getRestaurants(); + await _yelpRepositoryContract.getRestaurants(offset: offset); final restaurants = restaurantsQueryResults?.restaurants; await _restaurantsLocalStorageContract.saveRestaurants(restaurants ?? []); return restaurants ?? []; diff --git a/lib/presentation/controllers/cubit/restaurants_cubit.dart b/lib/presentation/controllers/cubit/restaurants_cubit.dart index b9e2168..f31dc15 100644 --- a/lib/presentation/controllers/cubit/restaurants_cubit.dart +++ b/lib/presentation/controllers/cubit/restaurants_cubit.dart @@ -11,12 +11,31 @@ class RestaurantsCubit extends Cubit { final GetRestaurantsUsecaseContract getRestaurantsUsecaseContract; + int offset = 0; + final List _restaurants = []; + Future getRestaurants({bool forceFetch = false}) async { emit(RestaurantsCubitLoading()); try { final restaurants = await getRestaurantsUsecaseContract.getRestaurants( forceFetch: forceFetch, + offset: offset, + ); + emit(RestaurantsCubitLoaded(restaurants)); + } catch (e) { + emit(RestaurantsCubitError(e.toString())); + } + } + + Future getNextPage({bool forceFetch = false}) async { + emit(RestaurantsCubitLoading()); + try { + offset += 20; + final restaurants = await getRestaurantsUsecaseContract.getRestaurants( + forceFetch: true, + offset: offset, ); + _restaurants.addAll(restaurants); emit(RestaurantsCubitLoaded(restaurants)); } catch (e) { emit(RestaurantsCubitError(e.toString())); diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart index 21e9134..b4ad623 100644 --- a/lib/presentation/pages/restaurants_page.dart +++ b/lib/presentation/pages/restaurants_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; -import 'package:restaurant_tour/presentation/widgets/restaurants_card.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurants_list.dart'; class RestaurantsPage extends StatefulWidget { const RestaurantsPage({super.key}); @@ -27,18 +27,7 @@ class _RestaurantsPageState extends State { ); } if (state is RestaurantsCubitLoaded) { - return Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...state.restaurants.map( - (restaurant) => RestaurantsCard(restaurant: restaurant), - ), - ], - ), - ), - ); + return RestaurantsList(restaurants: state.restaurants); } return const Center( child: Text('Failed to fetch restaurants'), diff --git a/lib/presentation/widgets/restaurants_list.dart b/lib/presentation/widgets/restaurants_list.dart new file mode 100644 index 0000000..66e119e --- /dev/null +++ b/lib/presentation/widgets/restaurants_list.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurants_card.dart'; + +class RestaurantsList extends StatefulWidget { + const RestaurantsList({super.key, required this.restaurants}); + + final List restaurants; + + @override + State createState() => _RestaurantsListState(); +} + +class _RestaurantsListState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + initState() { + super.initState(); + _scrollController.addListener(() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + context.read().getNextPage(); + } + }); + } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + context.read().getRestaurants( + forceFetch: true, + ); + }, + child: Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: widget.restaurants.length, + itemBuilder: (context, index) { + final restaurant = widget.restaurants[index]; + return RestaurantsCard(restaurant: restaurant); + }, + ), + ), + ); + } +} From 73e6b8a509b5b1f6d3d51b7094ee94ef745183d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Fri, 13 Sep 2024 21:55:00 -0300 Subject: [PATCH 05/14] chore: add mocked response --- lib/data/repositories/yelp_repository.dart | 377 +++++++++++++++++++++ 1 file changed, 377 insertions(+) diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 0d6ced4..e80675b 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -60,6 +60,383 @@ class YelpRepository extends YelpRepositoryContract { data: _getQuery(offset), ); return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } on DioException catch (e) { + final Map mockedResponse = { + "total": 10, + "business": [ + { + "id": "1", + "name": "Restaurant Name Goes Here And Wraps 2 Lines One", + "price": "\$", + "rating": 4.5, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "italian", + "title": "Italian", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r1", + "rating": 5, + "text": + "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", + "user": { + "id": "u1", + "image_url": + "https://media.licdn.com/dms/image/v2/C4E03AQFZWrm17QwLIQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1652147256110?e=1731542400&v=beta&t=gR9A-qKQ63iBuzeyS3YG6qGNH0CrqLmi0VI7ELRJ8Gw", + "name": "John Doe", + }, + }, + { + "id": "r2", + "rating": 2, + "text": + "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", + "user": { + "id": "u1", + "image_url": "https://example.com/user1.jpg", + "name": "John Doe", + }, + }, + { + "id": "r3", + "rating": 4, + "text": "Amazing food and atmosphere!", + "user": { + "id": "u1", + "image_url": "https://example.com/user1.jpg", + "name": "John Doe", + }, + } + ], + "location": { + "formatted_address": "123 Main St, New York, NY", + }, + }, + { + "id": "2", + "name": "Restaurant Name Goes Here And Wraps 2 Lines Two", + "price": "\$", + "rating": 4.0, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "mexican", + "title": "Mexican", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [], + "location": { + "formatted_address": "456 Broadway, Los Angeles, CA", + }, + }, + { + "id": "3", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Three", + "price": "\$", + "rating": 3.5, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "chinese", + "title": "Chinese", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r3", + "rating": 3, + "text": "Average experience, nothing special.", + "user": { + "id": "u3", + "image_url": "https://example.com/user3.jpg", + "name": "Alice Brown", + }, + } + ], + "location": { + "formatted_address": "789 Market St, San Francisco, CA", + }, + }, + { + "id": "4", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Four", + "price": "\$", + "rating": 4.8, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "french", + "title": "French", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r4", + "rating": 5, + "text": "Exquisite dining experience.", + "user": { + "id": "u4", + "image_url": "https://example.com/user4.jpg", + "name": "Charlie Green", + }, + } + ], + "location": { + "formatted_address": "321 Park Ave, Boston, MA", + }, + }, + { + "id": "5", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Five", + "price": "\$", + "rating": 4.2, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "indian", + "title": "Indian", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r5", + "rating": 4, + "text": "Authentic Indian cuisine, loved it.", + "user": { + "id": "u5", + "image_url": "https://example.com/user5.jpg", + "name": "David Lee", + }, + } + ], + "location": { + "formatted_address": "654 Elm St, Chicago, IL", + }, + }, + { + "id": "6", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Six", + "price": "\$", + "rating": 4.6, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "japanese", + "title": "Japanese", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r6", + "rating": 5, + "text": "Amazing sushi and service.", + "user": { + "id": "u6", + "image_url": "https://example.com/user6.jpg", + "name": "Emily White", + }, + } + ], + "location": { + "formatted_address": "987 Sunset Blvd, Miami, FL", + }, + }, + { + "id": "7", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Seven", + "price": "\$", + "rating": 4.3, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "thai", + "title": "Thai", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r7", + "rating": 4, + "text": "Great flavors, but service was slow.", + "user": { + "id": "u7", + "image_url": "https://example.com/user7.jpg", + "name": "Michael Brown", + }, + } + ], + "location": { + "formatted_address": "852 Ocean Ave, Seattle, WA", + }, + }, + { + "id": "8", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Eight", + "price": "\$", + "rating": 3.9, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "burger", + "title": "Burger", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r8", + "rating": 4, + "text": "Great burgers for a reasonable price.", + "user": { + "id": "u8", + "image_url": "https://example.com/user8.jpg", + "name": "Jessica Johnson", + }, + } + ], + "location": { + "formatted_address": "753 Central St, Austin, TX", + }, + }, + { + "id": "9", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Nine", + "price": "\$", + "rating": 4.9, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "steakhouse", + "title": "Steakhouse", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r9", + "rating": 5, + "text": "Best steakhouse in town.", + "user": { + "id": "u9", + "image_url": "https://example.com/user9.jpg", + "name": "Chris Blue", + }, + } + ], + "location": { + "formatted_address": "369 High St, Dallas, TX", + }, + }, + { + "id": "10", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Ten", + "price": "\$\$", + "rating": 3.8, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "pizza", + "title": "Pizza", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r10", + "rating": 3, + "text": "Good pizza, but nothing special.", + "user": { + "id": "u10", + "image_url": "https://example.com/user10.jpg", + "name": "Tom Green", + }, + } + ], + "location": { + "formatted_address": "258 Oak St, Orlando, FL", + }, + } + ], + }; + return RestaurantQueryResult.fromJson(mockedResponse); } catch (e) { return null; } From 0593c85a5c01ddef950eeb61f4d4e8d9c5fdb665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Fri, 13 Sep 2024 22:43:19 -0300 Subject: [PATCH 06/14] feat: restaurant details --- lib/data/repositories/yelp_repository.dart | 3 +- .../pages/restaurant_details_page.dart | 72 ++++++++++ .../restaurant_details/address_section.dart | 44 ++++++ .../general_info_section.dart | 47 +++++++ .../overall_rating_section.dart | 50 +++++++ .../restaurant_details/reviews_section.dart | 54 ++++++++ .../widgets/restaurants_card.dart | 130 ++++++++++-------- 7 files changed, 342 insertions(+), 58 deletions(-) create mode 100644 lib/presentation/pages/restaurant_details_page.dart create mode 100644 lib/presentation/widgets/restaurant_details/address_section.dart create mode 100644 lib/presentation/widgets/restaurant_details/general_info_section.dart create mode 100644 lib/presentation/widgets/restaurant_details/overall_rating_section.dart create mode 100644 lib/presentation/widgets/restaurant_details/reviews_section.dart diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index e80675b..0761e40 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -91,8 +91,7 @@ class YelpRepository extends YelpRepositoryContract { "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", "user": { "id": "u1", - "image_url": - "https://media.licdn.com/dms/image/v2/C4E03AQFZWrm17QwLIQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1652147256110?e=1731542400&v=beta&t=gR9A-qKQ63iBuzeyS3YG6qGNH0CrqLmi0VI7ELRJ8Gw", + "image_url": "https://example.com/user1.jpg", "name": "John Doe", }, }, diff --git a/lib/presentation/pages/restaurant_details_page.dart b/lib/presentation/pages/restaurant_details_page.dart new file mode 100644 index 0000000..19a3635 --- /dev/null +++ b/lib/presentation/pages/restaurant_details_page.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurant_details/address_section.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurant_details/general_info_section.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurant_details/overall_rating_section.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurant_details/reviews_section.dart'; + +class RestaurantDetailsPage extends StatefulWidget { + const RestaurantDetailsPage({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + State createState() => _RestaurantDetailsPageState(); +} + +class _RestaurantDetailsPageState extends State { + @override + Widget build(BuildContext context) { + final restaurant = widget.restaurant; + return Scaffold( + appBar: AppBar( + title: Text(restaurant.name ?? ''), + actions: [ + IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + // Add to favorites + }, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + key: Key(restaurant.heroImage), + tag: restaurant.heroImage, + transitionOnUserGestures: true, + child: Image.network( + restaurant.heroImage, + ), + ), + const SizedBox( + height: 24, + ), + GeneralInfoSection(restaurant: restaurant), + const SizedBox( + height: 24, + ), + AddressSection(restaurant: restaurant), + OverallRatingSection(restaurant: restaurant), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + '${restaurant.reviews?.length} Reviews', + style: AppTextStyles.openRegularTitle, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 24, + ), + ReviewsSection(reviews: restaurant.reviews ?? []), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/restaurant_details/address_section.dart b/lib/presentation/widgets/restaurant_details/address_section.dart new file mode 100644 index 0000000..4b7c671 --- /dev/null +++ b/lib/presentation/widgets/restaurant_details/address_section.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; + +class AddressSection extends StatelessWidget { + const AddressSection({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'Address', + style: AppTextStyles.openRegularText, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + restaurant.location?.formattedAddress ?? '', + style: AppTextStyles.openRegularTitle, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 24, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Divider(), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/restaurant_details/general_info_section.dart b/lib/presentation/widgets/restaurant_details/general_info_section.dart new file mode 100644 index 0000000..17bb854 --- /dev/null +++ b/lib/presentation/widgets/restaurant_details/general_info_section.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/widgets/is_open_widget.dart'; + +class GeneralInfoSection extends StatelessWidget { + const GeneralInfoSection({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + '${restaurant.price}', + style: AppTextStyles.openRegularText, + ), + const SizedBox(width: 4), + Text( + restaurant.categories?.first.alias ?? '', + style: AppTextStyles.openRegularText, + ), + ], + ), + IsOpenWidget(isOpen: restaurant.isOpen), + ], + ), + ), + const SizedBox( + height: 24, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Divider(), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/restaurant_details/overall_rating_section.dart b/lib/presentation/widgets/restaurant_details/overall_rating_section.dart new file mode 100644 index 0000000..be48eb6 --- /dev/null +++ b/lib/presentation/widgets/restaurant_details/overall_rating_section.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; + +class OverallRatingSection extends StatelessWidget { + const OverallRatingSection({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'Overall Rating', + style: AppTextStyles.openRegularTitle, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Text( + restaurant.rating.toString(), + style: AppTextStyles.openRegularTitle, + textAlign: TextAlign.start, + ), + const Icon( + Icons.star, + size: 16, + color: Colors.amber, + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Divider(), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/restaurant_details/reviews_section.dart b/lib/presentation/widgets/restaurant_details/reviews_section.dart new file mode 100644 index 0000000..15354b0 --- /dev/null +++ b/lib/presentation/widgets/restaurant_details/reviews_section.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/widgets/ratings.dart'; + +class ReviewsSection extends StatelessWidget { + const ReviewsSection({super.key, required this.reviews}); + + final List reviews; + + @override + Widget build(BuildContext context) { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: reviews.length ?? 0, + itemBuilder: (context, index) { + final review = reviews[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Ratings(rating: review.rating?.toDouble() ?? 0), + ], + ), + const SizedBox( + height: 8, + ), + Text( + review.text ?? 'description text', + style: AppTextStyles.openRegularText, + ), + Row( + children: [ + CircleAvatar( + backgroundImage: NetworkImage(review.user?.imageUrl ?? ''), + ), + Text( + review.user?.name ?? '', + style: AppTextStyles.openRegularText, + ), + ], + ), + const Divider(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/widgets/restaurants_card.dart b/lib/presentation/widgets/restaurants_card.dart index 5fd3d0a..77b5068 100644 --- a/lib/presentation/widgets/restaurants_card.dart +++ b/lib/presentation/widgets/restaurants_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/pages/restaurant_details_page.dart'; import 'package:restaurant_tour/presentation/widgets/is_open_widget.dart'; import 'package:restaurant_tour/presentation/widgets/ratings.dart'; @@ -11,68 +12,85 @@ class RestaurantsCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Image.network( - restaurant.heroImage, - fit: BoxFit.cover, - width: 100, - height: 100, - ), + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RestaurantDetailsPage( + restaurant: restaurant, ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8), - child: Expanded( - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - restaurant.name ?? '', - maxLines: 2, - style: AppTextStyles.loraRegularTitle, + ); + }, + child: Card( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Hero( + key: Key(restaurant.heroImage), + transitionOnUserGestures: true, + tag: restaurant.heroImage, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + restaurant.heroImage, + fit: BoxFit.cover, + width: 100, + height: 100, + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8), + child: Expanded( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + restaurant.name ?? '', + maxLines: 2, + style: AppTextStyles.loraRegularTitle, + ), ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Text( - '${restaurant.price}', - style: AppTextStyles.openRegularText, - ), - const SizedBox(width: 4), - Text( - restaurant.categories?.first.alias ?? '', - style: AppTextStyles.openRegularText, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Ratings( - rating: restaurant.rating ?? 0, - ), - IsOpenWidget(isOpen: restaurant.isOpen), - ], - ), - ], + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + '${restaurant.price}', + style: AppTextStyles.openRegularText, + ), + const SizedBox(width: 4), + Text( + restaurant.categories?.first.alias ?? '', + style: AppTextStyles.openRegularText, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Ratings( + rating: restaurant.rating ?? 0, + ), + IsOpenWidget(isOpen: restaurant.isOpen), + ], + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ); } From 318ec0ab5a7329d9731f4199728c99d02e288c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sat, 14 Sep 2024 11:00:21 -0300 Subject: [PATCH 07/14] feat: favorites ui and logic --- lib/core/dependency_injection.dart | 25 +++- .../favorites_local_storage.dart | 44 +++++++ lib/data/models/restaurant.dart | 10 +- .../favorites_local_storage_contract.dart | 7 ++ .../favorites_usecase_contract.dart | 8 ++ lib/domain/usecases/favorites_usecase.dart | 32 +++++ lib/main.dart | 14 ++- .../favorites/favorites_cubit.dart | 35 ++++++ .../favorites/favorites_state.dart | 32 +++++ .../restaurants_cubit.dart | 0 .../restaurants_cubit_state.dart | 0 lib/presentation/pages/favorites_page.dart | 49 ++++---- .../pages/restaurant_details_page.dart | 114 +++++++++++------- lib/presentation/pages/restaurants_page.dart | 2 +- .../widgets/restaurants_list.dart | 2 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 17 files changed, 306 insertions(+), 77 deletions(-) create mode 100644 lib/data/local_storages/favorites_local_storage.dart create mode 100644 lib/domain/local_storages/favorites_local_storage_contract.dart create mode 100644 lib/domain/usecase_contracts/favorites_usecase_contract.dart create mode 100644 lib/domain/usecases/favorites_usecase.dart create mode 100644 lib/presentation/controllers/favorites/favorites_cubit.dart create mode 100644 lib/presentation/controllers/favorites/favorites_state.dart rename lib/presentation/controllers/{cubit => restaurants}/restaurants_cubit.dart (100%) rename lib/presentation/controllers/{cubit => restaurants}/restaurants_cubit_state.dart (100%) diff --git a/lib/core/dependency_injection.dart b/lib/core/dependency_injection.dart index 28d61d8..5b7498d 100644 --- a/lib/core/dependency_injection.dart +++ b/lib/core/dependency_injection.dart @@ -2,13 +2,18 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:restaurant_tour/core/environment.dart'; import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/local_storages/favorites_local_storage.dart'; import 'package:restaurant_tour/data/local_storages/restaurants_local_storage.dart'; import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; +import 'package:restaurant_tour/domain/usecases/favorites_usecase.dart'; import 'package:restaurant_tour/domain/usecases/get_restaurants_usecase.dart'; -import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; import 'package:shared_preferences/shared_preferences.dart'; final GetIt getIt = GetIt.instance; @@ -43,6 +48,12 @@ class DependencyInjection { ), ); + getIt.registerLazySingleton( + () => FavoritesLocalStorage( + storage: getIt.get(), + ), + ); + //Usecases getIt.registerLazySingleton( () => GetRestaurantsUsecase( @@ -52,6 +63,12 @@ class DependencyInjection { ), ); + getIt.registerLazySingleton( + () => FavoritesUsecase( + favoritesLocalStorge: getIt.get(), + ), + ); + //Cubit getIt.registerLazySingleton( () => RestaurantsCubit( @@ -60,6 +77,12 @@ class DependencyInjection { ), ); + getIt.registerLazySingleton( + () => FavoritesCubit( + favoritesUsecaseContract: getIt.get(), + ), + ); + // External final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => sharedPreferences); diff --git a/lib/data/local_storages/favorites_local_storage.dart b/lib/data/local_storages/favorites_local_storage.dart new file mode 100644 index 0000000..cb67cf8 --- /dev/null +++ b/lib/data/local_storages/favorites_local_storage.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; + +class FavoritesLocalStorage implements FavoritesLocalStorageContract { + FavoritesLocalStorage({required StorageInterface storage}) + : _storage = storage; + + final StorageInterface _storage; + + final String key = 'favorites'; + + @override + Future> getFavorites() async { + final json = await _storage.read('favorites'); + if (json == null) { + return []; + } + final maps = jsonDecode(json) as List; + return maps.map((e) => Restaurant.fromJson(e)).toList(); + } + + @override + Future saveFavorite(Restaurant restaurant) async { + final favorites = await getFavorites(); + favorites.add(restaurant); + await _storage.write( + 'favorites', + jsonEncode(favorites.map((e) => e.toJson()).toList()), + ); + } + + @override + Future removeFavorite(Restaurant restaurant) async { + final favorites = await getFavorites(); + favorites.removeWhere((e) => e.id == restaurant.id); + await _storage.write( + 'favorites', + jsonEncode(favorites.map((e) => e.toJson()).toList()), + ); + } +} diff --git a/lib/data/models/restaurant.dart b/lib/data/models/restaurant.dart index 1c7ad2f..2d3fe7d 100644 --- a/lib/data/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'restaurant.g.dart'; @@ -85,7 +86,7 @@ class Location { } @JsonSerializable() -class Restaurant { +class Restaurant extends Equatable { final String? id; final String? name; final String? price; @@ -137,6 +138,13 @@ class Restaurant { } return false; } + + @override + List get props => [ + id, + name, + price, + ]; } @JsonSerializable() diff --git a/lib/domain/local_storages/favorites_local_storage_contract.dart b/lib/domain/local_storages/favorites_local_storage_contract.dart new file mode 100644 index 0000000..c23573a --- /dev/null +++ b/lib/domain/local_storages/favorites_local_storage_contract.dart @@ -0,0 +1,7 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; + +abstract class FavoritesLocalStorageContract { + Future> getFavorites(); + Future saveFavorite(Restaurant restaurant); + Future removeFavorite(Restaurant restaurant); +} diff --git a/lib/domain/usecase_contracts/favorites_usecase_contract.dart b/lib/domain/usecase_contracts/favorites_usecase_contract.dart new file mode 100644 index 0000000..3234a3f --- /dev/null +++ b/lib/domain/usecase_contracts/favorites_usecase_contract.dart @@ -0,0 +1,8 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; + +abstract class FavoritesUsecaseContract { + Future> getFavorites(); + Future saveFavorite(Restaurant restaurant); + Future removeFavorite(Restaurant restaurant); + Future isFavorite(Restaurant restaurant); +} diff --git a/lib/domain/usecases/favorites_usecase.dart b/lib/domain/usecases/favorites_usecase.dart new file mode 100644 index 0000000..1b29aa5 --- /dev/null +++ b/lib/domain/usecases/favorites_usecase.dart @@ -0,0 +1,32 @@ +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; + +class FavoritesUsecase implements FavoritesUsecaseContract { + FavoritesUsecase({ + required FavoritesLocalStorageContract favoritesLocalStorge, + }) : _favoritesLocalStorge = favoritesLocalStorge; + + final FavoritesLocalStorageContract _favoritesLocalStorge; + + @override + Future> getFavorites() async { + return _favoritesLocalStorge.getFavorites(); + } + + @override + Future saveFavorite(Restaurant restaurant) async { + return _favoritesLocalStorge.saveFavorite(restaurant); + } + + @override + Future isFavorite(Restaurant restaurant) async { + final favorites = await _favoritesLocalStorge.getFavorites(); + return favorites.contains(restaurant); + } + + @override + Future removeFavorite(Restaurant restaurant) async { + return _favoritesLocalStorge.removeFavorite(restaurant); + } +} diff --git a/lib/main.dart b/lib/main.dart index c5483f1..8681b76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/dependency_injection.dart'; import 'package:restaurant_tour/core/environment.dart'; -import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; import 'package:restaurant_tour/presentation/pages/home_page.dart'; void main() async { @@ -19,8 +20,15 @@ class RestaurantTour extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Restaurant Tour', - home: BlocProvider( - create: (context) => getIt.get(), + home: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => getIt.get(), + ), + BlocProvider( + create: (context) => getIt.get(), + ), + ], child: const HomePage(), ), ); diff --git a/lib/presentation/controllers/favorites/favorites_cubit.dart b/lib/presentation/controllers/favorites/favorites_cubit.dart new file mode 100644 index 0000000..e7f3329 --- /dev/null +++ b/lib/presentation/controllers/favorites/favorites_cubit.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; + +part 'favorites_state.dart'; + +class FavoritesCubit extends Cubit { + FavoritesCubit({required this.favoritesUsecaseContract}) + : super(FavoritesInitial()); + + final FavoritesUsecaseContract favoritesUsecaseContract; + + void getFavorites() async { + emit(FavoritesLoading()); + final favorites = await favoritesUsecaseContract.getFavorites(); + emit(FavoritesLoaded(favorites: favorites)); + } + + void saveFavorite(Restaurant restaurant) async { + await favoritesUsecaseContract.saveFavorite(restaurant); + getFavorites(); + isFavorite(restaurant); + } + + void removeFavorite(Restaurant restaurant) async { + await favoritesUsecaseContract.removeFavorite(restaurant); + getFavorites(); + } + + void isFavorite(Restaurant restaurant) async { + final isFavorite = await favoritesUsecaseContract.isFavorite(restaurant); + emit(FavoritesIsFavorite(isFavorite: isFavorite)); + } +} diff --git a/lib/presentation/controllers/favorites/favorites_state.dart b/lib/presentation/controllers/favorites/favorites_state.dart new file mode 100644 index 0000000..5bbece5 --- /dev/null +++ b/lib/presentation/controllers/favorites/favorites_state.dart @@ -0,0 +1,32 @@ +part of 'favorites_cubit.dart'; + +@immutable +sealed class FavoritesState {} + +final class FavoritesInitial extends FavoritesState {} + +final class FavoritesLoading extends FavoritesState {} + +final class FavoritesLoaded extends FavoritesState { + FavoritesLoaded({ + required this.favorites, + }); + + final List favorites; +} + +final class FavoritesIsFavorite extends FavoritesState { + FavoritesIsFavorite({ + required this.isFavorite, + }); + + final bool isFavorite; +} + +final class FavoritesError extends FavoritesState { + FavoritesError({ + required this.message, + }); + + final String message; +} diff --git a/lib/presentation/controllers/cubit/restaurants_cubit.dart b/lib/presentation/controllers/restaurants/restaurants_cubit.dart similarity index 100% rename from lib/presentation/controllers/cubit/restaurants_cubit.dart rename to lib/presentation/controllers/restaurants/restaurants_cubit.dart diff --git a/lib/presentation/controllers/cubit/restaurants_cubit_state.dart b/lib/presentation/controllers/restaurants/restaurants_cubit_state.dart similarity index 100% rename from lib/presentation/controllers/cubit/restaurants_cubit_state.dart rename to lib/presentation/controllers/restaurants/restaurants_cubit_state.dart diff --git a/lib/presentation/pages/favorites_page.dart b/lib/presentation/pages/favorites_page.dart index 925d915..3678cc4 100644 --- a/lib/presentation/pages/favorites_page.dart +++ b/lib/presentation/pages/favorites_page.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurants_list.dart'; class FavoritesPage extends StatefulWidget { const FavoritesPage({super.key}); @@ -9,33 +12,27 @@ class FavoritesPage extends StatefulWidget { class _FavoritesPageState extends State { @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Favorites'), - ElevatedButton( - child: const Text('Favorites'), - onPressed: () async { - // final yelpRepo = YelpRepository(); + void initState() { + super.initState(); + context.read().getFavorites(); + } - try { - // final result = await yelpRepo.getRestaurants(); - // if (result != null) { - // print( - // 'Fetched ${result.restaurants!.length} restaurants', - // ); - // } else { - // print('No restaurants fetched'); - // } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is FavoritesLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (state is FavoritesLoaded) { + return RestaurantsList(restaurants: state.favorites); + } + return const Center( + child: Text('Failed to fetch restaurants'), + ); + }, ); } } diff --git a/lib/presentation/pages/restaurant_details_page.dart b/lib/presentation/pages/restaurant_details_page.dart index 19a3635..72b53a2 100644 --- a/lib/presentation/pages/restaurant_details_page.dart +++ b/lib/presentation/pages/restaurant_details_page.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/dependency_injection.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurant_details/address_section.dart'; import 'package:restaurant_tour/presentation/widgets/restaurant_details/general_info_section.dart'; import 'package:restaurant_tour/presentation/widgets/restaurant_details/overall_rating_section.dart'; @@ -19,53 +22,76 @@ class _RestaurantDetailsPageState extends State { @override Widget build(BuildContext context) { final restaurant = widget.restaurant; - return Scaffold( - appBar: AppBar( - title: Text(restaurant.name ?? ''), - actions: [ - IconButton( - icon: const Icon(Icons.favorite), - onPressed: () { - // Add to favorites - }, - ), - ], - ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Hero( - key: Key(restaurant.heroImage), - tag: restaurant.heroImage, - transitionOnUserGestures: true, - child: Image.network( - restaurant.heroImage, - ), - ), - const SizedBox( - height: 24, - ), - GeneralInfoSection(restaurant: restaurant), - const SizedBox( - height: 24, + + initState() { + super.initState(); + } + + return BlocProvider( + create: (context) => getIt.get(), + child: BlocBuilder( + builder: (context, state) { + context.read().isFavorite(restaurant); + + return Scaffold( + appBar: AppBar( + title: Text(restaurant.name ?? ''), + actions: [ + IconButton( + icon: Icon( + state is FavoritesIsFavorite && state.isFavorite + ? Icons.favorite + : Icons.favorite_border_outlined, + ), + onPressed: () { + if (state is FavoritesIsFavorite && state.isFavorite) { + context.read().removeFavorite(restaurant); + return; + } + + context.read().saveFavorite(restaurant); + }, + ), + ], ), - AddressSection(restaurant: restaurant), - OverallRatingSection(restaurant: restaurant), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text( - '${restaurant.reviews?.length} Reviews', - style: AppTextStyles.openRegularTitle, - textAlign: TextAlign.start, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + key: Key(restaurant.heroImage), + tag: restaurant.heroImage, + transitionOnUserGestures: true, + child: Image.network( + restaurant.heroImage, + ), + ), + const SizedBox( + height: 24, + ), + GeneralInfoSection(restaurant: restaurant), + const SizedBox( + height: 24, + ), + AddressSection(restaurant: restaurant), + OverallRatingSection(restaurant: restaurant), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + '${restaurant.reviews?.length} Reviews', + style: AppTextStyles.openRegularTitle, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 24, + ), + ReviewsSection(reviews: restaurant.reviews ?? []), + ], ), ), - const SizedBox( - height: 24, - ), - ReviewsSection(reviews: restaurant.reviews ?? []), - ], - ), + ); + }, ), ); } diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart index b4ad623..7e8de40 100644 --- a/lib/presentation/pages/restaurants_page.dart +++ b/lib/presentation/pages/restaurants_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurants_list.dart'; class RestaurantsPage extends StatefulWidget { diff --git a/lib/presentation/widgets/restaurants_list.dart b/lib/presentation/widgets/restaurants_list.dart index 66e119e..f4936be 100644 --- a/lib/presentation/widgets/restaurants_list.dart +++ b/lib/presentation/widgets/restaurants_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/data/models/restaurant.dart'; -import 'package:restaurant_tour/presentation/controllers/cubit/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurants_card.dart'; class RestaurantsList extends StatefulWidget { diff --git a/pubspec.lock b/pubspec.lock index d55b0ae..93616ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7301d40..e37d830 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_bloc: ^8.1.6 shared_preferences: ^2.3.1 flutter_dotenv: ^5.1.0 + equatable: ^2.0.5 dev_dependencies: flutter_test: From 684c61b0dc2539ed2b65a51f3825fe92c5e3096b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sat, 14 Sep 2024 11:13:32 -0300 Subject: [PATCH 08/14] fix: Restaurant details Ui --- lib/data/local_storages/favorites_local_storage.dart | 2 +- .../local_storages/restaurants_local_storage.dart | 2 +- lib/data/repositories/yelp_repository.dart | 5 +++-- .../favorites_local_storage_contract.dart | 2 +- .../restaurants_local_storage_contract.dart | 2 +- lib/{data => domain}/models/restaurant.dart | 0 lib/{data => domain}/models/restaurant.g.dart | 0 .../repositories/yelp_repository_contract.dart | 2 +- .../favorites_usecase_contract.dart | 2 +- .../get_restaurants_usecase_contract.dart | 2 +- lib/domain/usecases/favorites_usecase.dart | 2 +- lib/domain/usecases/get_restaurants_usecase.dart | 2 +- lib/main.dart | 1 + .../controllers/favorites/favorites_cubit.dart | 12 ++++++++---- .../controllers/restaurants/restaurants_cubit.dart | 2 +- lib/presentation/pages/restaurant_details_page.dart | 4 ++-- .../widgets/restaurant_details/address_section.dart | 4 ++-- .../restaurant_details/general_info_section.dart | 2 +- .../restaurant_details/overall_rating_section.dart | 6 +++--- .../widgets/restaurant_details/reviews_section.dart | 4 ++-- lib/presentation/widgets/restaurants_card.dart | 2 +- lib/presentation/widgets/restaurants_list.dart | 2 +- 22 files changed, 34 insertions(+), 28 deletions(-) rename lib/{data => domain}/models/restaurant.dart (100%) rename lib/{data => domain}/models/restaurant.g.dart (100%) diff --git a/lib/data/local_storages/favorites_local_storage.dart b/lib/data/local_storages/favorites_local_storage.dart index cb67cf8..fb2387a 100644 --- a/lib/data/local_storages/favorites_local_storage.dart +++ b/lib/data/local_storages/favorites_local_storage.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:restaurant_tour/core/utils/storage.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; class FavoritesLocalStorage implements FavoritesLocalStorageContract { FavoritesLocalStorage({required StorageInterface storage}) diff --git a/lib/data/local_storages/restaurants_local_storage.dart b/lib/data/local_storages/restaurants_local_storage.dart index d8053a2..20f09e7 100644 --- a/lib/data/local_storages/restaurants_local_storage.dart +++ b/lib/data/local_storages/restaurants_local_storage.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:restaurant_tour/core/utils/storage.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; class RestaurantsLocalStorage implements RestaurantsLocalStorageContract { RestaurantsLocalStorage({ diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 0761e40..5afe175 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; const _apiKey = @@ -61,6 +61,7 @@ class YelpRepository extends YelpRepositoryContract { ); return RestaurantQueryResult.fromJson(response.data!['data']['search']); } on DioException catch (e) { + //Used to mock values beacuse of API limit final Map mockedResponse = { "total": 10, "business": [ @@ -437,7 +438,7 @@ class YelpRepository extends YelpRepositoryContract { }; return RestaurantQueryResult.fromJson(mockedResponse); } catch (e) { - return null; + throw Exception('Failed to get restaurants'); } } diff --git a/lib/domain/local_storages/favorites_local_storage_contract.dart b/lib/domain/local_storages/favorites_local_storage_contract.dart index c23573a..99517ea 100644 --- a/lib/domain/local_storages/favorites_local_storage_contract.dart +++ b/lib/domain/local_storages/favorites_local_storage_contract.dart @@ -1,4 +1,4 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; abstract class FavoritesLocalStorageContract { Future> getFavorites(); diff --git a/lib/domain/local_storages/restaurants_local_storage_contract.dart b/lib/domain/local_storages/restaurants_local_storage_contract.dart index 2735bef..3bf3fdf 100644 --- a/lib/domain/local_storages/restaurants_local_storage_contract.dart +++ b/lib/domain/local_storages/restaurants_local_storage_contract.dart @@ -1,4 +1,4 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; abstract class RestaurantsLocalStorageContract { Future saveRestaurants(List restaurants); diff --git a/lib/data/models/restaurant.dart b/lib/domain/models/restaurant.dart similarity index 100% rename from lib/data/models/restaurant.dart rename to lib/domain/models/restaurant.dart diff --git a/lib/data/models/restaurant.g.dart b/lib/domain/models/restaurant.g.dart similarity index 100% rename from lib/data/models/restaurant.g.dart rename to lib/domain/models/restaurant.g.dart diff --git a/lib/domain/repositories/yelp_repository_contract.dart b/lib/domain/repositories/yelp_repository_contract.dart index 6cb109c..3e8eeb8 100644 --- a/lib/domain/repositories/yelp_repository_contract.dart +++ b/lib/domain/repositories/yelp_repository_contract.dart @@ -1,4 +1,4 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; abstract class YelpRepositoryContract { Future getRestaurants({int offset = 0}); diff --git a/lib/domain/usecase_contracts/favorites_usecase_contract.dart b/lib/domain/usecase_contracts/favorites_usecase_contract.dart index 3234a3f..5d80f51 100644 --- a/lib/domain/usecase_contracts/favorites_usecase_contract.dart +++ b/lib/domain/usecase_contracts/favorites_usecase_contract.dart @@ -1,4 +1,4 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; abstract class FavoritesUsecaseContract { Future> getFavorites(); diff --git a/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart index 029dd82..eb206fd 100644 --- a/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart +++ b/lib/domain/usecase_contracts/get_restaurants_usecase_contract.dart @@ -1,4 +1,4 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; abstract class GetRestaurantsUsecaseContract { Future> getRestaurants({ diff --git a/lib/domain/usecases/favorites_usecase.dart b/lib/domain/usecases/favorites_usecase.dart index 1b29aa5..8637d18 100644 --- a/lib/domain/usecases/favorites_usecase.dart +++ b/lib/domain/usecases/favorites_usecase.dart @@ -1,5 +1,5 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; class FavoritesUsecase implements FavoritesUsecaseContract { diff --git a/lib/domain/usecases/get_restaurants_usecase.dart b/lib/domain/usecases/get_restaurants_usecase.dart index 29ee39b..b558a5b 100644 --- a/lib/domain/usecases/get_restaurants_usecase.dart +++ b/lib/domain/usecases/get_restaurants_usecase.dart @@ -1,5 +1,5 @@ -import 'package:restaurant_tour/data/models/restaurant.dart'; import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; diff --git a/lib/main.dart b/lib/main.dart index 8681b76..45bfd5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'Restaurant Tour', home: MultiBlocProvider( providers: [ diff --git a/lib/presentation/controllers/favorites/favorites_cubit.dart b/lib/presentation/controllers/favorites/favorites_cubit.dart index e7f3329..e6135e2 100644 --- a/lib/presentation/controllers/favorites/favorites_cubit.dart +++ b/lib/presentation/controllers/favorites/favorites_cubit.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; part 'favorites_state.dart'; @@ -12,9 +12,13 @@ class FavoritesCubit extends Cubit { final FavoritesUsecaseContract favoritesUsecaseContract; void getFavorites() async { - emit(FavoritesLoading()); - final favorites = await favoritesUsecaseContract.getFavorites(); - emit(FavoritesLoaded(favorites: favorites)); + try { + emit(FavoritesLoading()); + final favorites = await favoritesUsecaseContract.getFavorites(); + emit(FavoritesLoaded(favorites: favorites)); + } catch (e) { + emit(FavoritesError(message: e.toString())); + } } void saveFavorite(Restaurant restaurant) async { diff --git a/lib/presentation/controllers/restaurants/restaurants_cubit.dart b/lib/presentation/controllers/restaurants/restaurants_cubit.dart index f31dc15..69d46c4 100644 --- a/lib/presentation/controllers/restaurants/restaurants_cubit.dart +++ b/lib/presentation/controllers/restaurants/restaurants_cubit.dart @@ -1,6 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; part 'restaurants_cubit_state.dart'; diff --git a/lib/presentation/pages/restaurant_details_page.dart b/lib/presentation/pages/restaurant_details_page.dart index 72b53a2..e84be88 100644 --- a/lib/presentation/pages/restaurant_details_page.dart +++ b/lib/presentation/pages/restaurant_details_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/dependency_injection.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurant_details/address_section.dart'; import 'package:restaurant_tour/presentation/widgets/restaurant_details/general_info_section.dart'; @@ -79,7 +79,7 @@ class _RestaurantDetailsPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( '${restaurant.reviews?.length} Reviews', - style: AppTextStyles.openRegularTitle, + style: AppTextStyles.openRegularText, textAlign: TextAlign.start, ), ), diff --git a/lib/presentation/widgets/restaurant_details/address_section.dart b/lib/presentation/widgets/restaurant_details/address_section.dart index 4b7c671..35f22f5 100644 --- a/lib/presentation/widgets/restaurant_details/address_section.dart +++ b/lib/presentation/widgets/restaurant_details/address_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; class AddressSection extends StatelessWidget { const AddressSection({super.key, required this.restaurant}); @@ -27,7 +27,7 @@ class AddressSection extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( restaurant.location?.formattedAddress ?? '', - style: AppTextStyles.openRegularTitle, + style: AppTextStyles.openRegularTitleSemiBold, textAlign: TextAlign.start, ), ), diff --git a/lib/presentation/widgets/restaurant_details/general_info_section.dart b/lib/presentation/widgets/restaurant_details/general_info_section.dart index 17bb854..9d245bb 100644 --- a/lib/presentation/widgets/restaurant_details/general_info_section.dart +++ b/lib/presentation/widgets/restaurant_details/general_info_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/presentation/widgets/is_open_widget.dart'; class GeneralInfoSection extends StatelessWidget { diff --git a/lib/presentation/widgets/restaurant_details/overall_rating_section.dart b/lib/presentation/widgets/restaurant_details/overall_rating_section.dart index be48eb6..9b40202 100644 --- a/lib/presentation/widgets/restaurant_details/overall_rating_section.dart +++ b/lib/presentation/widgets/restaurant_details/overall_rating_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; class OverallRatingSection extends StatelessWidget { const OverallRatingSection({super.key, required this.restaurant}); @@ -16,7 +16,7 @@ class OverallRatingSection extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 24), child: Text( 'Overall Rating', - style: AppTextStyles.openRegularTitle, + style: AppTextStyles.openRegularText, textAlign: TextAlign.start, ), ), @@ -29,7 +29,7 @@ class OverallRatingSection extends StatelessWidget { children: [ Text( restaurant.rating.toString(), - style: AppTextStyles.openRegularTitle, + style: AppTextStyles.loraRegularHeadline, textAlign: TextAlign.start, ), const Icon( diff --git a/lib/presentation/widgets/restaurant_details/reviews_section.dart b/lib/presentation/widgets/restaurant_details/reviews_section.dart index 15354b0..58382a3 100644 --- a/lib/presentation/widgets/restaurant_details/reviews_section.dart +++ b/lib/presentation/widgets/restaurant_details/reviews_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/presentation/widgets/ratings.dart'; class ReviewsSection extends StatelessWidget { @@ -31,7 +31,7 @@ class ReviewsSection extends StatelessWidget { ), Text( review.text ?? 'description text', - style: AppTextStyles.openRegularText, + style: AppTextStyles.openRegularHeadline, ), Row( children: [ diff --git a/lib/presentation/widgets/restaurants_card.dart b/lib/presentation/widgets/restaurants_card.dart index 77b5068..b5334d3 100644 --- a/lib/presentation/widgets/restaurants_card.dart +++ b/lib/presentation/widgets/restaurants_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/utils/typography.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/presentation/pages/restaurant_details_page.dart'; import 'package:restaurant_tour/presentation/widgets/is_open_widget.dart'; import 'package:restaurant_tour/presentation/widgets/ratings.dart'; diff --git a/lib/presentation/widgets/restaurants_list.dart b/lib/presentation/widgets/restaurants_list.dart index f4936be..e1e44ea 100644 --- a/lib/presentation/widgets/restaurants_list.dart +++ b/lib/presentation/widgets/restaurants_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/data/models/restaurant.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurants_card.dart'; From dfeae633c2955018db899b7358de077c0d6f2a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sat, 14 Sep 2024 11:25:32 -0300 Subject: [PATCH 09/14] fix: Bloc provider error --- lib/presentation/pages/restaurant_details_page.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/presentation/pages/restaurant_details_page.dart b/lib/presentation/pages/restaurant_details_page.dart index e84be88..483dd56 100644 --- a/lib/presentation/pages/restaurant_details_page.dart +++ b/lib/presentation/pages/restaurant_details_page.dart @@ -23,12 +23,8 @@ class _RestaurantDetailsPageState extends State { Widget build(BuildContext context) { final restaurant = widget.restaurant; - initState() { - super.initState(); - } - - return BlocProvider( - create: (context) => getIt.get(), + return BlocProvider.value( + value: getIt.get(), child: BlocBuilder( builder: (context, state) { context.read().isFavorite(restaurant); From d337934ca3bf42e861d6c58c238d58b6b5a57683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sun, 15 Sep 2024 11:29:23 -0300 Subject: [PATCH 10/14] fix: widget rendering --- .../favorites/favorites_cubit.dart | 4 + .../favorites/favorites_state.dart | 2 + lib/presentation/pages/favorites_page.dart | 14 +++- .../widgets/restaurants_card.dart | 76 +++++++++---------- .../widgets/restaurants_list.dart | 16 ++-- pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 72 insertions(+), 49 deletions(-) diff --git a/lib/presentation/controllers/favorites/favorites_cubit.dart b/lib/presentation/controllers/favorites/favorites_cubit.dart index e6135e2..c806593 100644 --- a/lib/presentation/controllers/favorites/favorites_cubit.dart +++ b/lib/presentation/controllers/favorites/favorites_cubit.dart @@ -15,6 +15,10 @@ class FavoritesCubit extends Cubit { try { emit(FavoritesLoading()); final favorites = await favoritesUsecaseContract.getFavorites(); + if (favorites.isEmpty) { + emit(FavoritesEmpty()); + return; + } emit(FavoritesLoaded(favorites: favorites)); } catch (e) { emit(FavoritesError(message: e.toString())); diff --git a/lib/presentation/controllers/favorites/favorites_state.dart b/lib/presentation/controllers/favorites/favorites_state.dart index 5bbece5..6f34500 100644 --- a/lib/presentation/controllers/favorites/favorites_state.dart +++ b/lib/presentation/controllers/favorites/favorites_state.dart @@ -30,3 +30,5 @@ final class FavoritesError extends FavoritesState { final String message; } + +final class FavoritesEmpty extends FavoritesState {} diff --git a/lib/presentation/pages/favorites_page.dart b/lib/presentation/pages/favorites_page.dart index 3678cc4..1862307 100644 --- a/lib/presentation/pages/favorites_page.dart +++ b/lib/presentation/pages/favorites_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/utils/typography.dart'; import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; import 'package:restaurant_tour/presentation/widgets/restaurants_list.dart'; @@ -29,8 +30,19 @@ class _FavoritesPageState extends State { if (state is FavoritesLoaded) { return RestaurantsList(restaurants: state.favorites); } + if (state is FavoritesEmpty) { + return const Center( + child: Text( + 'There is no favorites restaurants', + style: AppTextStyles.loraRegularTitle, + ), + ); + } return const Center( - child: Text('Failed to fetch restaurants'), + child: Text( + 'Failed to fetch restaurants', + style: AppTextStyles.loraRegularTitle, + ), ); }, ); diff --git a/lib/presentation/widgets/restaurants_card.dart b/lib/presentation/widgets/restaurants_card.dart index b5334d3..3396cd1 100644 --- a/lib/presentation/widgets/restaurants_card.dart +++ b/lib/presentation/widgets/restaurants_card.dart @@ -46,46 +46,44 @@ class RestaurantsCard extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.all(8), - child: Expanded( - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - restaurant.name ?? '', - maxLines: 2, - style: AppTextStyles.loraRegularTitle, - ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + restaurant.name ?? '', + maxLines: 2, + style: AppTextStyles.loraRegularTitle, ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Text( - '${restaurant.price}', - style: AppTextStyles.openRegularText, - ), - const SizedBox(width: 4), - Text( - restaurant.categories?.first.alias ?? '', - style: AppTextStyles.openRegularText, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Ratings( - rating: restaurant.rating ?? 0, - ), - IsOpenWidget(isOpen: restaurant.isOpen), - ], - ), - ], - ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + '${restaurant.price}', + style: AppTextStyles.openRegularText, + ), + const SizedBox(width: 4), + Text( + restaurant.categories?.first.alias ?? '', + style: AppTextStyles.openRegularText, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Ratings( + rating: restaurant.rating ?? 0, + ), + IsOpenWidget(isOpen: restaurant.isOpen), + ], + ), + ], ), ), ), diff --git a/lib/presentation/widgets/restaurants_list.dart b/lib/presentation/widgets/restaurants_list.dart index e1e44ea..08bbe92 100644 --- a/lib/presentation/widgets/restaurants_list.dart +++ b/lib/presentation/widgets/restaurants_list.dart @@ -35,15 +35,13 @@ class _RestaurantsListState extends State { forceFetch: true, ); }, - child: Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: widget.restaurants.length, - itemBuilder: (context, index) { - final restaurant = widget.restaurants[index]; - return RestaurantsCard(restaurant: restaurant); - }, - ), + child: ListView.builder( + controller: _scrollController, + itemCount: widget.restaurants.length, + itemBuilder: (context, index) { + final restaurant = widget.restaurants[index]; + return RestaurantsCard(restaurant: restaurant); + }, ), ); } diff --git a/pubspec.lock b/pubspec.lock index 93616ef..62e0be9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -440,6 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e37d830..b5ee365 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: shared_preferences: ^2.3.1 flutter_dotenv: ^5.1.0 equatable: ^2.0.5 + mocktail: ^1.0.4 dev_dependencies: flutter_test: From 6d0ec7b8c11a3a9cd56d11cdca7f2c2667555f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sun, 15 Sep 2024 12:45:26 -0300 Subject: [PATCH 11/14] test: add base tests --- coverage/lcov.info | 670 ++++++++++++++++++ lib/presentation/pages/home_page.dart | 2 +- .../pages/restaurant_details_page.dart | 7 + .../favorites_local_storage_test.dart | 58 ++ .../restaurants_local_storage_test.dart | 46 ++ .../repositories/yelp_repository_test.dart | 37 + .../usecases/favorites_usecase_test.dart | 71 ++ .../get_restaurants_usecase_test.dart | 46 ++ test/utils/mocks.dart | 21 + test/widget_test.dart | 19 - 10 files changed, 957 insertions(+), 20 deletions(-) create mode 100644 coverage/lcov.info create mode 100644 test/data/local_storages/favorites_local_storage_test.dart create mode 100644 test/data/local_storages/restaurants_local_storage_test.dart create mode 100644 test/data/repositories/yelp_repository_test.dart create mode 100644 test/domain/usecases/favorites_usecase_test.dart create mode 100644 test/domain/usecases/get_restaurants_usecase_test.dart create mode 100644 test/utils/mocks.dart delete mode 100644 test/widget_test.dart diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..c18c52b --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,670 @@ +SF:lib/data/repositories/yelp_repository.dart +DA:9,1 +DA:55,1 +DA:58,2 +DA:60,1 +DA:62,0 +DA:63,1 +DA:65,0 +DA:67,0 +DA:68,0 +DA:73,0 +DA:76,0 +DA:77,0 +DA:82,0 +DA:83,0 +DA:87,0 +DA:88,0 +DA:93,0 +DA:99,0 +DA:104,0 +DA:110,0 +DA:114,0 +DA:121,0 +DA:125,0 +DA:130,0 +DA:133,0 +DA:134,0 +DA:139,0 +DA:140,0 +DA:144,0 +DA:145,0 +DA:149,0 +DA:155,0 +DA:158,0 +DA:159,0 +DA:164,0 +DA:165,0 +DA:169,0 +DA:170,0 +DA:174,0 +DA:181,0 +DA:185,0 +DA:191,0 +DA:194,0 +DA:195,0 +DA:200,0 +DA:201,0 +DA:205,0 +DA:206,0 +DA:210,0 +DA:217,0 +DA:221,0 +DA:227,0 +DA:230,0 +DA:231,0 +DA:236,0 +DA:237,0 +DA:241,0 +DA:242,0 +DA:246,0 +DA:253,0 +DA:257,0 +DA:263,0 +DA:266,0 +DA:267,0 +DA:272,0 +DA:273,0 +DA:277,0 +DA:278,0 +DA:282,0 +DA:289,0 +DA:293,0 +DA:299,0 +DA:302,0 +DA:303,0 +DA:308,0 +DA:309,0 +DA:313,0 +DA:314,0 +DA:318,0 +DA:325,0 +DA:329,0 +DA:335,0 +DA:338,0 +DA:339,0 +DA:344,0 +DA:345,0 +DA:349,0 +DA:350,0 +DA:354,0 +DA:361,0 +DA:365,0 +DA:371,0 +DA:374,0 +DA:375,0 +DA:380,0 +DA:381,0 +DA:385,0 +DA:386,0 +DA:390,0 +DA:397,0 +DA:401,0 +DA:407,0 +DA:410,0 +DA:411,0 +DA:416,0 +DA:417,0 +DA:421,0 +DA:422,0 +DA:426,0 +DA:433,0 +DA:439,0 +DA:441,1 +DA:445,1 +DA:479,1 +LF:114 +LH:8 +end_of_record +SF:lib/core/utils/storage.dart +DA:12,0 +DA:14,0 +DA:16,0 +DA:19,0 +DA:21,0 +DA:24,0 +DA:26,0 +LF:7 +LH:0 +end_of_record +SF:lib/domain/models/restaurant.dart +DA:11,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:27,0 +DA:31,0 +DA:33,0 +DA:43,0 +DA:49,0 +DA:51,0 +DA:61,0 +DA:68,0 +DA:70,0 +DA:78,0 +DA:82,0 +DA:83,0 +DA:85,0 +DA:100,3 +DA:112,0 +DA:113,0 +DA:115,4 +DA:118,0 +DA:119,0 +DA:120,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:156,0 +DA:161,0 +DA:162,0 +DA:164,0 +LF:39 +LH:2 +end_of_record +SF:lib/domain/models/restaurant.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:19,0 +DA:20,0 +DA:23,0 +DA:24,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:53,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:79,0 +DA:82,2 +DA:83,2 +DA:84,2 +DA:85,2 +DA:86,2 +DA:87,2 +DA:88,2 +DA:89,2 +DA:90,2 +DA:91,2 +DA:92,2 +DA:95,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:104,0 +DA:106,0 +DA:107,0 +DA:108,0 +LF:69 +LH:11 +end_of_record +SF:lib/domain/usecases/get_restaurants_usecase.dart +DA:7,1 +DA:16,1 +DA:22,2 +DA:24,1 +DA:29,2 +DA:30,0 +DA:31,3 +DA:32,1 +LF:8 +LH:7 +end_of_record +SF:lib/core/dependency_injection.dart +DA:19,3 +DA:22,0 +DA:24,0 +DA:25,0 +DA:28,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:87,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:94,0 +DA:95,0 +LF:38 +LH:1 +end_of_record +SF:lib/core/environment.dart +DA:6,0 +DA:7,0 +DA:8,0 +LF:3 +LH:0 +end_of_record +SF:lib/data/local_storages/favorites_local_storage.dart +DA:8,1 +DA:15,1 +DA:17,2 +DA:19,0 +DA:21,1 +DA:22,2 +DA:25,1 +DA:27,1 +DA:28,1 +DA:29,2 +DA:31,5 +DA:35,1 +DA:37,1 +DA:38,1 +DA:39,2 +DA:41,3 +LF:16 +LH:15 +end_of_record +SF:lib/data/local_storages/restaurants_local_storage.dart +DA:8,1 +DA:16,1 +DA:19,4 +DA:21,2 +DA:22,1 +DA:23,1 +DA:27,1 +DA:29,3 +DA:31,0 +DA:33,1 +DA:34,2 +LF:11 +LH:10 +end_of_record +SF:lib/domain/usecases/favorites_usecase.dart +DA:6,1 +DA:12,1 +DA:14,2 +DA:17,1 +DA:19,2 +DA:22,1 +DA:24,2 +DA:25,1 +DA:28,1 +DA:30,2 +LF:10 +LH:10 +end_of_record +SF:lib/presentation/controllers/favorites/favorites_state.dart +DA:11,0 +DA:19,0 +DA:27,0 +LF:3 +LH:0 +end_of_record +SF:lib/presentation/controllers/favorites/favorites_cubit.dart +DA:9,0 +DA:10,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:22,0 +DA:24,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +LF:19 +LH:0 +end_of_record +SF:lib/presentation/controllers/restaurants/restaurants_cubit.dart +DA:9,0 +DA:10,0 +DA:17,0 +DA:18,0 +DA:20,0 +DA:22,0 +DA:24,0 +DA:26,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:41,0 +LF:16 +LH:0 +end_of_record +SF:lib/presentation/controllers/restaurants/restaurants_cubit_state.dart +DA:13,0 +DA:19,0 +LF:2 +LH:0 +end_of_record +SF:lib/main.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:13,0 +DA:17,5 +DA:19,1 +DA:21,1 +DA:24,1 +DA:25,1 +DA:26,1 +DA:27,3 +DA:29,1 +DA:30,0 +LF:13 +LH:8 +end_of_record +SF:lib/presentation/pages/home_page.dart +DA:6,4 +DA:8,1 +DA:10,1 +DA:13,1 +DA:14,1 +DA:16,1 +DA:17,2 +LF:7 +LH:7 +end_of_record +SF:lib/presentation/pages/favorites_page.dart +DA:8,4 +DA:10,0 +DA:11,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:30,0 +DA:31,0 +DA:33,0 +LF:13 +LH:1 +end_of_record +SF:lib/presentation/widgets/restaurants_list.dart +DA:8,0 +DA:12,0 +DA:13,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +LF:19 +LH:0 +end_of_record +SF:lib/presentation/pages/restaurants_page.dart +DA:7,4 +DA:9,1 +DA:10,1 +DA:14,1 +DA:16,1 +DA:17,2 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:29,0 +DA:30,0 +LF:12 +LH:6 +end_of_record +SF:lib/presentation/pages/restaurant_details_page.dart +DA:13,0 +DA:17,0 +DA:18,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:55,0 +DA:60,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:75,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:84,0 +DA:92,0 +LF:39 +LH:0 +end_of_record +SF:lib/presentation/widgets/restaurant_details/address_section.dart +DA:6,0 +DA:10,0 +DA:12,0 +DA:14,0 +DA:26,0 +DA:28,0 +DA:29,0 +LF:7 +LH:0 +end_of_record +SF:lib/presentation/widgets/restaurant_details/general_info_section.dart +DA:7,0 +DA:11,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:27,0 +DA:28,0 +DA:33,0 +LF:14 +LH:0 +end_of_record +SF:lib/presentation/widgets/restaurant_details/overall_rating_section.dart +DA:6,0 +DA:10,0 +DA:12,0 +DA:14,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +LF:9 +LH:0 +end_of_record +SF:lib/presentation/widgets/restaurant_details/reviews_section.dart +DA:7,0 +DA:11,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +LF:20 +LH:0 +end_of_record +SF:lib/presentation/widgets/is_open_widget.dart +DA:5,0 +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:18,0 +DA:21,0 +DA:22,0 +LF:9 +LH:0 +end_of_record +SF:lib/presentation/widgets/ratings.dart +DA:4,0 +DA:7,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +LF:6 +LH:0 +end_of_record +SF:lib/presentation/widgets/restaurants_card.dart +DA:9,0 +DA:13,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:70,0 +DA:71,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:83,0 +LF:39 +LH:0 +end_of_record diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index 143ea98..5845b5a 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -9,7 +9,7 @@ class HomePage extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 2, - initialIndex: 0, + // initialIndex: 0, child: Scaffold( appBar: AppBar( title: const Text('Restaurant Tour'), diff --git a/lib/presentation/pages/restaurant_details_page.dart b/lib/presentation/pages/restaurant_details_page.dart index 483dd56..4b7961e 100644 --- a/lib/presentation/pages/restaurant_details_page.dart +++ b/lib/presentation/pages/restaurant_details_page.dart @@ -19,6 +19,13 @@ class RestaurantDetailsPage extends StatefulWidget { } class _RestaurantDetailsPageState extends State { + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + context.read().getFavorites(); + } + @override Widget build(BuildContext context) { final restaurant = widget.restaurant; diff --git a/test/data/local_storages/favorites_local_storage_test.dart b/test/data/local_storages/favorites_local_storage_test.dart new file mode 100644 index 0000000..ba90be4 --- /dev/null +++ b/test/data/local_storages/favorites_local_storage_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/local_storages/favorites_local_storage.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late FavoritesLocalStorageContract favoritesLocalStorage; + late StorageInterface storage; + + setUp(() { + storage = MockStorage(); + favoritesLocalStorage = FavoritesLocalStorage(storage: storage); + }); + + group( + FavoritesLocalStorage, + () { + test('Should get a list of restaurants', () async { + //Arrange + + when(() => storage.read(any())).thenAnswer((_) async => '[]'); + //Act + final result = await favoritesLocalStorage.getFavorites(); + //Assert + expect(result, []); + verify(() => storage.read('favorites')).called(1); + }); + test('Should save a restaurants', () async { + //Arrange + const Restaurant restaurant = Restaurant(); + when(() => storage.write(any(), any())).thenAnswer((_) async {}); + when(() => storage.read(any())).thenAnswer((_) async => '[]'); + + //Act + await favoritesLocalStorage.saveFavorite(restaurant); + //Assert + + verify(() => storage.write('favorites', any())).called(1); + }); + test('Should remove a restaurants', () async { + //Arrange + const Restaurant restaurant = Restaurant(); + when(() => storage.write(any(), any())).thenAnswer((_) async {}); + when(() => storage.read(any())).thenAnswer((_) async => '[]'); + + //Act + await favoritesLocalStorage.removeFavorite(restaurant); + //Assert + + verify(() => storage.write('favorites', any())).called(1); + }); + }, + ); +} diff --git a/test/data/local_storages/restaurants_local_storage_test.dart b/test/data/local_storages/restaurants_local_storage_test.dart new file mode 100644 index 0000000..b8e9758 --- /dev/null +++ b/test/data/local_storages/restaurants_local_storage_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/data/local_storages/restaurants_local_storage.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late RestaurantsLocalStorageContract restaurantsLocalStorage; + late StorageInterface storage; + + setUp(() { + storage = MockStorage(); + restaurantsLocalStorage = RestaurantsLocalStorage(localStorage: storage); + }); + + group( + RestaurantsLocalStorage, + () { + test('Should get a list of restaurants', () async { + //Arrange + + when(() => storage.read(any())).thenAnswer((_) async => '[]'); + //Act + final result = await restaurantsLocalStorage.getCachedRestaurants(); + //Assert + expect(result, []); + verify(() => storage.read('restaurants')).called(1); + }); + test('Should save a restaurants', () async { + //Arrange + const restaurant = Restaurant(); + when(() => storage.write(any(), any())).thenAnswer((_) async {}); + when(() => storage.read(any())).thenAnswer((_) async => '[]'); + + //Act + await restaurantsLocalStorage.saveRestaurants([restaurant]); + //Assert + + verify(() => storage.write('restaurants', any())).called(1); + }); + }, + ); +} diff --git a/test/data/repositories/yelp_repository_test.dart b/test/data/repositories/yelp_repository_test.dart new file mode 100644 index 0000000..ec2bd73 --- /dev/null +++ b/test/data/repositories/yelp_repository_test.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late YelpRepositoryContract yelpRepository; + late Dio dio; + + setUp(() { + dio = MockDio(); + yelpRepository = YelpRepository(dio: dio); + }); + + group( + YelpRepository, + () { + test('Should get a list of restaurants', () async { + //Arrange + when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer( + (_) async => Response( + data: {}, + requestOptions: RequestOptions(), + ), + ); + //Act + final result = await yelpRepository.getRestaurants(); + //Assert + expect(result, []); + verify(() => dio.get(any())).called(1); + }); + }, + ); +} diff --git a/test/domain/usecases/favorites_usecase_test.dart b/test/domain/usecases/favorites_usecase_test.dart new file mode 100644 index 0000000..f3f3b87 --- /dev/null +++ b/test/domain/usecases/favorites_usecase_test.dart @@ -0,0 +1,71 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; +import 'package:restaurant_tour/domain/usecases/favorites_usecase.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late FavoritesUsecaseContract favoritesUsecaseContract; + late FavoritesLocalStorageContract favoritesLocalStorageContract; + + setUp(() { + registerFallbackValue(FakeRestaurant()); + favoritesLocalStorageContract = MockFavoritesLocalStorage(); + favoritesUsecaseContract = FavoritesUsecase( + favoritesLocalStorge: favoritesLocalStorageContract, + ); + }); + + group( + 'FavoritesUsecase', + () { + test('Should get a list of restaurants', () async { + //Arrange + when(() => favoritesLocalStorageContract.getFavorites()) + .thenAnswer((_) async => []); + //Act + await favoritesUsecaseContract.getFavorites(); + //Assert + verify(() => favoritesLocalStorageContract.getFavorites()).called(1); + }); + test('Should save a restaurants', () async { + //Arrange + when(() => favoritesLocalStorageContract.getFavorites()) + .thenAnswer((_) async => []); + when(() => favoritesLocalStorageContract.saveFavorite(any())) + .thenAnswer((_) async {}); + const restaurant = Restaurant(); + //Act + await favoritesUsecaseContract.saveFavorite(restaurant); + //Assert + verify(() => favoritesLocalStorageContract.saveFavorite(restaurant)) + .called(1); + }); + test('Should check if a restaurant is favorite', () async { + //Arrange + when(() => favoritesLocalStorageContract.getFavorites()) + .thenAnswer((_) async => []); + const restaurant = Restaurant(); + //Act + await favoritesUsecaseContract.isFavorite(restaurant); + //Assert + verify(() => favoritesLocalStorageContract.getFavorites()).called(1); + }); + test('Should remove a restaurants', () async { + //Arrange + when(() => favoritesLocalStorageContract.removeFavorite(any())) + .thenAnswer((_) async {}); + const restaurant = Restaurant(); + + //Act + await favoritesUsecaseContract.removeFavorite(restaurant); + //Assert + verify(() => favoritesLocalStorageContract.removeFavorite(restaurant)) + .called(1); + }); + }, + ); +} diff --git a/test/domain/usecases/get_restaurants_usecase_test.dart b/test/domain/usecases/get_restaurants_usecase_test.dart new file mode 100644 index 0000000..b9bcc04 --- /dev/null +++ b/test/domain/usecases/get_restaurants_usecase_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; +import 'package:restaurant_tour/domain/usecases/get_restaurants_usecase.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late GetRestaurantsUsecaseContract getRestaurantsUsecaseContract; + late RestaurantsLocalStorageContract restaurantsLocalStorageContract; + late YelpRepositoryContract yelpRepositoryContract; + + setUp(() { + registerFallbackValue(FakeRestaurant()); + restaurantsLocalStorageContract = MockRestaurantsLocalStorageContract(); + yelpRepositoryContract = MockYelpRepository(); + getRestaurantsUsecaseContract = GetRestaurantsUsecase( + yelpRepositoryContract: yelpRepositoryContract, + restaurantsLocalStorageContract: restaurantsLocalStorageContract, + ); + }); + + group(GetRestaurantsUsecase, () { + test('Should get a list of restaurants', () async { + //Arrange + when(() => restaurantsLocalStorageContract.getCachedRestaurants()) + .thenAnswer((_) async => []); + when( + () => restaurantsLocalStorageContract.saveRestaurants( + any(), + ), + ).thenAnswer((_) async => []); + when(() => yelpRepositoryContract.getRestaurants(offset: 0)) + .thenAnswer((_) async => null); + + //Act + await getRestaurantsUsecaseContract.getRestaurants(); + //Assert + verify(() => restaurantsLocalStorageContract.getCachedRestaurants()) + .called(1); + // verify(() => yelpRepositoryContract.getRestaurants(offset: 0)).called(1); + }); + }); +} diff --git a/test/utils/mocks.dart b/test/utils/mocks.dart new file mode 100644 index 0000000..8361aaf --- /dev/null +++ b/test/utils/mocks.dart @@ -0,0 +1,21 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/utils/storage.dart'; +import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; + +class MockStorage extends Mock implements Storage {} + +class MockDio extends Mock implements Dio {} + +class MockFavoritesLocalStorage extends Mock + implements FavoritesLocalStorageContract {} + +class FakeRestaurant extends Fake implements Restaurant {} + +class MockYelpRepository extends Mock implements YelpRepositoryContract {} + +class MockRestaurantsLocalStorageContract extends Mock + implements RestaurantsLocalStorageContract {} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From 7944d44b357e8c4ec158fd3bdc1ef181a1d77cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sun, 15 Sep 2024 18:04:18 -0300 Subject: [PATCH 12/14] test: add missing tests --- lib/data/repositories/yelp_repository.dart | 377 ----------------- .../repositories/yelp_repository_test.dart | 16 +- .../favorites/favorites_cubit_test.dart | 68 ++++ .../restaurants/restaurants_cubit_test.dart | 43 ++ test/utils/mocked_response.dart | 380 ++++++++++++++++++ test/utils/mocks.dart | 8 + 6 files changed, 510 insertions(+), 382 deletions(-) create mode 100644 test/presentation/controllers/favorites/favorites_cubit_test.dart create mode 100644 test/presentation/controllers/restaurants/restaurants_cubit_test.dart create mode 100644 test/utils/mocked_response.dart diff --git a/lib/data/repositories/yelp_repository.dart b/lib/data/repositories/yelp_repository.dart index 5afe175..d9f222d 100644 --- a/lib/data/repositories/yelp_repository.dart +++ b/lib/data/repositories/yelp_repository.dart @@ -60,383 +60,6 @@ class YelpRepository extends YelpRepositoryContract { data: _getQuery(offset), ); return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } on DioException catch (e) { - //Used to mock values beacuse of API limit - final Map mockedResponse = { - "total": 10, - "business": [ - { - "id": "1", - "name": "Restaurant Name Goes Here And Wraps 2 Lines One", - "price": "\$", - "rating": 4.5, - "photos": [ - "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", - ], - "categories": [ - { - "alias": "italian", - "title": "Italian", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r1", - "rating": 5, - "text": - "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", - "user": { - "id": "u1", - "image_url": "https://example.com/user1.jpg", - "name": "John Doe", - }, - }, - { - "id": "r2", - "rating": 2, - "text": - "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", - "user": { - "id": "u1", - "image_url": "https://example.com/user1.jpg", - "name": "John Doe", - }, - }, - { - "id": "r3", - "rating": 4, - "text": "Amazing food and atmosphere!", - "user": { - "id": "u1", - "image_url": "https://example.com/user1.jpg", - "name": "John Doe", - }, - } - ], - "location": { - "formatted_address": "123 Main St, New York, NY", - }, - }, - { - "id": "2", - "name": "Restaurant Name Goes Here And Wraps 2 Lines Two", - "price": "\$", - "rating": 4.0, - "photos": [ - "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", - ], - "categories": [ - { - "alias": "mexican", - "title": "Mexican", - } - ], - "hours": [ - { - "is_open_now": false, - } - ], - "reviews": [], - "location": { - "formatted_address": "456 Broadway, Los Angeles, CA", - }, - }, - { - "id": "3", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Three", - "price": "\$", - "rating": 3.5, - "photos": [ - "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", - ], - "categories": [ - { - "alias": "chinese", - "title": "Chinese", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r3", - "rating": 3, - "text": "Average experience, nothing special.", - "user": { - "id": "u3", - "image_url": "https://example.com/user3.jpg", - "name": "Alice Brown", - }, - } - ], - "location": { - "formatted_address": "789 Market St, San Francisco, CA", - }, - }, - { - "id": "4", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Four", - "price": "\$", - "rating": 4.8, - "photos": [ - "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", - ], - "categories": [ - { - "alias": "french", - "title": "French", - } - ], - "hours": [ - { - "is_open_now": false, - } - ], - "reviews": [ - { - "id": "r4", - "rating": 5, - "text": "Exquisite dining experience.", - "user": { - "id": "u4", - "image_url": "https://example.com/user4.jpg", - "name": "Charlie Green", - }, - } - ], - "location": { - "formatted_address": "321 Park Ave, Boston, MA", - }, - }, - { - "id": "5", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Five", - "price": "\$", - "rating": 4.2, - "photos": [ - "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", - ], - "categories": [ - { - "alias": "indian", - "title": "Indian", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r5", - "rating": 4, - "text": "Authentic Indian cuisine, loved it.", - "user": { - "id": "u5", - "image_url": "https://example.com/user5.jpg", - "name": "David Lee", - }, - } - ], - "location": { - "formatted_address": "654 Elm St, Chicago, IL", - }, - }, - { - "id": "6", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Six", - "price": "\$", - "rating": 4.6, - "photos": [ - "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", - ], - "categories": [ - { - "alias": "japanese", - "title": "Japanese", - } - ], - "hours": [ - { - "is_open_now": false, - } - ], - "reviews": [ - { - "id": "r6", - "rating": 5, - "text": "Amazing sushi and service.", - "user": { - "id": "u6", - "image_url": "https://example.com/user6.jpg", - "name": "Emily White", - }, - } - ], - "location": { - "formatted_address": "987 Sunset Blvd, Miami, FL", - }, - }, - { - "id": "7", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Seven", - "price": "\$", - "rating": 4.3, - "photos": [ - "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", - ], - "categories": [ - { - "alias": "thai", - "title": "Thai", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r7", - "rating": 4, - "text": "Great flavors, but service was slow.", - "user": { - "id": "u7", - "image_url": "https://example.com/user7.jpg", - "name": "Michael Brown", - }, - } - ], - "location": { - "formatted_address": "852 Ocean Ave, Seattle, WA", - }, - }, - { - "id": "8", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Eight", - "price": "\$", - "rating": 3.9, - "photos": [ - "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", - ], - "categories": [ - { - "alias": "burger", - "title": "Burger", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r8", - "rating": 4, - "text": "Great burgers for a reasonable price.", - "user": { - "id": "u8", - "image_url": "https://example.com/user8.jpg", - "name": "Jessica Johnson", - }, - } - ], - "location": { - "formatted_address": "753 Central St, Austin, TX", - }, - }, - { - "id": "9", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Nine", - "price": "\$", - "rating": 4.9, - "photos": [ - "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", - ], - "categories": [ - { - "alias": "steakhouse", - "title": "Steakhouse", - } - ], - "hours": [ - { - "is_open_now": false, - } - ], - "reviews": [ - { - "id": "r9", - "rating": 5, - "text": "Best steakhouse in town.", - "user": { - "id": "u9", - "image_url": "https://example.com/user9.jpg", - "name": "Chris Blue", - }, - } - ], - "location": { - "formatted_address": "369 High St, Dallas, TX", - }, - }, - { - "id": "10", - "name": - "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Ten", - "price": "\$\$", - "rating": 3.8, - "photos": [ - "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", - ], - "categories": [ - { - "alias": "pizza", - "title": "Pizza", - } - ], - "hours": [ - { - "is_open_now": true, - } - ], - "reviews": [ - { - "id": "r10", - "rating": 3, - "text": "Good pizza, but nothing special.", - "user": { - "id": "u10", - "image_url": "https://example.com/user10.jpg", - "name": "Tom Green", - }, - } - ], - "location": { - "formatted_address": "258 Oak St, Orlando, FL", - }, - } - ], - }; - return RestaurantQueryResult.fromJson(mockedResponse); } catch (e) { throw Exception('Failed to get restaurants'); } diff --git a/test/data/repositories/yelp_repository_test.dart b/test/data/repositories/yelp_repository_test.dart index ec2bd73..efb858f 100644 --- a/test/data/repositories/yelp_repository_test.dart +++ b/test/data/repositories/yelp_repository_test.dart @@ -2,8 +2,10 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/data/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import '../../utils/mocked_response.dart'; import '../../utils/mocks.dart'; void main() { @@ -18,19 +20,23 @@ void main() { group( YelpRepository, () { - test('Should get a list of restaurants', () async { + test('Should get a list of restaurants with success', () async { //Arrange - when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer( + when( + () => dio.post>(any(), data: any(named: 'data')), + ).thenAnswer( (_) async => Response( - data: {}, + data: mockedApiResponse, requestOptions: RequestOptions(), ), ); //Act final result = await yelpRepository.getRestaurants(); //Assert - expect(result, []); - verify(() => dio.get(any())).called(1); + expect(result, isA()); + verify( + () => dio.post>(any(), data: any(named: 'data')), + ).called(1); }); }, ); diff --git a/test/presentation/controllers/favorites/favorites_cubit_test.dart b/test/presentation/controllers/favorites/favorites_cubit_test.dart new file mode 100644 index 0000000..cb5bcd2 --- /dev/null +++ b/test/presentation/controllers/favorites/favorites_cubit_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; +import 'package:restaurant_tour/presentation/controllers/favorites/favorites_cubit.dart'; + +import '../../../utils/mocks.dart'; + +void main() { + late FavoritesCubit favoritesCubit; + late FavoritesUsecaseContract favoritesUsecaseContract; + + setUp(() { + registerFallbackValue(FakeRestaurant()); + + favoritesUsecaseContract = MockFavoritesUsecaseContract(); + favoritesCubit = + FavoritesCubit(favoritesUsecaseContract: favoritesUsecaseContract); + }); + + group(FavoritesCubit, () { + test('Should get favorites with success', () async { + //Arrange + when(() => favoritesUsecaseContract.getFavorites()) + .thenAnswer((_) async => []); + //Act + favoritesCubit.getFavorites(); + //Assert + verify(() => favoritesUsecaseContract.getFavorites()).called(1); + }); + + test('Should save favorite with success', () async { + //Arrange + when(() => favoritesUsecaseContract.saveFavorite(any())) + .thenAnswer((_) async {}); + when(() => favoritesUsecaseContract.getFavorites()) + .thenAnswer((_) async => []); + when(() => favoritesUsecaseContract.isFavorite(any())) + .thenAnswer((_) async => true); + + //Act + favoritesCubit.saveFavorite(FakeRestaurant()); + //Assert + verify(() => favoritesUsecaseContract.saveFavorite(any())).called(1); + }); + + test('Should remove favorite with success', () async { + //Arrange + when(() => favoritesUsecaseContract.removeFavorite(any())) + .thenAnswer((_) async {}); + //Act + favoritesCubit.removeFavorite(FakeRestaurant()); + //Assert + await untilCalled(() => favoritesUsecaseContract.removeFavorite(any())); + verify(() => favoritesUsecaseContract.removeFavorite(any())).called(1); + }); + + test('Should check if restaurant is favorite with success', () async { + //Arrange + when(() => favoritesUsecaseContract.isFavorite(any())) + .thenAnswer((_) async => true); + //Act + favoritesCubit.isFavorite(FakeRestaurant()); + //Assert + await untilCalled(() => favoritesUsecaseContract.isFavorite(any())); + verify(() => favoritesUsecaseContract.isFavorite(any())).called(1); + }); + }); +} diff --git a/test/presentation/controllers/restaurants/restaurants_cubit_test.dart b/test/presentation/controllers/restaurants/restaurants_cubit_test.dart new file mode 100644 index 0000000..fde58b9 --- /dev/null +++ b/test/presentation/controllers/restaurants/restaurants_cubit_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; + +import '../../../utils/mocks.dart'; + +void main() { + late RestaurantsCubit restaurantsCubit; + late GetRestaurantsUsecaseContract getRestaurantsUsecaseContract; + + setUp(() { + getRestaurantsUsecaseContract = MockGetRestaurantsUsecaseContract(); + restaurantsCubit = RestaurantsCubit( + getRestaurantsUsecaseContract: getRestaurantsUsecaseContract); + }); + + group(RestaurantsCubit, () { + test('Should get restaurants with success', () async { + //Arrange + when(() => getRestaurantsUsecaseContract.getRestaurants( + forceFetch: any(named: 'forceFetch'), + offset: any(named: 'offset'))).thenAnswer((_) async => []); + //Act + restaurantsCubit.getRestaurants(); + //Assert + verify(() => getRestaurantsUsecaseContract.getRestaurants( + forceFetch: false, offset: 0)).called(1); + }); + + test('Should get next page with success', () async { + //Arrange + when(() => getRestaurantsUsecaseContract.getRestaurants( + forceFetch: any(named: 'forceFetch'), + offset: any(named: 'offset'))).thenAnswer((_) async => []); + //Act + restaurantsCubit.getNextPage(); + //Assert + verify(() => getRestaurantsUsecaseContract.getRestaurants( + forceFetch: true, offset: 20)).called(1); + }); + }); +} diff --git a/test/utils/mocked_response.dart b/test/utils/mocked_response.dart new file mode 100644 index 0000000..4d5eec4 --- /dev/null +++ b/test/utils/mocked_response.dart @@ -0,0 +1,380 @@ +final Map mockedResponse = { + "total": 10, + "business": [ + { + "id": "1", + "name": "Restaurant Name Goes Here And Wraps 2 Lines One", + "price": "\$", + "rating": 4.5, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "italian", + "title": "Italian", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r1", + "rating": 5, + "text": + "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", + "user": { + "id": "u1", + "image_url": "https://example.com/user1.jpg", + "name": "John Doe", + }, + }, + { + "id": "r2", + "rating": 2, + "text": + "Review text goes here. Review text goes here. This is a review. This is a review that is 3 lines long.", + "user": { + "id": "u1", + "image_url": "https://example.com/user1.jpg", + "name": "John Doe", + }, + }, + { + "id": "r3", + "rating": 4, + "text": "Amazing food and atmosphere!", + "user": { + "id": "u1", + "image_url": "https://example.com/user1.jpg", + "name": "John Doe", + }, + } + ], + "location": { + "formatted_address": "123 Main St, New York, NY", + }, + }, + { + "id": "2", + "name": "Restaurant Name Goes Here And Wraps 2 Lines Two", + "price": "\$", + "rating": 4.0, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "mexican", + "title": "Mexican", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [], + "location": { + "formatted_address": "456 Broadway, Los Angeles, CA", + }, + }, + { + "id": "3", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Three", + "price": "\$", + "rating": 3.5, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "chinese", + "title": "Chinese", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r3", + "rating": 3, + "text": "Average experience, nothing special.", + "user": { + "id": "u3", + "image_url": "https://example.com/user3.jpg", + "name": "Alice Brown", + }, + } + ], + "location": { + "formatted_address": "789 Market St, San Francisco, CA", + }, + }, + { + "id": "4", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Four", + "price": "\$", + "rating": 4.8, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "french", + "title": "French", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r4", + "rating": 5, + "text": "Exquisite dining experience.", + "user": { + "id": "u4", + "image_url": "https://example.com/user4.jpg", + "name": "Charlie Green", + }, + } + ], + "location": { + "formatted_address": "321 Park Ave, Boston, MA", + }, + }, + { + "id": "5", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Five", + "price": "\$", + "rating": 4.2, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "indian", + "title": "Indian", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r5", + "rating": 4, + "text": "Authentic Indian cuisine, loved it.", + "user": { + "id": "u5", + "image_url": "https://example.com/user5.jpg", + "name": "David Lee", + }, + } + ], + "location": { + "formatted_address": "654 Elm St, Chicago, IL", + }, + }, + { + "id": "6", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Six", + "price": "\$", + "rating": 4.6, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "japanese", + "title": "Japanese", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r6", + "rating": 5, + "text": "Amazing sushi and service.", + "user": { + "id": "u6", + "image_url": "https://example.com/user6.jpg", + "name": "Emily White", + }, + } + ], + "location": { + "formatted_address": "987 Sunset Blvd, Miami, FL", + }, + }, + { + "id": "7", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Seven", + "price": "\$", + "rating": 4.3, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "thai", + "title": "Thai", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r7", + "rating": 4, + "text": "Great flavors, but service was slow.", + "user": { + "id": "u7", + "image_url": "https://example.com/user7.jpg", + "name": "Michael Brown", + }, + } + ], + "location": { + "formatted_address": "852 Ocean Ave, Seattle, WA", + }, + }, + { + "id": "8", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Eight", + "price": "\$", + "rating": 3.9, + "photos": [ + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQMh9wiF1vxFv2KBN2QWkxbC1RekcuVeDKFdw&s", + ], + "categories": [ + { + "alias": "burger", + "title": "Burger", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r8", + "rating": 4, + "text": "Great burgers for a reasonable price.", + "user": { + "id": "u8", + "image_url": "https://example.com/user8.jpg", + "name": "Jessica Johnson", + }, + } + ], + "location": { + "formatted_address": "753 Central St, Austin, TX", + }, + }, + { + "id": "9", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Nine", + "price": "\$", + "rating": 4.9, + "photos": [ + "https://media.cntraveler.com/photos/654bd5e13892537a8ded0947/16:9/w_2560%2Cc_limit/phy2023.din.oss.Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines-lr.jpg", + ], + "categories": [ + { + "alias": "steakhouse", + "title": "Steakhouse", + } + ], + "hours": [ + { + "is_open_now": false, + } + ], + "reviews": [ + { + "id": "r9", + "rating": 5, + "text": "Best steakhouse in town.", + "user": { + "id": "u9", + "image_url": "https://example.com/user9.jpg", + "name": "Chris Blue", + }, + } + ], + "location": { + "formatted_address": "369 High St, Dallas, TX", + }, + }, + { + "id": "10", + "name": + "Restaurant Name Goes Here And Wraps 2 Lines Name Goes Here And Wraps 2 Lines Ten", + "price": "\$\$", + "rating": 3.8, + "photos": [ + "https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg", + ], + "categories": [ + { + "alias": "pizza", + "title": "Pizza", + } + ], + "hours": [ + { + "is_open_now": true, + } + ], + "reviews": [ + { + "id": "r10", + "rating": 3, + "text": "Good pizza, but nothing special.", + "user": { + "id": "u10", + "image_url": "https://example.com/user10.jpg", + "name": "Tom Green", + }, + } + ], + "location": { + "formatted_address": "258 Oak St, Orlando, FL", + }, + } + ], +}; + +final Map mockedApiResponse = { + "data": { + "search": mockedResponse, + }, +}; diff --git a/test/utils/mocks.dart b/test/utils/mocks.dart index 8361aaf..69d4f7f 100644 --- a/test/utils/mocks.dart +++ b/test/utils/mocks.dart @@ -5,6 +5,8 @@ import 'package:restaurant_tour/domain/local_storages/favorites_local_storage_co import 'package:restaurant_tour/domain/local_storages/restaurants_local_storage_contract.dart'; import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; +import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; class MockStorage extends Mock implements Storage {} @@ -19,3 +21,9 @@ class MockYelpRepository extends Mock implements YelpRepositoryContract {} class MockRestaurantsLocalStorageContract extends Mock implements RestaurantsLocalStorageContract {} + +class MockFavoritesUsecaseContract extends Mock + implements FavoritesUsecaseContract {} + +class MockGetRestaurantsUsecaseContract extends Mock + implements GetRestaurantsUsecaseContract {} From 001d528f258414391239698f127b91159010b162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sun, 15 Sep 2024 18:07:52 -0300 Subject: [PATCH 13/14] fix: my favorites rendering --- lib/core/dependency_injection.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/dependency_injection.dart b/lib/core/dependency_injection.dart index 5b7498d..b3723b2 100644 --- a/lib/core/dependency_injection.dart +++ b/lib/core/dependency_injection.dart @@ -77,7 +77,7 @@ class DependencyInjection { ), ); - getIt.registerLazySingleton( + getIt.registerFactory( () => FavoritesCubit( favoritesUsecaseContract: getIt.get(), ), From 537d382ca4b6a6a63e8bc5fb18e9f9e1d8c58300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Reis=20Alves=20-=20JAV?= Date: Sun, 15 Sep 2024 19:15:40 -0300 Subject: [PATCH 14/14] test: add restaurant page widget test --- pubspec.lock | 100 +++++++++-- pubspec.yaml | 1 + .../pages/restaurants_page_test.dart | 160 ++++++++++++++++++ .../pages/widgets/restaurant_card_test.dart | 0 test/utils/mocks.dart | 5 + 5 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 test/presentation/pages/restaurants_page_test.dart create mode 100644 test/presentation/pages/widgets/restaurant_card_test.dart diff --git a/pubspec.lock b/pubspec.lock index 62e0be9..46d38af 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -177,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + 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 main" description: @@ -456,6 +480,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" package_config: dependency: transitive description: @@ -624,14 +656,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -653,6 +701,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: @@ -701,6 +765,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -709,6 +781,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" timing: dependency: transitive description: @@ -781,22 +861,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - web_socket: + web_socket_channel: dependency: transitive description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "0.1.6" - web_socket_channel: + version: "2.4.0" + webkit_inspection_protocol: dependency: transitive description: - name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "1.2.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b5ee365..969c07e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: flutter_dotenv: ^5.1.0 equatable: ^2.0.5 mocktail: ^1.0.4 + bloc_test: ^9.1.7 dev_dependencies: flutter_test: diff --git a/test/presentation/pages/restaurants_page_test.dart b/test/presentation/pages/restaurants_page_test.dart new file mode 100644 index 0000000..ca4ebbc --- /dev/null +++ b/test/presentation/pages/restaurants_page_test.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/domain/models/restaurant.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; +import 'package:restaurant_tour/presentation/pages/restaurants_page.dart'; +import 'package:restaurant_tour/presentation/widgets/restaurants_list.dart'; + +import '../../utils/mocks.dart'; + +void main() { + late MockRestaurantsCubit mockRestaurantsCubit; + + setUp(() { + mockRestaurantsCubit = MockRestaurantsCubit(); + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('should render RestaurantPage', (WidgetTester tester) async { + when(() => mockRestaurantsCubit.state) + .thenReturn(RestaurantsCubitLoading()); + when(() => mockRestaurantsCubit.getRestaurants()).thenAnswer((_) async {}); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider( + create: (_) => mockRestaurantsCubit, + child: const RestaurantsPage(), + ), + ), + ); + + final resturantPage = find.byType(RestaurantsPage); + + // Expect CircularProgressIndicator while loading + expect(resturantPage, findsOneWidget); + }); + testWidgets('RestaurantsPage shows loading indicator while loading', + (WidgetTester tester) async { + when(() => mockRestaurantsCubit.state) + .thenReturn(RestaurantsCubitLoading()); + when(() => mockRestaurantsCubit.getRestaurants()).thenAnswer((_) async {}); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider( + create: (_) => mockRestaurantsCubit, + child: const RestaurantsPage(), + ), + ), + ); + + final progressIndicator = find.byType(CircularProgressIndicator); + + expect(progressIndicator, findsOneWidget); + }); + + testWidgets('RestaurantsPage shows list when restaurants are loaded', + (WidgetTester tester) async { + when(() => mockRestaurantsCubit.state).thenReturn( + RestaurantsCubitLoaded( + [ + Restaurant( + name: 'Test Restaurant 1', + id: 'id1', + categories: [ + Category(title: 'Test Category 1'), + ], + photos: const [ + 'https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg' + ], + hours: const [ + Hours(isOpenNow: true), + ], + location: Location( + formattedAddress: 'Test Address 1', + ), + price: 'PRice', + rating: 4.5, + reviews: const [ + Review( + id: 'id1', + rating: 4, + text: 'Test Review 1', + user: User( + id: 'id1', + name: 'Test User 1', + imageUrl: + 'https://i.natgeofe.com/n/04cf2a79-4a49-45eb-90f8-38356167690d/image00037.jpeg', + ), + ), + ], + ), + Restaurant( + name: 'Test Restaurant 2', + id: 'id1', + categories: [ + Category(title: 'Test Category 2'), + ], + photos: const ['test.jpg'], + hours: const [ + Hours(isOpenNow: true), + ], + location: Location( + formattedAddress: 'Test Address 2', + ), + price: 'PRice', + rating: 4.5, + reviews: const [ + Review( + id: 'id1', + rating: 4, + text: 'Test Review 2', + user: User( + id: 'id1', + name: 'Test User 2', + imageUrl: 'test.jpg', + ), + ), + ], + ), + ], + ), + ); + when(() => mockRestaurantsCubit.getRestaurants()).thenAnswer((_) async {}); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BlocProvider( + create: (_) => mockRestaurantsCubit, + child: const RestaurantsPage(), + ), + ), + ), + ); + + expect(find.byType(RestaurantsList), findsOneWidget); + }); + + testWidgets('RestaurantsPage shows error message on failure', + (WidgetTester tester) async { + // Mock the failure state + when(() => mockRestaurantsCubit.state) + .thenReturn(RestaurantsCubitError('')); + when(() => mockRestaurantsCubit.getRestaurants()).thenAnswer((_) async {}); + + await tester.pumpWidget( + MaterialApp( + home: BlocProvider( + create: (_) => mockRestaurantsCubit, + child: const RestaurantsPage(), + ), + ), + ); + + expect(find.text('Failed to fetch restaurants'), findsOneWidget); + }); +} diff --git a/test/presentation/pages/widgets/restaurant_card_test.dart b/test/presentation/pages/widgets/restaurant_card_test.dart new file mode 100644 index 0000000..e69de29 diff --git a/test/utils/mocks.dart b/test/utils/mocks.dart index 69d4f7f..9c4e0a5 100644 --- a/test/utils/mocks.dart +++ b/test/utils/mocks.dart @@ -1,3 +1,4 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/core/utils/storage.dart'; @@ -7,6 +8,7 @@ import 'package:restaurant_tour/domain/models/restaurant.dart'; import 'package:restaurant_tour/domain/repositories/yelp_repository_contract.dart'; import 'package:restaurant_tour/domain/usecase_contracts/favorites_usecase_contract.dart'; import 'package:restaurant_tour/domain/usecase_contracts/get_restaurants_usecase_contract.dart'; +import 'package:restaurant_tour/presentation/controllers/restaurants/restaurants_cubit.dart'; class MockStorage extends Mock implements Storage {} @@ -27,3 +29,6 @@ class MockFavoritesUsecaseContract extends Mock class MockGetRestaurantsUsecaseContract extends Mock implements GetRestaurantsUsecaseContract {} + +class MockRestaurantsCubit extends MockCubit + implements RestaurantsCubit {}