├── .github └── workflows │ └── main.yml ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── movies_app │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ ├── Raleway-Bold.ttf │ ├── Raleway-Light.ttf │ ├── Raleway-Medium.ttf │ └── Raleway-Regular.ttf └── images │ ├── placeholder-female.png │ ├── placeholder-male.png │ └── placeholder-unknown.png ├── coverage └── lcov.info ├── coverage_badge.svg ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── core │ ├── configs │ │ ├── configs.dart │ │ └── styles │ │ │ ├── app_colors.dart │ │ │ ├── app_text_styles.dart │ │ │ ├── app_themes.dart │ │ │ └── ui_constants.dart │ ├── exceptions │ │ └── http_exception.dart │ ├── models │ │ ├── cache_response.dart │ │ └── paginated_response.dart │ ├── services │ │ ├── http │ │ │ ├── dio-interceptors │ │ │ │ └── cache_interceptor.dart │ │ │ ├── dio_http_service.dart │ │ │ ├── http_service.dart │ │ │ └── http_service_provider.dart │ │ ├── media │ │ │ └── media_service.dart │ │ └── storage │ │ │ ├── hive_storage_service.dart │ │ │ ├── storage_service.dart │ │ │ └── storage_service_provider.dart │ └── widgets │ │ ├── app_bar_leading.dart │ │ ├── app_cached_network_image.dart │ │ ├── app_loader.dart │ │ ├── error_view.dart │ │ ├── grid_shimmer.dart │ │ ├── list_item_shimmer.dart │ │ └── shimmer.dart ├── features │ ├── media │ │ ├── enums │ │ │ └── media_type.dart │ │ └── models │ │ │ └── media.dart │ ├── people │ │ ├── enums │ │ │ └── gender.dart │ │ ├── models │ │ │ ├── person.dart │ │ │ └── person_image.dart │ │ ├── providers │ │ │ ├── current_popular_person_provider.dart │ │ │ ├── paginated_popular_people_provider.dart │ │ │ ├── person_details_provider.dart │ │ │ ├── person_images_provider.dart │ │ │ ├── popular_people_count_provider.dart │ │ │ └── popular_people_list_scroll_controller_provider.dart │ │ ├── repositories │ │ │ ├── http_people_repository.dart │ │ │ └── people_repository.dart │ │ └── views │ │ │ ├── pages │ │ │ ├── person_details_page.dart │ │ │ ├── person_images_slider_page.dart │ │ │ └── popular_people_page.dart │ │ │ └── widgets │ │ │ ├── person_avatar.dart │ │ │ ├── person_bio.dart │ │ │ ├── person_cover.dart │ │ │ ├── person_details_sliver_app_bar.dart │ │ │ ├── person_images.dart │ │ │ ├── person_images_grid.dart │ │ │ ├── person_images_grid_item.dart │ │ │ ├── person_info.dart │ │ │ ├── person_media.dart │ │ │ ├── person_name.dart │ │ │ ├── popular_people_app_bar.dart │ │ │ ├── popular_people_list.dart │ │ │ ├── popular_person_list_item.dart │ │ │ ├── save_image_slider_action.dart │ │ │ └── slider_action.dart │ └── tmdb-configs │ │ ├── enums │ │ └── image_size.dart │ │ ├── models │ │ ├── tmdb_configs.dart │ │ └── tmdb_image_configs.dart │ │ ├── providers │ │ └── tmdb_configs_provider.dart │ │ └── repositories │ │ ├── http_tmdb_configs_repository.dart │ │ └── tmdb_configs_repository.dart ├── main.dart └── movies_app.dart ├── macos ├── Flutter │ ├── GeneratedPluginRegistrant.swift │ └── ephemeral │ │ ├── Flutter-Generated.xcconfig │ │ └── flutter_export_environment.sh ├── Podfile ├── Pods │ ├── FMDB │ │ ├── LICENSE.txt │ │ ├── README.markdown │ │ └── src │ │ │ └── fmdb │ │ │ ├── FMDB.h │ │ │ ├── FMDatabase.h │ │ │ ├── FMDatabase.m │ │ │ ├── FMDatabaseAdditions.h │ │ │ ├── FMDatabaseAdditions.m │ │ │ ├── FMDatabasePool.h │ │ │ ├── FMDatabasePool.m │ │ │ ├── FMDatabaseQueue.h │ │ │ ├── FMDatabaseQueue.m │ │ │ ├── FMResultSet.h │ │ │ └── FMResultSet.m │ ├── Local Podspecs │ │ ├── FlutterMacOS.podspec.json │ │ ├── path_provider_macos.podspec.json │ │ ├── sqflite.podspec.json │ │ └── url_launcher_macos.podspec.json │ ├── Pods.xcodeproj │ │ ├── project.pbxproj │ │ └── xcuserdata │ │ │ └── roaakhaddam.xcuserdatad │ │ │ └── xcschemes │ │ │ ├── FMDB.xcscheme │ │ │ ├── FlutterMacOS.xcscheme │ │ │ ├── Pods-Runner.xcscheme │ │ │ ├── path_provider_macos.xcscheme │ │ │ ├── sqflite.xcscheme │ │ │ ├── url_launcher_macos.xcscheme │ │ │ └── xcschememanagement.plist │ └── Target Support Files │ │ ├── FMDB │ │ ├── FMDB-Info.plist │ │ ├── FMDB-dummy.m │ │ ├── FMDB-prefix.pch │ │ ├── FMDB-umbrella.h │ │ ├── FMDB.debug.xcconfig │ │ ├── FMDB.modulemap │ │ └── FMDB.release.xcconfig │ │ ├── FlutterMacOS │ │ ├── FlutterMacOS.debug.xcconfig │ │ └── FlutterMacOS.release.xcconfig │ │ ├── Pods-Runner │ │ ├── Pods-Runner-Info.plist │ │ ├── Pods-Runner-acknowledgements.markdown │ │ ├── Pods-Runner-acknowledgements.plist │ │ ├── Pods-Runner-dummy.m │ │ ├── Pods-Runner-frameworks-Debug-input-files.xcfilelist │ │ ├── Pods-Runner-frameworks-Debug-output-files.xcfilelist │ │ ├── Pods-Runner-frameworks-Profile-input-files.xcfilelist │ │ ├── Pods-Runner-frameworks-Profile-output-files.xcfilelist │ │ ├── Pods-Runner-frameworks-Release-input-files.xcfilelist │ │ ├── Pods-Runner-frameworks-Release-output-files.xcfilelist │ │ ├── Pods-Runner-frameworks.sh │ │ ├── Pods-Runner-umbrella.h │ │ ├── Pods-Runner.debug.xcconfig │ │ ├── Pods-Runner.modulemap │ │ ├── Pods-Runner.profile.xcconfig │ │ └── Pods-Runner.release.xcconfig │ │ ├── path_provider_macos │ │ ├── path_provider_macos-Info.plist │ │ ├── path_provider_macos-dummy.m │ │ ├── path_provider_macos-prefix.pch │ │ ├── path_provider_macos-umbrella.h │ │ ├── path_provider_macos.debug.xcconfig │ │ ├── path_provider_macos.modulemap │ │ └── path_provider_macos.release.xcconfig │ │ ├── sqflite │ │ ├── sqflite-Info.plist │ │ ├── sqflite-dummy.m │ │ ├── sqflite-prefix.pch │ │ ├── sqflite-umbrella.h │ │ ├── sqflite.debug.xcconfig │ │ ├── sqflite.modulemap │ │ └── sqflite.release.xcconfig │ │ └── url_launcher_macos │ │ ├── url_launcher_macos-Info.plist │ │ ├── url_launcher_macos-dummy.m │ │ ├── url_launcher_macos-prefix.pch │ │ ├── url_launcher_macos-umbrella.h │ │ ├── url_launcher_macos.debug.xcconfig │ │ ├── url_launcher_macos.modulemap │ │ └── url_launcher_macos.release.xcconfig ├── Runner.xcodeproj │ └── xcuserdata │ │ └── roaakhaddam.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── Runner.xcworkspace │ └── xcuserdata │ └── roaakhaddam.xcuserdatad │ └── UserInterfaceState.xcuserstate ├── pubspec.yaml └── test ├── core ├── services │ ├── http │ │ ├── dio-intercepters │ │ │ └── caching_interceptor_test.dart │ │ ├── dio_http_service_test.dart │ │ └── http_service_provider_test.dart │ ├── media │ │ └── media_service_test.dart │ └── storage │ │ ├── hive_storage_service_test.dart │ │ └── storage_service_provider_test.dart └── widgets │ ├── app_bar_leading_test.dart │ └── app_cached_network_image_test.dart ├── features ├── media │ └── models │ │ └── media_test.dart ├── people │ ├── enums │ │ └── gender_test.dart │ ├── models │ │ ├── person_image_test.dart │ │ └── person_test.dart │ ├── providers │ │ ├── current_popular_person_provider_test.dart │ │ ├── paginated_popular_people_provider_test.dart │ │ ├── person_details_provider_test.dart │ │ ├── person_images_provider_test.dart │ │ ├── popular_people_count_provider_test.dart │ │ └── popular_people_scroll_controller_provider.dart │ ├── repositories │ │ ├── http_people_repository_test.dart │ │ └── people_repository_test.dart │ └── views │ │ ├── pages │ │ ├── person_details_page_test.dart │ │ ├── person_images_slider_page_test.dart │ │ └── popular_people_page_test.dart │ │ └── widgets │ │ ├── person_avatar_test.dart │ │ ├── person_cover_test.dart │ │ ├── person_images_grid_item_test.dart │ │ ├── person_images_grid_test.dart │ │ ├── person_images_test.dart │ │ ├── person_media_test.dart │ │ ├── popular_people_list_test.dart │ │ ├── popular_person_list_item_test.dart │ │ └── save_image_slider_action_test.dart └── tmdb-configs │ ├── models │ └── tmdb_image_configs_test.dart │ ├── providers │ └── tmdb_configs_provider_test.dart │ └── repositories │ ├── http_tmdb_configs_repository_test.dart │ └── tmdb_configs_repository_test.dart ├── movies_app_test.dart └── test-utils ├── dummy-data ├── dummy_configs.dart ├── dummy_people.dart └── json │ ├── example_configuration_response.json │ ├── example_person_details_response.json │ ├── example_person_images_response.json │ └── example_popular_people_response.json ├── golden_test_utils.dart ├── mocks.dart └── pump_app.dart /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Flutter Test 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "main" branch 8 | push: 9 | branches: [ "main" ] 10 | pull_request: 11 | branches: [ "main" ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-java@v1 23 | with: 24 | java-version: '12.x' 25 | - uses: subosito/flutter-action@v1 26 | with: 27 | channel: 'stable' 28 | 29 | - name: Get all Flutter Packages 30 | run: flutter pub get 31 | 32 | - name: Run Flutter Test 33 | run: flutter test --update-goldens 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | pubspec.lock 49 | /coverage/html/ 50 | 51 | .fvm 52 | .vscode 53 | 54 | # Goldens 55 | /test/**/goldens/**/*.* 56 | /test/**/failures/**/*.* 57 | 58 | #mason 59 | .mason/ 60 | mason-lock.json -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f1875d570e39de09040c8f79aa13cc56baab8db1 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 17 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 18 | - platform: android 19 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 20 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 21 | - platform: ios 22 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 23 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_single_quotes: true 6 | always_use_package_imports: true 7 | flutter_style_todos: false 8 | avoid_redundant_argument_values: false 9 | sort_pub_dependencies: false 10 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.movies_app" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion 23 51 | targetSdkVersion 33 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/movies_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.movies_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/fonts/Raleway-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/fonts/Raleway-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/fonts/Raleway-Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/fonts/Raleway-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /assets/images/placeholder-female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/images/placeholder-female.png -------------------------------------------------------------------------------- /assets/images/placeholder-male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/images/placeholder-male.png -------------------------------------------------------------------------------- /assets/images/placeholder-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/assets/images/placeholder-unknown.png -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | 21 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | Podfile.lock -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Movies App 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | movies_app 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | NSPhotoLibraryUsageDescription 49 | This app requires access to the photo library in order to upload images to your device. 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/core/configs/configs.dart: -------------------------------------------------------------------------------- 1 | /// App level configuration variables 2 | class Configs { 3 | /// The max allowed age duration for the http cache 4 | static const Duration maxCacheAge = Duration(hours: 1); 5 | 6 | /// Key used in dio options to indicate whether 7 | /// cache should be force refreshed 8 | static const String dioCacheForceRefreshKey = 'dio_cache_force_refresh_key'; 9 | 10 | /// Base API URL of The TMDB API 11 | /// 12 | /// See: https://developers.themoviedb.org/3/getting-started/introduction 13 | static const String apiBaseUrl = 'https://api.themoviedb.org/3'; 14 | 15 | /// API Key registered with The TMDB API 16 | /// 17 | /// See: https://developers.themoviedb.org/3/getting-started/introduction 18 | static const String tmdbAPIKey = String.fromEnvironment('TMDB_API_KEY'); 19 | } 20 | -------------------------------------------------------------------------------- /lib/core/configs/styles/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Styles class holding app color information 4 | /// and helper methods 5 | class AppColors { 6 | /// App primary color 7 | static const Color primary = Color(0xff50C9FF); 8 | 9 | /// App secondary color 10 | static const Color secondary = Color(0xffFC698C); 11 | 12 | /// App black color 13 | static const Color black = Color(0xff141414); 14 | 15 | /// App white color 16 | static const Color white = Color(0xffffffff); 17 | 18 | /// Returns a shade of a [Color] from a double value 19 | /// 20 | /// The 'darker' boolean determines whether the shade 21 | /// should be .1 darker or lighter than the provided color 22 | static Color getShade(Color color, {bool darker = false, double value = .1}) { 23 | assert(value >= 0 && value <= 1, 'shade values must be between 0 and 1'); 24 | 25 | final hsl = HSLColor.fromColor(color); 26 | final hslDark = hsl.withLightness( 27 | (darker ? (hsl.lightness - value) : (hsl.lightness + value)) 28 | .clamp(0.0, 1.0), 29 | ); 30 | 31 | return hslDark.toColor(); 32 | } 33 | 34 | /// Returns a [MaterialColor] from a [Color] object 35 | static MaterialColor getMaterialColorFromColor(Color color) { 36 | final colorShades = { 37 | 50: getShade(color, value: 0.5), 38 | 100: getShade(color, value: 0.4), 39 | 200: getShade(color, value: 0.3), 40 | 300: getShade(color, value: 0.2), 41 | 400: getShade(color, value: 0.1), 42 | 500: color, //Primary value 43 | 600: getShade(color, value: 0.1, darker: true), 44 | 700: getShade(color, value: 0.15, darker: true), 45 | 800: getShade(color, value: 0.2, darker: true), 46 | 900: getShade(color, value: 0.25, darker: true), 47 | }; 48 | return MaterialColor(color.value, colorShades); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/core/configs/styles/app_text_styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Styles class holding app text styles 4 | class AppTextStyles { 5 | /// Font family string 6 | static const String fontFamily = 'Raleway'; 7 | 8 | /// Text style for large body text 9 | static const TextStyle bodyLg = TextStyle( 10 | fontSize: 16, 11 | fontWeight: FontWeight.w300, 12 | ); 13 | 14 | /// Text style for body text 15 | static const TextStyle body = TextStyle( 16 | fontSize: 14, 17 | fontWeight: FontWeight.w400, 18 | ); 19 | 20 | /// Text style for small body text 21 | static const TextStyle bodySm = TextStyle( 22 | fontSize: 12, 23 | fontWeight: FontWeight.w300, 24 | ); 25 | 26 | /// Text style for extra small body text 27 | static const TextStyle bodyXs = TextStyle( 28 | fontSize: 10, 29 | fontWeight: FontWeight.w300, 30 | ); 31 | 32 | /// Text style for heading 1 text 33 | static const TextStyle h1 = TextStyle( 34 | fontSize: 35, 35 | fontWeight: FontWeight.w700, 36 | ); 37 | 38 | /// Text style for heading 2 text 39 | static const TextStyle h2 = TextStyle( 40 | fontSize: 25, 41 | fontWeight: FontWeight.w400, 42 | ); 43 | 44 | /// Text style for heading 3 text 45 | static const TextStyle h3 = TextStyle( 46 | fontSize: 20, 47 | fontWeight: FontWeight.w400, 48 | ); 49 | 50 | /// Text style for heading 4 text 51 | static const TextStyle h4 = TextStyle( 52 | fontSize: 18, 53 | fontWeight: FontWeight.w400, 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /lib/core/configs/styles/ui_constants.dart: -------------------------------------------------------------------------------- 1 | /// Class holding constants used in UI code 2 | class UIConstants { 3 | /// Height of a list item in a popular people list 4 | static const double personListItemHeight = 110; 5 | } 6 | -------------------------------------------------------------------------------- /lib/core/exceptions/http_exception.dart: -------------------------------------------------------------------------------- 1 | /// Custom exception used with Http requests 2 | class HttpException implements Exception { 3 | /// Creates a new instance of [HttpException] 4 | HttpException({ 5 | this.title, 6 | this.message, 7 | this.statusCode, 8 | }); 9 | 10 | /// Exception title 11 | final String? title; 12 | 13 | /// Exception message 14 | final String? message; 15 | 16 | /// Exception http response code 17 | final int? statusCode; 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/models/cache_response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:clock/clock.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:movies_app/core/configs/configs.dart'; 6 | 7 | /// Model for the response returned from HTTP Cache 8 | class CachedResponse { 9 | /// Creates an instance of [CachedResponse] 10 | CachedResponse({ 11 | required this.data, 12 | required this.age, 13 | required this.statusCode, 14 | required this.headers, 15 | }); 16 | 17 | /// Creates an instance of [CachedResponse] parsed raw data 18 | factory CachedResponse.fromJson(Map data) { 19 | return CachedResponse( 20 | data: data['data'], 21 | age: DateTime.parse(data['age'] as String), 22 | statusCode: data['statusCode'] as int, 23 | headers: Headers.fromMap( 24 | Map>.from( 25 | json.decode(json.encode(data['headers'])) as Map, 26 | ).map( 27 | (k, v) => MapEntry(k, List.from(v)), 28 | ), 29 | ), 30 | ); 31 | } 32 | 33 | /// The data inside the cached response 34 | final dynamic data; 35 | 36 | /// The age of the cached response 37 | /// 38 | /// This is used to determine whether the cache has expired or not 39 | /// based on the [Configs.maxCacheAge] value 40 | /// 41 | /// see [isValid] 42 | final DateTime age; 43 | 44 | /// The http status code of the cached http response 45 | final int statusCode; 46 | 47 | /// The cached http response headers 48 | final Headers headers; 49 | 50 | /// Determines if a cached response has expired 51 | /// 52 | /// A cached response is expired when its [age] is older 53 | /// than the [Configs.maxCacheAge] 54 | bool get isValid => clock.now().isBefore(age.add(Configs.maxCacheAge)); 55 | 56 | /// Converts data to json Map 57 | Map toJson() { 58 | return { 59 | 'data': data, 60 | 'age': age.toString(), 61 | 'statusCode': statusCode, 62 | 'headers': headers.map, 63 | }; 64 | } 65 | 66 | /// Builds a dio response from a [RequestOptions] object 67 | Response buildResponse(RequestOptions options) { 68 | return Response( 69 | data: data, 70 | headers: headers, 71 | requestOptions: options.copyWith(extra: options.extra), 72 | statusCode: statusCode, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/core/models/paginated_response.dart: -------------------------------------------------------------------------------- 1 | /// Model representing a paginated TMDB http response 2 | class PaginatedResponse { 3 | /// Creates new instance of [PaginatedResponse] 4 | PaginatedResponse({ 5 | this.page = 1, 6 | this.results = const [], 7 | this.totalPages = 1, 8 | this.totalResults = 1, 9 | }); 10 | 11 | /// Creates new instance of [PaginatedResponse] parsed from raw dara 12 | factory PaginatedResponse.fromJson( 13 | Map json, { 14 | required List results, 15 | }) { 16 | return PaginatedResponse( 17 | page: json['page'] as int, 18 | results: results, 19 | totalPages: json['total_pages'] as int, 20 | totalResults: json['total_results'] as int, 21 | ); 22 | } 23 | 24 | /// Page number 25 | final int page; 26 | 27 | /// List of results of the current page 28 | final List results; 29 | 30 | /// Total number of pages 31 | final int totalPages; 32 | 33 | /// Total number of results in all pages 34 | final int totalResults; 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/services/http/http_service.dart: -------------------------------------------------------------------------------- 1 | /// Http Service Interface 2 | abstract class HttpService { 3 | /// Http base url 4 | String get baseUrl; 5 | 6 | /// Http headers 7 | Map get headers; 8 | 9 | /// Http get request 10 | Future> get( 11 | String endpoint, { 12 | Map? queryParameters, 13 | bool forceRefresh = false, 14 | }); 15 | 16 | /// Http post request 17 | Future post( 18 | String endpoint, { 19 | Map? queryParameters, 20 | }); 21 | 22 | /// Http put request 23 | Future put(); 24 | 25 | /// Http delete request 26 | Future delete(); 27 | } 28 | -------------------------------------------------------------------------------- /lib/core/services/http/http_service_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/services/http/dio_http_service.dart'; 3 | import 'package:movies_app/core/services/http/http_service.dart'; 4 | import 'package:movies_app/core/services/storage/storage_service_provider.dart'; 5 | 6 | /// Provider that maps an [HttpService] interface to implementation 7 | final httpServiceProvider = Provider((ref) { 8 | final storageService = ref.watch(storageServiceProvider); 9 | 10 | return DioHttpService(storageService); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/core/services/media/media_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:gallery_saver/gallery_saver.dart'; 5 | 6 | /// Provider that maps an [MediaService] interface to implementation 7 | final mediaServiceProvider = Provider( 8 | (_) => GallerySaverMediaService(), 9 | ); 10 | 11 | /// Interface for a media service 12 | abstract class MediaService { 13 | /// Saves a network image to device gallery 14 | Future saveNetworkImageToGallery(String imageUrl); 15 | 16 | /// Saves a file image to device gallery 17 | Future saveFileImageToGallery(File file); 18 | } 19 | 20 | /// [MediaService] interface implementation using the [GallerySaver] package 21 | /// 22 | /// See: https://pub.dev/packages/gallery_saver 23 | class GallerySaverMediaService implements MediaService { 24 | // coverage:ignore-start 25 | @override 26 | Future saveNetworkImageToGallery(String imageUrl) async { 27 | await GallerySaver.saveImage(imageUrl); 28 | } 29 | 30 | @override 31 | Future saveFileImageToGallery(File file) { 32 | // TODO: implement saveFileImageToGallery 33 | throw UnimplementedError(); 34 | } // coverage:ignore-end 35 | } 36 | -------------------------------------------------------------------------------- /lib/core/services/storage/hive_storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:hive_flutter/hive_flutter.dart'; 2 | import 'package:movies_app/core/services/storage/storage_service.dart'; 3 | 4 | /// [StorageService] interface implementation using the Hive package 5 | /// 6 | /// See: https://pub.dev/packages/hive_flutter 7 | class HiveStorageService implements StorageService { 8 | /// A Hive Box 9 | late Box hiveBox; 10 | 11 | /// Opens a Hive box by its name 12 | Future openBox([String boxName = 'MOVES_APP']) async { 13 | hiveBox = await Hive.openBox(boxName); 14 | } 15 | 16 | @override 17 | Future init() async { 18 | await openBox(); 19 | } 20 | 21 | @override 22 | Future remove(String key) async { 23 | await hiveBox.delete(key); 24 | } 25 | 26 | @override 27 | dynamic get(String key) { 28 | return hiveBox.get(key); 29 | } 30 | 31 | @override 32 | dynamic getAll() { 33 | return hiveBox.values.toList(); 34 | } 35 | 36 | @override 37 | bool has(String key) { 38 | return hiveBox.containsKey(key); 39 | } 40 | 41 | @override 42 | Future set(String? key, dynamic data) async { 43 | await hiveBox.put(key, data); 44 | } 45 | 46 | @override 47 | Future clear() async { 48 | await hiveBox.clear(); 49 | } 50 | 51 | @override 52 | Future close() async { 53 | await hiveBox.close(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/core/services/storage/storage_service.dart: -------------------------------------------------------------------------------- 1 | /// Storage service interface 2 | abstract class StorageService { 3 | /// Initializes service 4 | Future init(); 5 | 6 | /// Removes item from storage by a key 7 | Future remove(String key); 8 | 9 | /// Retrieves item from storage by a key 10 | dynamic get(String key); 11 | 12 | /// Retrieves all items from storage 13 | dynamic getAll(); 14 | 15 | /// Clears storage 16 | Future clear(); 17 | 18 | /// Checks if an item exists in storage by a key 19 | bool has(String key); 20 | 21 | /// Sets an item data in storage by a key 22 | Future set(String? key, dynamic data); 23 | 24 | /// Terminates service 25 | Future close(); 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/services/storage/storage_service_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/services/storage/hive_storage_service.dart'; 3 | import 'package:movies_app/core/services/storage/storage_service.dart'; 4 | 5 | /// Provider that locates an [StorageService] interface to implementation 6 | final storageServiceProvider = Provider( 7 | (_) => HiveStorageService(), 8 | ); 9 | -------------------------------------------------------------------------------- /lib/core/widgets/app_bar_leading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 3 | 4 | /// Widget with a default AppBar leading icon and action 5 | class AppBarLeading extends StatefulWidget { 6 | /// Creates a new instance of [AppBarLeading] 7 | const AppBarLeading({super.key}); 8 | 9 | @override 10 | State createState() => _AppBarLeadingState(); 11 | } 12 | 13 | class _AppBarLeadingState extends State 14 | with SingleTickerProviderStateMixin { 15 | late final AnimationController animationController; 16 | late final Animation fadeAnimation; 17 | 18 | @override 19 | void initState() { 20 | animationController = AnimationController( 21 | vsync: this, 22 | duration: const Duration(milliseconds: 600), 23 | )..forward(); 24 | 25 | fadeAnimation = Tween(begin: 0, end: 1).animate( 26 | CurvedAnimation( 27 | parent: animationController, 28 | curve: const Interval(0.5, 1, curve: Curves.easeInOut), 29 | ), 30 | ); 31 | super.initState(); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | animationController.dispose(); 37 | super.dispose(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return FadeTransition( 43 | opacity: fadeAnimation, 44 | child: Center( 45 | child: ClipOval( 46 | child: Material( 47 | color: AppColors.primary, 48 | child: InkWell( 49 | onTap: () => Navigator.of(context).pop(), 50 | child: const Padding( 51 | padding: EdgeInsets.all(12), 52 | child: Icon(Icons.arrow_back), 53 | ), 54 | ), 55 | ), 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/core/widgets/app_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Default loading widget with an adaptive [CircularProgressIndicator] 4 | class AppLoader extends StatelessWidget { 5 | /// Creates a new instance of [AppLoader] 6 | const AppLoader({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Center( 11 | child: CircularProgressIndicator.adaptive( 12 | valueColor: 13 | AlwaysStoppedAnimation(Theme.of(context).primaryColor), 14 | backgroundColor: Theme.of(context).colorScheme.secondary, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/widgets/error_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Default error view widget 4 | class ErrorView extends StatelessWidget { 5 | /// Creates a new instance of [ErrorView] 6 | const ErrorView({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return const Center( 11 | child: Icon(Icons.error, size: 40), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/widgets/grid_shimmer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/widgets/shimmer.dart'; 3 | 4 | /// Widget used for a Grid shimmer effect 5 | class GridShimmer extends StatelessWidget { 6 | /// Creates a new instance of [GridShimmer] 7 | const GridShimmer({ 8 | super.key, 9 | this.minOpacity = 0.05, 10 | this.maxOpacity = 0.1, 11 | }); 12 | 13 | /// Minimum opacity of the shimmer effect 14 | final double minOpacity; 15 | 16 | /// Maximum opacity of the shimmer effect 17 | final double maxOpacity; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final itemHeight = 22 | (MediaQuery.of(context).size.width - 17 * 2 - 10 * 2) / 3; 23 | 24 | return SizedBox( 25 | height: itemHeight * 3 + 10 * 4, 26 | child: GridView.count( 27 | physics: const NeverScrollableScrollPhysics(), 28 | padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), 29 | crossAxisCount: 3, 30 | mainAxisSpacing: 10, 31 | crossAxisSpacing: 10, 32 | children: List.generate( 33 | 9, 34 | (index) => Shimmer( 35 | height: itemHeight, 36 | minOpacity: minOpacity, 37 | maxOpacity: maxOpacity, 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/core/widgets/list_item_shimmer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/widgets/shimmer.dart'; 3 | 4 | /// Widget used for a list shimmer effect 5 | class ListItemShimmer extends StatelessWidget { 6 | /// Creates a new instance of [ListItemShimmer] 7 | const ListItemShimmer({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Padding( 12 | padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), 13 | child: Row( 14 | children: [ 15 | const Expanded( 16 | flex: 1, 17 | child: Shimmer(height: 90), 18 | ), 19 | const SizedBox(width: 20), 20 | Expanded( 21 | flex: 3, 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | const Shimmer(height: 15), 26 | const SizedBox(height: 20), 27 | Shimmer( 28 | height: 15, 29 | width: MediaQuery.of(context).size.width * 0.4, 30 | ), 31 | ], 32 | ), 33 | ), 34 | ], 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/core/widgets/shimmer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 3 | 4 | /// Shimmer widget with simple fade animation 5 | class Shimmer extends StatefulWidget { 6 | /// Creates a new instance of [Shimmer] 7 | const Shimmer({ 8 | super.key, 9 | this.width, 10 | this.height, 11 | this.minOpacity = 0.05, 12 | this.maxOpacity = 0.1, 13 | }); 14 | 15 | /// Shimmer area width 16 | final double? width; 17 | 18 | /// Shimmer area height 19 | final double? height; 20 | 21 | /// Shimmer fade minimum opacity 22 | final double minOpacity; 23 | 24 | /// Shimmer fade maximum opacity 25 | final double maxOpacity; 26 | 27 | @override 28 | State createState() => _ShimmerState(); 29 | } 30 | 31 | class _ShimmerState extends State with SingleTickerProviderStateMixin { 32 | late final AnimationController animationController; 33 | 34 | @override 35 | void initState() { 36 | animationController = AnimationController( 37 | vsync: this, 38 | duration: const Duration(milliseconds: 1000), 39 | lowerBound: widget.minOpacity, 40 | upperBound: widget.maxOpacity, 41 | ); 42 | animationController.repeat(reverse: true); 43 | super.initState(); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | animationController.dispose(); 49 | super.dispose(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return FadeTransition( 55 | opacity: animationController, 56 | child: Container( 57 | width: widget.width, 58 | height: widget.height, 59 | decoration: BoxDecoration( 60 | color: AppColors.white, 61 | borderRadius: BorderRadius.circular(10), 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/features/media/enums/media_type.dart: -------------------------------------------------------------------------------- 1 | /// Enum for a person's media type options 2 | /// 3 | /// See: https://developers.themoviedb.org/3/people/get-popular-people 4 | enum MediaType { 5 | /// An unknown media type 6 | unknown, 7 | 8 | /// A media of type TV 9 | tv, 10 | 11 | /// A media of type Movie 12 | movie; 13 | } 14 | -------------------------------------------------------------------------------- /lib/features/people/enums/gender.dart: -------------------------------------------------------------------------------- 1 | /// Enum to indicated male, female, or unknown person gender 2 | enum Gender { 3 | /// Female gender value 4 | female, 5 | 6 | /// Male gender value 7 | male, 8 | 9 | /// Unknown gender value 10 | unknown; 11 | 12 | /// Integer value from gender value based on the TMDB response 13 | int get toInt { 14 | switch (this) { 15 | case Gender.unknown: 16 | return 0; 17 | case Gender.female: 18 | return 1; 19 | case Gender.male: 20 | return 2; 21 | } 22 | } 23 | 24 | /// Gender value from integer value based on the TMDB response 25 | static Gender fromInt(int integer) { 26 | switch (integer) { 27 | case 1: 28 | return Gender.female; 29 | case 2: 30 | return Gender.male; 31 | default: 32 | return Gender.unknown; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/features/people/providers/current_popular_person_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/features/people/models/person.dart'; 3 | 4 | /// The provider that provides the Person data for each list item 5 | /// 6 | /// Initially it throws an UnimplementedError because we won't use it 7 | /// before overriding it 8 | /// 9 | /// For infinite scroll tutorial: 10 | /// https://github.com/Roaa94/movies_app/tree/main#infinite-scroll-functionality 11 | final currentPopularPersonProvider = Provider>((ref) { 12 | throw UnimplementedError(); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/features/people/providers/paginated_popular_people_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/models/paginated_response.dart'; 3 | import 'package:movies_app/features/people/models/person.dart'; 4 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 5 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 6 | 7 | /// FutureProvider that fetches paginated popular people 8 | /// 9 | /// See: 10 | /// https://developers.themoviedb.org/3/people/get-popular-people 11 | /// For infinite scroll tutorial: 12 | /// https://github.com/Roaa94/movies_app/tree/main#infinite-scroll-functionality 13 | final paginatedPopularPeopleProvider = 14 | FutureProvider.family, int>( 15 | (ref, int pageIndex) async { 16 | final peopleRepository = ref.watch(peopleRepositoryProvider); 17 | final tmdbConfigs = await ref.watch(tmdbConfigsProvider.future); 18 | 19 | return peopleRepository.getPopularPeople( 20 | page: pageIndex + 1, 21 | imageConfigs: tmdbConfigs.images, 22 | ); 23 | }, 24 | ); 25 | -------------------------------------------------------------------------------- /lib/features/people/providers/person_details_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/features/people/models/person.dart'; 3 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 4 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 5 | 6 | /// Future provider that fetches a person's details 7 | /// from the person's id 8 | /// 9 | /// See: https://developers.themoviedb.org/3/people/get-person-details 10 | final personDetailsProvider = FutureProvider.family( 11 | (ref, personId) async { 12 | final peopleRepository = ref.watch(peopleRepositoryProvider); 13 | final tmdbConfigs = await ref.watch(tmdbConfigsProvider.future); 14 | 15 | return peopleRepository.getPersonDetails( 16 | personId, 17 | imageConfigs: tmdbConfigs.images, 18 | ); 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /lib/features/people/providers/person_images_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/features/people/models/person_image.dart'; 3 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 4 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 5 | 6 | 7 | /// FutureProvider that fetches a person's images 8 | /// 9 | /// See: https://developers.themoviedb.org/3/people/get-person-images 10 | final personImagesProvider = 11 | FutureProvider.family, int>((ref, personId) async { 12 | final peopleRepository = ref.watch(peopleRepositoryProvider); 13 | final tmdbConfigs = await ref.watch(tmdbConfigsProvider.future); 14 | 15 | return peopleRepository.getPersonImages( 16 | personId, 17 | imageConfigs: tmdbConfigs.images, 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/features/people/providers/popular_people_count_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/models/paginated_response.dart'; 3 | import 'package:movies_app/features/people/models/person.dart'; 4 | import 'package:movies_app/features/people/providers/paginated_popular_people_provider.dart'; 5 | 6 | /// The provider that has the value of the total count of the list items 7 | /// 8 | /// The [PaginatedResponse] class contains information about the total number of 9 | /// pages and the total results in all pages along with a list of the 10 | /// provided type 11 | /// 12 | /// An example response from the API for any page looks like this: 13 | /// { 14 | /// "page": 1, 15 | /// "results": [], // list of 20 items 16 | /// "total_pages": 500, 17 | /// "total_results": 10000 // Value taken by this provider 18 | /// } 19 | /// 20 | /// For infinite scroll tutorial: 21 | /// https://github.com/Roaa94/movies_app/tree/main#infinite-scroll-functionality 22 | final popularPeopleCountProvider = Provider>((ref) { 23 | return ref.watch(paginatedPopularPeopleProvider(0)).whenData( 24 | (PaginatedResponse pageData) => pageData.totalResults, 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/features/people/providers/popular_people_list_scroll_controller_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | 4 | /// Scroll controller attached to the [ListView] widget of the 5 | /// popular people list 6 | /// 7 | /// Accessed in PopularPeopleAppBar to scroll to top 8 | final popularPeopleScrollControllerProvider = Provider((ref) { 9 | final scrollController = ScrollController(); 10 | ref.onDispose(scrollController.dispose); 11 | return scrollController; 12 | }); 13 | -------------------------------------------------------------------------------- /lib/features/people/repositories/http_people_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/configs/configs.dart'; 2 | import 'package:movies_app/core/models/paginated_response.dart'; 3 | import 'package:movies_app/core/services/http/http_service.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | import 'package:movies_app/features/people/models/person_image.dart'; 6 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 7 | import 'package:movies_app/features/tmdb-configs/models/tmdb_image_configs.dart'; 8 | 9 | /// Http implementation of the [PeopleRepository] 10 | class HttpPeopleRepository implements PeopleRepository { 11 | /// Creates a new instance of [HttpPeopleRepository] 12 | HttpPeopleRepository(this.httpService); 13 | 14 | /// Http service used to access an Http client and make calls 15 | final HttpService httpService; 16 | 17 | @override 18 | String get path => '/person'; 19 | 20 | @override 21 | String get apiKey => Configs.tmdbAPIKey; 22 | 23 | @override 24 | Future> getPopularPeople({ 25 | int page = 1, 26 | bool forceRefresh = false, 27 | required TMDBImageConfigs imageConfigs, 28 | }) async { 29 | final responseData = await httpService.get( 30 | '$path/popular', 31 | forceRefresh: forceRefresh, 32 | queryParameters: { 33 | 'page': page, 34 | 'api_key': apiKey, 35 | }, 36 | ); 37 | 38 | return PaginatedResponse.fromJson( 39 | responseData, 40 | results: List.from( 41 | (responseData['results'] as List).map( 42 | (dynamic x) => Person.fromJson(x as Map) 43 | .populateImages(imageConfigs), 44 | ), 45 | ), 46 | ); 47 | } 48 | 49 | @override 50 | Future getPersonDetails( 51 | int personId, { 52 | bool forceRefresh = false, 53 | required TMDBImageConfigs imageConfigs, 54 | }) async { 55 | final responseData = await httpService.get( 56 | '$path/$personId', 57 | forceRefresh: forceRefresh, 58 | queryParameters: { 59 | 'api_key': apiKey, 60 | }, 61 | ); 62 | 63 | return Person.fromJson(responseData).populateImages(imageConfigs); 64 | } 65 | 66 | @override 67 | Future> getPersonImages( 68 | int personId, { 69 | bool forceRefresh = false, 70 | required TMDBImageConfigs imageConfigs, 71 | }) async { 72 | final responseData = await httpService.get( 73 | '$path/$personId/images', 74 | queryParameters: { 75 | 'api_key': apiKey, 76 | }, 77 | ); 78 | 79 | return List.from( 80 | (responseData['profiles'] as List).map( 81 | (dynamic x) => PersonImage.fromJson(x as Map) 82 | .populateImages(imageConfigs), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/features/people/repositories/people_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/models/paginated_response.dart'; 3 | import 'package:movies_app/core/services/http/http_service_provider.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | import 'package:movies_app/features/people/models/person_image.dart'; 6 | import 'package:movies_app/features/people/repositories/http_people_repository.dart'; 7 | import 'package:movies_app/features/tmdb-configs/models/tmdb_image_configs.dart'; 8 | 9 | /// Provider to map [HttpPeopleRepository] implementation to 10 | /// [PeopleRepository] interface 11 | final peopleRepositoryProvider = Provider( 12 | (ref) { 13 | final httpService = ref.watch(httpServiceProvider); 14 | 15 | return HttpPeopleRepository(httpService); 16 | }, 17 | ); 18 | 19 | /// People repository interface 20 | abstract class PeopleRepository { 21 | /// TMDB Base endpoint path for people requests 22 | /// 23 | /// See: https://developers.themoviedb.org/3/people 24 | String get path; 25 | 26 | /// API Key used to authenticate TMDB requests 27 | /// 28 | /// See: https://developers.themoviedb.org/3/getting-started/introduction 29 | String get apiKey; 30 | 31 | /// Request to get a person details endpoint 32 | /// 33 | /// See: https://developers.themoviedb.org/3/people/get-person-details 34 | Future getPersonDetails( 35 | int personId, { 36 | bool forceRefresh = false, 37 | required TMDBImageConfigs imageConfigs, 38 | }); 39 | 40 | /// Request to get a list of popular people endpoint 41 | /// 42 | /// See: https://developers.themoviedb.org/3/people/get-popular-people 43 | Future> getPopularPeople({ 44 | int page = 1, 45 | bool forceRefresh = false, 46 | required TMDBImageConfigs imageConfigs, 47 | }); 48 | 49 | /// Request to get a list of person images endpoint 50 | /// 51 | /// See: https://developers.themoviedb.org/3/people/get-person-images 52 | Future> getPersonImages( 53 | int personId, { 54 | bool forceRefresh = false, 55 | required TMDBImageConfigs imageConfigs, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /lib/features/people/views/pages/popular_people_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/features/people/views/widgets/popular_people_app_bar.dart'; 3 | import 'package:movies_app/features/people/views/widgets/popular_people_list.dart'; 4 | 5 | /// Widget for the popular people page 6 | class PopularPeoplePage extends StatelessWidget { 7 | /// Creates a new instance of [PopularPeoplePage] 8 | const PopularPeoplePage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar( 14 | title: const PopularPeopleAppBar(), 15 | ), 16 | body: const PopularPeopleList(), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/enums/gender.dart'; 4 | 5 | /// Avatar widget for person 6 | class PersonAvatar extends StatelessWidget { 7 | /// Creates a new instance of [PersonAvatar] 8 | const PersonAvatar( 9 | this.avatarUrl, { 10 | super.key, 11 | this.gender = Gender.unknown, 12 | }); 13 | 14 | /// Image url of person avatar 15 | final String? avatarUrl; 16 | 17 | /// Gender of the person 18 | /// 19 | /// This is used to show female/male/unknown placeholder images: 20 | /// assets/images/placeholder-female.png 21 | /// assets/images/placeholder-male.png 22 | /// assets/images/placeholder-unknown.png 23 | final Gender gender; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return ClipRRect( 28 | borderRadius: BorderRadius.circular(10), 29 | child: avatarUrl == null 30 | ? Image.asset( 31 | 'assets/images/placeholder-${gender.name}.png', 32 | fit: BoxFit.cover, 33 | height: 90, 34 | ) 35 | : AppCachedNetworkImage( 36 | imageUrl: avatarUrl!, 37 | height: 90, 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_bio.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 3 | 4 | /// Widget for a Person Biography 5 | class PersonBio extends StatelessWidget { 6 | /// Creates a new instance of [PersonBio] 7 | const PersonBio( 8 | this.biography, { 9 | super.key, 10 | }); 11 | 12 | /// Person biography text 13 | final String? biography; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ColoredBox( 18 | color: AppColors.primary, 19 | child: ClipRRect( 20 | borderRadius: const BorderRadius.only( 21 | topLeft: Radius.circular(50), 22 | ), 23 | child: Container( 24 | width: double.infinity, 25 | color: Theme.of(context).scaffoldBackgroundColor, 26 | padding: const EdgeInsetsDirectional.only( 27 | start: 40, 28 | end: 17, 29 | top: 30, 30 | bottom: 20, 31 | ), 32 | child: Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | Text( 36 | 'Biography', 37 | style: Theme.of(context).textTheme.headline2, 38 | ), 39 | const SizedBox(height: 20), 40 | Text( 41 | biography ?? '', 42 | style: Theme.of(context) 43 | .textTheme 44 | .bodyText1! 45 | .copyWith(height: 1.5), 46 | ), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_cover.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/ui_constants.dart'; 3 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 4 | import 'package:movies_app/features/people/enums/gender.dart'; 5 | 6 | /// Widget for a person cover image 7 | class PersonCover extends StatelessWidget { 8 | /// Creates a new instance of [PersonCover] 9 | const PersonCover( 10 | this.coverUrl, { 11 | super.key, 12 | this.gender = Gender.unknown, 13 | this.height = UIConstants.personListItemHeight, 14 | }); 15 | 16 | /// Cover image url 17 | final String? coverUrl; 18 | 19 | /// Gender of the person 20 | /// 21 | /// This is used to show female/male/unknown placeholder images: 22 | /// assets/images/placeholder-female.png 23 | /// assets/images/placeholder-male.png 24 | /// assets/images/placeholder-unknown.png 25 | final Gender gender; 26 | 27 | /// Cover image height 28 | final double height; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return SizedBox( 33 | height: height, 34 | child: coverUrl == null 35 | ? Image.asset( 36 | 'assets/images/placeholder-${gender.name}.png', 37 | fit: BoxFit.cover, 38 | ) 39 | : AppCachedNetworkImage( 40 | imageUrl: coverUrl!, 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_details_sliver_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/widgets/app_bar_leading.dart'; 3 | import 'package:movies_app/features/people/enums/gender.dart'; 4 | import 'package:movies_app/features/people/views/widgets/person_cover.dart'; 5 | 6 | /// Person details page Sliver AppBar widget 7 | class PersonDetailsSliverAppBar extends StatelessWidget { 8 | /// Creates a new instance of [PersonDetailsSliverAppBar] 9 | const PersonDetailsSliverAppBar({ 10 | super.key, 11 | this.avatar, 12 | this.gender = Gender.unknown, 13 | required this.personId, 14 | }); 15 | 16 | /// Id of the person being previewed 17 | final int personId; 18 | 19 | /// Avatar of the person 20 | /// used for the Hero animation 21 | final String? avatar; 22 | 23 | /// Gender of the person 24 | /// 25 | /// This is used to show female/male/unknown placeholder images: 26 | /// assets/images/placeholder-female.png 27 | /// assets/images/placeholder-male.png 28 | /// assets/images/placeholder-unknown.png 29 | final Gender gender; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return SliverAppBar( 34 | expandedHeight: MediaQuery.of(context).size.height * 0.35, 35 | collapsedHeight: 100, 36 | leadingWidth: 70, 37 | backgroundColor: Colors.transparent, 38 | leading: const AppBarLeading(), 39 | pinned: true, 40 | flexibleSpace: Padding( 41 | padding: const EdgeInsetsDirectional.only(start: 40), 42 | child: Hero( 43 | tag: 'person_${personId}_profile_picture', 44 | transitionOnUserGestures: true, 45 | child: ClipRRect( 46 | borderRadius: const BorderRadius.only( 47 | bottomLeft: Radius.circular(50), 48 | ), 49 | child: PersonCover( 50 | avatar, 51 | gender: gender, 52 | height: double.infinity, 53 | ), 54 | ), 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_images_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/features/people/models/person_image.dart'; 3 | import 'package:movies_app/features/people/views/pages/person_images_slider_page.dart'; 4 | import 'package:movies_app/features/people/views/widgets/person_images_grid_item.dart'; 5 | 6 | /// Widget of the images grid section of the person details 7 | class PersonImagesGrid extends StatelessWidget { 8 | /// Creates a new instance of [PersonImagesGrid] 9 | const PersonImagesGrid( 10 | this.images, { 11 | super.key, 12 | }); 13 | 14 | /// List of person images to be displayed in the grid 15 | final List images; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return SizedBox( 20 | height: (images.length / 3).ceil() * 21 | ((MediaQuery.of(context).size.width - 10 * 4) / 3), 22 | child: GridView.builder( 23 | physics: const NeverScrollableScrollPhysics(), 24 | itemCount: images.length, 25 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 26 | crossAxisCount: 3, 27 | crossAxisSpacing: 10, 28 | mainAxisSpacing: 10, 29 | ), 30 | padding: const EdgeInsetsDirectional.only(start: 40, end: 17), 31 | itemBuilder: (context, index) => ClipRRect( 32 | borderRadius: BorderRadius.circular(10), 33 | child: PersonImagesGridItem( 34 | images[index], 35 | onTap: () { 36 | Navigator.of(context).push( 37 | MaterialPageRoute( 38 | builder: (context) => PersonImagesSliderPage( 39 | images: images, 40 | initialImageIndex: index, 41 | ), 42 | ), 43 | ); 44 | }, 45 | ), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_images_grid_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/models/person_image.dart'; 4 | 5 | /// Widget of a person images section grid item 6 | class PersonImagesGridItem extends StatelessWidget { 7 | /// Creates a new instance of [PersonImagesGridItem] 8 | const PersonImagesGridItem( 9 | this.personImage, { 10 | super.key, 11 | this.onTap, 12 | }); 13 | 14 | /// Image item of the person images grid 15 | final PersonImage personImage; 16 | 17 | /// Callback for tap event on image item 18 | final VoidCallback? onTap; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return GestureDetector( 23 | onTap: onTap, 24 | child: personImage.thumbnail == null 25 | ? const ColoredBox(color: Colors.white) 26 | : AppCachedNetworkImage( 27 | imageUrl: personImage.thumbnail!, 28 | fit: BoxFit.cover, 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | 6 | /// Person information widget 7 | /// 8 | /// Contains person department and birthday info 9 | class PersonInfo extends StatelessWidget { 10 | /// Creates a new instance of [PersonInfo] 11 | const PersonInfo( 12 | this.person, { 13 | super.key, 14 | }); 15 | 16 | /// Person object 17 | final Person person; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ColoredBox( 22 | color: AppColors.secondary, 23 | child: ClipRRect( 24 | borderRadius: const BorderRadius.only( 25 | bottomRight: Radius.circular(50), 26 | ), 27 | child: Container( 28 | padding: const EdgeInsetsDirectional.only( 29 | start: 40, 30 | end: 17, 31 | bottom: 20, 32 | ), 33 | color: Theme.of(context).scaffoldBackgroundColor, 34 | child: Row( 35 | children: [ 36 | if (person.knownForDepartment != null) 37 | Expanded( 38 | child: Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Text( 42 | 'Known For', 43 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 44 | fontWeight: FontWeight.w700, 45 | color: AppColors.white.withOpacity(0.5), 46 | ), 47 | ), 48 | const SizedBox(height: 10), 49 | Text( 50 | person.knownForDepartment!, 51 | style: Theme.of(context).primaryTextTheme.headline4, 52 | ), 53 | ], 54 | ), 55 | ), 56 | if (person.birthday != null) 57 | Expanded( 58 | child: Column( 59 | crossAxisAlignment: CrossAxisAlignment.start, 60 | children: [ 61 | Text( 62 | 'Birthday', 63 | style: Theme.of(context).textTheme.subtitle1!.copyWith( 64 | fontWeight: FontWeight.w700, 65 | color: AppColors.white.withOpacity(0.5), 66 | ), 67 | ), 68 | const SizedBox(height: 10), 69 | Text( 70 | DateFormat('dd-MM-yyyy').format(person.birthday!), 71 | style: Theme.of(context).primaryTextTheme.headline4, 72 | ), 73 | ], 74 | ), 75 | ), 76 | // Expanded(), 77 | ], 78 | ), 79 | ), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/person_name.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 3 | 4 | /// Widget for a person name with adaptive text size 5 | class PersonName extends StatelessWidget { 6 | /// Creates a new instance of [PersonName] 7 | const PersonName( 8 | this.personName, { 9 | super.key, 10 | }); 11 | 12 | /// Person name 13 | final String personName; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Padding( 18 | padding: const EdgeInsetsDirectional.only( 19 | start: 40, 20 | end: 100, 21 | top: 20, 22 | bottom: 15, 23 | ), 24 | child: Text( 25 | personName, 26 | style: Theme.of(context).textTheme.headline1!.copyWith( 27 | fontSize: personName.length > 10 ? 40 : 60, 28 | fontWeight: FontWeight.w400, 29 | color: AppColors.white, 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/popular_people_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:movies_app/features/people/providers/popular_people_list_scroll_controller_provider.dart'; 4 | 5 | /// AppBar widget for popular people page 6 | class PopularPeopleAppBar extends ConsumerWidget { 7 | /// Creates a new instance of [PopularPeopleAppBar] 8 | const PopularPeopleAppBar({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context, WidgetRef ref) { 12 | final popularPeopleScrollController = 13 | ref.watch(popularPeopleScrollControllerProvider); 14 | 15 | return GestureDetector( 16 | key: const ValueKey('__app_bar_title_gesture_detector__'), 17 | onTap: () => popularPeopleScrollController.animateTo( 18 | popularPeopleScrollController.position.minScrollExtent, 19 | duration: const Duration(milliseconds: 300), 20 | curve: Curves.easeInOut, 21 | ), 22 | child: const Text('Popular'), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/popular_people_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:movies_app/core/configs/styles/ui_constants.dart'; 6 | import 'package:movies_app/core/widgets/error_view.dart'; 7 | import 'package:movies_app/core/widgets/list_item_shimmer.dart'; 8 | import 'package:movies_app/features/people/providers/current_popular_person_provider.dart'; 9 | import 'package:movies_app/features/people/providers/paginated_popular_people_provider.dart'; 10 | import 'package:movies_app/features/people/providers/popular_people_count_provider.dart'; 11 | import 'package:movies_app/features/people/providers/popular_people_list_scroll_controller_provider.dart'; 12 | import 'package:movies_app/features/people/views/widgets/popular_person_list_item.dart'; 13 | 14 | /// Widget holding the list of popular people 15 | class PopularPeopleList extends ConsumerWidget { 16 | /// Creates new instance of [PopularPeopleList] 17 | const PopularPeopleList({super.key}); 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | final popularPeopleCount = ref.watch(popularPeopleCountProvider); 22 | final scrollController = ref.watch(popularPeopleScrollControllerProvider); 23 | 24 | return popularPeopleCount.when( 25 | loading: () => const ListItemShimmer(), 26 | data: (int count) { 27 | return ListView.builder( 28 | controller: scrollController, 29 | itemCount: count, 30 | itemExtent: UIConstants.personListItemHeight, 31 | itemBuilder: (context, index) { 32 | final currentPopularPersonFromIndex = ref 33 | .watch(paginatedPopularPeopleProvider(index ~/ 20)) 34 | .whenData((pageData) => pageData.results[index % 20]); 35 | 36 | return ProviderScope( 37 | overrides: [ 38 | currentPopularPersonProvider 39 | .overrideWithValue(currentPopularPersonFromIndex) 40 | ], 41 | child: const PopularPersonListItem(), 42 | ); 43 | }, 44 | ); 45 | }, 46 | error: (Object error, StackTrace? stackTrace) { 47 | log('Error fetching popular people'); 48 | log(error.toString()); 49 | log(stackTrace.toString()); 50 | return const ErrorView(); 51 | }, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/save_image_slider_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 4 | import 'package:movies_app/core/services/media/media_service.dart'; 5 | import 'package:movies_app/core/widgets/app_loader.dart'; 6 | import 'package:movies_app/features/people/views/widgets/slider_action.dart'; 7 | 8 | /// StateProvider for loading state of image saving action 9 | final isLoadingSaveImage = StateProvider((_) => false); 10 | 11 | /// Slider action widget for saving image to device gallery 12 | class SaveImageSliderAction extends ConsumerWidget { 13 | /// Creates a new instance of [SaveImageSliderAction] 14 | const SaveImageSliderAction({ 15 | super.key, 16 | required this.imageUrl, 17 | }); 18 | 19 | /// Url of the image to be saved 20 | final String imageUrl; 21 | 22 | @override 23 | Widget build(BuildContext context, WidgetRef ref) { 24 | return ref.watch(isLoadingSaveImage) 25 | ? const Padding( 26 | padding: EdgeInsets.all(10), 27 | child: AppLoader(), 28 | ) 29 | : SliderAction( 30 | color: AppColors.secondary, 31 | icon: const Icon(Icons.download), 32 | onTap: ref.watch(isLoadingSaveImage) 33 | ? null 34 | : () { 35 | ref.read(isLoadingSaveImage.notifier).state = true; 36 | ref 37 | .read(mediaServiceProvider) 38 | .saveNetworkImageToGallery(imageUrl) 39 | .then((_) { 40 | ScaffoldMessenger.of(context).showSnackBar( 41 | const SnackBar( 42 | content: Text('Image saved to gallery successfully!'), 43 | duration: Duration(seconds: 1), 44 | ), 45 | ); 46 | ref.read(isLoadingSaveImage.notifier).state = false; 47 | }); 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/features/people/views/widgets/slider_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:movies_app/core/configs/styles/app_colors.dart'; 3 | 4 | /// Slider action widget like next, prev, download 5 | class SliderAction extends StatelessWidget { 6 | /// Creates a new instance of [SliderAction] 7 | const SliderAction({ 8 | super.key, 9 | required this.icon, 10 | this.onTap, 11 | this.color = AppColors.primary, 12 | }); 13 | 14 | /// Icons widget inside the slider action 15 | final Widget icon; 16 | 17 | /// onTap action 18 | final VoidCallback? onTap; 19 | 20 | /// widget background color 21 | final Color color; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Center( 26 | child: ClipOval( 27 | child: Material( 28 | color: color, 29 | child: InkWell( 30 | onTap: onTap, 31 | child: Padding( 32 | padding: const EdgeInsets.all(8), 33 | child: icon, 34 | ), 35 | ), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/features/tmdb-configs/enums/image_size.dart: -------------------------------------------------------------------------------- 1 | /// Enum for image sizes from The TMDB API configurations 2 | /// 3 | /// See: https://developers.themoviedb.org/3/configuration/get-api-configuration 4 | enum ImageSize { 5 | /// Image width of 300px 6 | w300, 7 | 8 | /// Image width of 780px 9 | w780, 10 | 11 | /// Image width of 1280px 12 | w1280, 13 | 14 | /// Image width of 45px 15 | w45, 16 | 17 | /// Image width of 92px 18 | w92, 19 | 20 | /// Image width of 154px 21 | w154, 22 | 23 | /// Image width of 185px 24 | w185, 25 | 26 | /// Image width of 500px 27 | w500, 28 | 29 | /// Image width of 342px 30 | w342, 31 | 32 | /// Image height of 632px 33 | h632, 34 | 35 | /// Original image size 36 | original, 37 | } 38 | -------------------------------------------------------------------------------- /lib/features/tmdb-configs/models/tmdb_configs.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:movies_app/features/tmdb-configs/models/tmdb_image_configs.dart'; 3 | 4 | 5 | /// Model for TMDB Configs fetched from the /configuration endpoint 6 | /// 7 | /// See https://developers.themoviedb.org/3/configuration/get-api-configuration 8 | class TMDBConfigs extends Equatable { 9 | /// Creates new instance of [TMDBConfigs] 10 | const TMDBConfigs({ 11 | required this.images, 12 | }); 13 | 14 | /// Creates new instance of [TMDBConfigs] from parsed raw data 15 | factory TMDBConfigs.fromJson(Map json) { 16 | return TMDBConfigs( 17 | images: TMDBImageConfigs.fromJson(json['images'] as Map), 18 | ); 19 | } 20 | 21 | /// Image configurations information 22 | final TMDBImageConfigs images; 23 | 24 | @override 25 | List get props => [images]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/features/tmdb-configs/providers/tmdb_configs_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 3 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 4 | 5 | /// FutureProvider that fetches TMDB Configurations 6 | /// from the /configurations endpoint 7 | /// 8 | /// See: https://developers.themoviedb.org/3/configuration/get-api-configuration 9 | final tmdbConfigsProvider = FutureProvider((ref) async { 10 | final tmdbConfigsRepository = ref.watch(tmdbConfigsRepositoryProvider); 11 | 12 | return tmdbConfigsRepository.get(); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/features/tmdb-configs/repositories/http_tmdb_configs_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:movies_app/core/configs/configs.dart'; 2 | import 'package:movies_app/core/services/http/http_service.dart'; 3 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 4 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 5 | 6 | /// Http implementation of [TMDBConfigsRepository] 7 | /// 8 | /// See: https://developers.themoviedb.org/3/configuration/get-api-configuration 9 | class HttpTMDBConfigsRepository implements TMDBConfigsRepository { 10 | /// Creates a new instance of [HttpTMDBConfigsRepository] 11 | HttpTMDBConfigsRepository(this.httpService); 12 | 13 | /// Http service used to access an Http client and make calls 14 | final HttpService httpService; 15 | 16 | @override 17 | String get path => '/configuration'; 18 | 19 | @override 20 | String get apiKey => Configs.tmdbAPIKey; 21 | 22 | @override 23 | Future get({bool forceRefresh = false}) async { 24 | final response = await httpService.get( 25 | path, 26 | queryParameters: { 27 | 'api_key': apiKey, 28 | }, 29 | forceRefresh: forceRefresh, 30 | ); 31 | 32 | return TMDBConfigs.fromJson(response); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/tmdb-configs/repositories/tmdb_configs_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:movies_app/core/services/http/http_service_provider.dart'; 3 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 4 | import 'package:movies_app/features/tmdb-configs/repositories/http_tmdb_configs_repository.dart'; 5 | 6 | /// Provider to map [HttpTMDBConfigsRepository] implementation to 7 | /// [TMDBConfigsRepository] interface 8 | final tmdbConfigsRepositoryProvider = Provider((ref) { 9 | final httpService = ref.watch(httpServiceProvider); 10 | 11 | return HttpTMDBConfigsRepository(httpService); 12 | }); 13 | 14 | /// TMDB Configurations repository interface 15 | abstract class TMDBConfigsRepository { 16 | /// TMDB Base endpoint path for configurations endpoint 17 | /// 18 | /// See: https://developers.themoviedb.org/3/configuration/get-api-configuration 19 | String get path; 20 | 21 | /// API Key used to authenticate TMDB requests 22 | /// 23 | /// See: https://developers.themoviedb.org/3/getting-started/introduction 24 | String get apiKey; 25 | 26 | /// Request to get TMDB configurations endpoint 27 | /// 28 | /// See: https://developers.themoviedb.org/3/configuration/get-api-configuration 29 | Future get({bool forceRefresh = false}); 30 | } 31 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:hive_flutter/hive_flutter.dart'; 6 | import 'package:movies_app/core/services/storage/hive_storage_service.dart'; 7 | import 'package:movies_app/core/services/storage/storage_service.dart'; 8 | import 'package:movies_app/core/services/storage/storage_service_provider.dart'; 9 | import 'package:movies_app/movies_app.dart'; 10 | 11 | void main() { 12 | runZonedGuarded>( 13 | () async { 14 | // Hive-specific initialization 15 | await Hive.initFlutter(); 16 | final StorageService initializedStorageService = HiveStorageService(); 17 | await initializedStorageService.init(); 18 | 19 | runApp( 20 | ProviderScope( 21 | overrides: [ 22 | storageServiceProvider.overrideWithValue(initializedStorageService), 23 | ], 24 | child: const MoviesApp(), 25 | ), 26 | ); 27 | }, 28 | // ignore: only_throw_errors 29 | (e, _) => throw e, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/movies_app.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:movies_app/core/configs/styles/app_themes.dart'; 6 | import 'package:movies_app/core/widgets/app_loader.dart'; 7 | import 'package:movies_app/core/widgets/error_view.dart'; 8 | import 'package:movies_app/features/people/views/pages/popular_people_page.dart'; 9 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 10 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 11 | 12 | /// Main App Widget 13 | class MoviesApp extends ConsumerWidget { 14 | /// Creates new instance of [MoviesApp] 15 | const MoviesApp({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context, WidgetRef ref) { 19 | final configsAsync = ref.watch(tmdbConfigsProvider); 20 | 21 | return MaterialApp( 22 | title: 'Movies App', 23 | debugShowCheckedModeBanner: false, 24 | themeMode: ThemeMode.dark, 25 | theme: AppThemes.lightTheme, 26 | darkTheme: AppThemes.darkTheme, 27 | home: configsAsync.when( 28 | data: (TMDBConfigs tmdbConfigs) { 29 | return const PopularPeoplePage(); 30 | }, 31 | error: (Object error, StackTrace? stackTrace) { 32 | log('Error fetching configurations'); 33 | log(error.toString()); 34 | log(stackTrace.toString()); 35 | return const Scaffold(body: ErrorView()); 36 | }, 37 | loading: () => const Scaffold(body: AppLoader()), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import path_provider_macos 9 | import sqflite 10 | import url_launcher_macos 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 14 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 15 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /macos/Flutter/ephemeral/Flutter-Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/Users/roaakhaddam/development/flutter 3 | FLUTTER_APPLICATION_PATH=/Users/roaakhaddam/flutter_packages/movies_app 4 | COCOAPODS_PARALLEL_CODE_SIGN=true 5 | FLUTTER_BUILD_DIR=build 6 | FLUTTER_BUILD_NAME=1.0.0 7 | FLUTTER_BUILD_NUMBER=1 8 | DART_OBFUSCATION=false 9 | TRACK_WIDGET_CREATION=true 10 | TREE_SHAKE_ICONS=false 11 | PACKAGE_CONFIG=.dart_tool/package_config.json 12 | -------------------------------------------------------------------------------- /macos/Flutter/ephemeral/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/roaakhaddam/development/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/roaakhaddam/flutter_packages/movies_app" 5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "FLUTTER_BUILD_NAME=1.0.0" 8 | export "FLUTTER_BUILD_NUMBER=1" 9 | export "DART_OBFUSCATION=false" 10 | export "TRACK_WIDGET_CREATION=true" 11 | export "TREE_SHAKE_ICONS=false" 12 | export "PACKAGE_CONFIG=.dart_tool/package_config.json" 13 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.11' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Pods/FMDB/LICENSE.txt: -------------------------------------------------------------------------------- 1 | If you are using FMDB in your project, I'd love to hear about it. Let Gus know 2 | by sending an email to gus@flyingmeat.com. 3 | 4 | And if you happen to come across either Gus Mueller or Rob Ryan in a bar, you 5 | might consider purchasing a drink of their choosing if FMDB has been useful to 6 | you. 7 | 8 | Finally, and shortly, this is the MIT License. 9 | 10 | Copyright (c) 2008-2014 Flying Meat Inc. 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in 20 | all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | THE SOFTWARE. -------------------------------------------------------------------------------- /macos/Pods/FMDB/src/fmdb/FMDB.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | FOUNDATION_EXPORT double FMDBVersionNumber; 4 | FOUNDATION_EXPORT const unsigned char FMDBVersionString[]; 5 | 6 | #import "FMDatabase.h" 7 | #import "FMResultSet.h" 8 | #import "FMDatabaseAdditions.h" 9 | #import "FMDatabaseQueue.h" 10 | #import "FMDatabasePool.h" 11 | -------------------------------------------------------------------------------- /macos/Pods/Local Podspecs/FlutterMacOS.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FlutterMacOS", 3 | "version": "1.0.0", 4 | "summary": "A UI toolkit for beautiful and fast apps.", 5 | "homepage": "https://flutter.dev", 6 | "license": { 7 | "type": "BSD" 8 | }, 9 | "authors": { 10 | "Flutter Dev Team": "flutter-dev@googlegroups.com" 11 | }, 12 | "source": { 13 | "git": "https://github.com/flutter/engine", 14 | "tag": "1.0.0" 15 | }, 16 | "platforms": { 17 | "osx": "10.11" 18 | }, 19 | "vendored_frameworks": "path/to/nothing" 20 | } 21 | -------------------------------------------------------------------------------- /macos/Pods/Local Podspecs/path_provider_macos.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path_provider_macos", 3 | "version": "0.0.1", 4 | "summary": "A macOS implementation of the path_provider plugin.", 5 | "description": "A macOS implementation of the Flutter plugin for getting commonly used locations on the filesystem.", 6 | "homepage": "https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos", 7 | "license": { 8 | "type": "BSD", 9 | "file": "../LICENSE" 10 | }, 11 | "authors": { 12 | "Flutter Dev Team": "flutter-dev@googlegroups.com" 13 | }, 14 | "source": { 15 | "http": "https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos" 16 | }, 17 | "source_files": "Classes/**/*", 18 | "dependencies": { 19 | "FlutterMacOS": [ 20 | 21 | ] 22 | }, 23 | "platforms": { 24 | "osx": "10.11" 25 | }, 26 | "swift_versions": "5.0", 27 | "swift_version": "5.0" 28 | } 29 | -------------------------------------------------------------------------------- /macos/Pods/Local Podspecs/sqflite.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqflite", 3 | "version": "0.0.2", 4 | "summary": "SQLite plugin.", 5 | "description": "Accss SQLite database.", 6 | "homepage": "https://github.com/tekartik/sqflite", 7 | "license": { 8 | "file": "../LICENSE" 9 | }, 10 | "authors": { 11 | "Tekartik": "alex@tekartik.com" 12 | }, 13 | "source": { 14 | "path": "." 15 | }, 16 | "source_files": "Classes/**/*", 17 | "dependencies": { 18 | "FlutterMacOS": [ 19 | 20 | ], 21 | "FMDB": [ 22 | ">= 2.7.5" 23 | ] 24 | }, 25 | "platforms": { 26 | "osx": "10.11" 27 | }, 28 | "pod_target_xcconfig": { 29 | "DEFINES_MODULE": "YES" 30 | }, 31 | "swift_versions": "5.0", 32 | "swift_version": "5.0" 33 | } 34 | -------------------------------------------------------------------------------- /macos/Pods/Local Podspecs/url_launcher_macos.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url_launcher_macos", 3 | "version": "0.0.1", 4 | "summary": "Flutter macos plugin for launching a URL.", 5 | "description": "A macOS implementation of the url_launcher plugin.", 6 | "homepage": "https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_macos", 7 | "license": { 8 | "type": "BSD", 9 | "file": "../LICENSE" 10 | }, 11 | "authors": { 12 | "Flutter Team": "flutter-dev@googlegroups.com" 13 | }, 14 | "source": { 15 | "http": "https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos" 16 | }, 17 | "source_files": "Classes/**/*", 18 | "dependencies": { 19 | "FlutterMacOS": [ 20 | 21 | ] 22 | }, 23 | "platforms": { 24 | "osx": "10.11" 25 | }, 26 | "pod_target_xcconfig": { 27 | "DEFINES_MODULE": "YES" 28 | }, 29 | "swift_versions": "5.0", 30 | "swift_version": "5.0" 31 | } 32 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/FMDB.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/FlutterMacOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/Pods-Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/path_provider_macos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/sqflite.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/url_launcher_macos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /macos/Pods/Pods.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FMDB.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 2 13 | 14 | FlutterMacOS.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 1 20 | 21 | Pods-Runner.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 4 27 | 28 | path_provider_macos.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 3 34 | 35 | sqflite.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 5 41 | 42 | url_launcher_macos.xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 6 48 | 49 | 50 | SuppressBuildableAutocreation 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.7.5 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_FMDB : NSObject 3 | @end 4 | @implementation PodsDummy_FMDB 5 | @end 6 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "FMDatabase.h" 14 | #import "FMDatabaseAdditions.h" 15 | #import "FMDatabasePool.h" 16 | #import "FMDatabaseQueue.h" 17 | #import "FMDB.h" 18 | #import "FMResultSet.h" 19 | 20 | FOUNDATION_EXPORT double FMDBVersionNumber; 21 | FOUNDATION_EXPORT const unsigned char FMDBVersionString[]; 22 | 23 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FMDB 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | OTHER_LDFLAGS = $(inherited) -l"sqlite3" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/FMDB 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB.modulemap: -------------------------------------------------------------------------------- 1 | framework module FMDB { 2 | umbrella header "FMDB-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FMDB/FMDB.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FMDB 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | OTHER_LDFLAGS = $(inherited) -l"sqlite3" 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/FMDB 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FlutterMacOS/FlutterMacOS.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FlutterMacOS 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral 9 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 10 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 11 | SKIP_INSTALL = YES 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/FlutterMacOS/FlutterMacOS.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FlutterMacOS 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral 9 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 10 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 11 | SKIP_INSTALL = YES 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Runner : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Runner 5 | @end 6 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework 3 | ${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework 4 | ${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework 5 | ${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework 2 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework 3 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework 4 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Profile-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework 3 | ${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework 4 | ${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework 5 | ${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Profile-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework 2 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework 3 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework 4 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework 3 | ${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework 4 | ${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework 5 | ${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework 2 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework 3 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework 4 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_RunnerVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_RunnerVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB/FMDB.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos/path_provider_macos.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite/sqflite.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos/url_launcher_macos.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -l"sqlite3" -framework "FMDB" -framework "path_provider_macos" -framework "sqflite" -framework "url_launcher_macos" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Runner { 2 | umbrella header "Pods-Runner-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB/FMDB.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos/path_provider_macos.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite/sqflite.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos/url_launcher_macos.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -l"sqlite3" -framework "FMDB" -framework "path_provider_macos" -framework "sqflite" -framework "url_launcher_macos" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB/FMDB.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos/path_provider_macos.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/sqflite/sqflite.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos/url_launcher_macos.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/../Frameworks' '@loader_path/Frameworks' "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 7 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 8 | OTHER_LDFLAGS = $(inherited) -l"sqlite3" -framework "FMDB" -framework "path_provider_macos" -framework "sqflite" -framework "url_launcher_macos" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_path_provider_macos : NSObject 3 | @end 4 | @implementation PodsDummy_path_provider_macos 5 | @end 6 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double path_provider_macosVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char path_provider_macosVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_ROOT = ${SRCROOT} 10 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos 11 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 12 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 13 | SKIP_INSTALL = YES 14 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 15 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos.modulemap: -------------------------------------------------------------------------------- 1 | framework module path_provider_macos { 2 | umbrella header "path_provider_macos-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/path_provider_macos/path_provider_macos.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/path_provider_macos 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_ROOT = ${SRCROOT} 10 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos 11 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 12 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 13 | SKIP_INSTALL = YES 14 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 15 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.0.2 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_sqflite : NSObject 3 | @end 4 | @implementation PodsDummy_sqflite 5 | @end 6 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "SqfliteOperation.h" 14 | #import "SqflitePlugin.h" 15 | 16 | FOUNDATION_EXPORT double sqfliteVersionNumber; 17 | FOUNDATION_EXPORT const unsigned char sqfliteVersionString[]; 18 | 19 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/sqflite 4 | DEFINES_MODULE = YES 5 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB" 6 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 7 | OTHER_LDFLAGS = $(inherited) -framework "FMDB" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/sqflite/macos 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite.modulemap: -------------------------------------------------------------------------------- 1 | framework module sqflite { 2 | umbrella header "sqflite-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/sqflite/sqflite.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/sqflite 4 | DEFINES_MODULE = YES 5 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FMDB" 6 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 7 | OTHER_LDFLAGS = $(inherited) -framework "FMDB" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/sqflite/macos 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_url_launcher_macos : NSObject 3 | @end 4 | @implementation PodsDummy_url_launcher_macos 5 | @end 6 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double url_launcher_macosVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char url_launcher_macosVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos 4 | DEFINES_MODULE = YES 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos.modulemap: -------------------------------------------------------------------------------- 1 | framework module url_launcher_macos { 2 | umbrella header "url_launcher_macos-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /macos/Pods/Target Support Files/url_launcher_macos/url_launcher_macos.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CODE_SIGN_IDENTITY = 3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos 4 | DEFINES_MODULE = YES 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT} 11 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 12 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 13 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 14 | SKIP_INSTALL = YES 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcuserdata/roaakhaddam.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Flutter Assemble.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 7 11 | 12 | Runner.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcuserdata/roaakhaddam.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roaa94/movies_app/3e0ec08f2da5dbc092c7e71ad8a57d4b21dd59ca/macos/Runner.xcworkspace/xcuserdata/roaakhaddam.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /test/core/services/http/http_service_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/services/http/dio_http_service.dart'; 4 | import 'package:movies_app/core/services/http/http_service_provider.dart'; 5 | 6 | void main() { 7 | test('httpServiceProvider is a DioHttpService', () { 8 | final providerContainer = ProviderContainer(); 9 | 10 | addTearDown(providerContainer.dispose); 11 | 12 | expect( 13 | providerContainer.read(httpServiceProvider), 14 | isA(), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/core/services/media/media_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/services/media/media_service.dart'; 4 | 5 | void main() { 6 | test('mediaServiceProvider is a GallerySaverMediaService', () { 7 | final providerContainer = ProviderContainer(); 8 | 9 | addTearDown(providerContainer.dispose); 10 | 11 | expect( 12 | providerContainer.read(mediaServiceProvider), 13 | isA(), 14 | ); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/core/services/storage/hive_storage_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:hive_test/hive_test.dart'; 3 | import 'package:movies_app/core/services/storage/hive_storage_service.dart'; 4 | import 'package:movies_app/core/services/storage/storage_service.dart'; 5 | 6 | void main() { 7 | final StorageService hiveStorageService = HiveStorageService(); 8 | const testStorageKey = 'test'; 9 | 10 | setUp(() async { 11 | await setUpTestHive(); 12 | await hiveStorageService.init(); 13 | }); 14 | 15 | test('Can store & get value', () async { 16 | await hiveStorageService.set(testStorageKey, 'someValue'); 17 | 18 | expect(hiveStorageService.get(testStorageKey), 'someValue'); 19 | }); 20 | 21 | test('Can check if key exists', () async { 22 | await hiveStorageService.set(testStorageKey, 'someValue'); 23 | expect(hiveStorageService.has(testStorageKey), true); 24 | 25 | await hiveStorageService.remove(testStorageKey); 26 | expect(hiveStorageService.has(testStorageKey), false); 27 | }); 28 | 29 | test('Can delete value', () async { 30 | await hiveStorageService.set(testStorageKey, 'someValue'); 31 | await hiveStorageService.remove(testStorageKey); 32 | 33 | expect(hiveStorageService.get(testStorageKey), null); 34 | }); 35 | 36 | test('Can get all values', () async { 37 | await hiveStorageService.set(testStorageKey, 'someValue'); 38 | await hiveStorageService.set('test2', 'otherValue'); 39 | 40 | expect(hiveStorageService.getAll(), ['someValue', 'otherValue']); 41 | }); 42 | 43 | test('Can clear all values', () async { 44 | await hiveStorageService.set(testStorageKey, 'someValue'); 45 | await hiveStorageService.set('test2', 'otherValue'); 46 | await hiveStorageService.clear(); 47 | 48 | expect(hiveStorageService.getAll(), []); 49 | }); 50 | 51 | tearDown(() async { 52 | await hiveStorageService.close(); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/core/services/storage/storage_service_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/services/storage/hive_storage_service.dart'; 4 | import 'package:movies_app/core/services/storage/storage_service_provider.dart'; 5 | 6 | void main() { 7 | test('serviceProvider returns HiveStorageService', () { 8 | final providerContainer = ProviderContainer(); 9 | 10 | addTearDown(providerContainer.dispose); 11 | 12 | expect( 13 | providerContainer.read(storageServiceProvider), 14 | isA(), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/core/widgets/app_bar_leading_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/core/widgets/app_bar_leading.dart'; 5 | 6 | import '../../test-utils/golden_test_utils.dart'; 7 | import '../../test-utils/mocks.dart'; 8 | import '../../test-utils/pump_app.dart'; 9 | 10 | void main() { 11 | setUpAll(() { 12 | registerFallbackValue(MockRoute()); 13 | }); 14 | 15 | testWidgets('can pop', (WidgetTester tester) async { 16 | final mockNavigatorObserver = MockNavigatorObserver(); 17 | 18 | await tester.pumpApp( 19 | const AppBarLeading(), 20 | navigatorObserver: mockNavigatorObserver, 21 | ); 22 | 23 | await tester.pumpAndSettle(); 24 | 25 | await tester.tap(find.byType(InkWell)); 26 | verify(() => mockNavigatorObserver.didPop(any(), any())); 27 | }); 28 | 29 | testWidgets('matches expected widget', (WidgetTester tester) async { 30 | await GoldenTestUtils.loadMaterialIconsFont(); 31 | await tester.pumpApp( 32 | const AppBarLeading(), 33 | ); 34 | 35 | await tester.pumpAndSettle(); 36 | 37 | await expectLater( 38 | find.byType(AppBarLeading), 39 | matchesGoldenFile('goldens/app_bar_leading.png'), 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/core/widgets/app_cached_network_image_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/core/widgets/app_loader.dart'; 4 | import 'package:movies_app/core/widgets/shimmer.dart'; 5 | 6 | import '../../test-utils/pump_app.dart'; 7 | 8 | // Todo: test error widget 9 | void main() { 10 | testWidgets('shows loading widget', (WidgetTester tester) async { 11 | await tester.pumpApp( 12 | const AppCachedNetworkImage( 13 | imageUrl: 'image_url', 14 | isLoaderShimmer: false, 15 | ), 16 | ); 17 | 18 | await tester.pump(); 19 | expect(find.byType(AppLoader), findsOneWidget); 20 | }); 21 | 22 | testWidgets('shows shimmer widget', (WidgetTester tester) async { 23 | await tester.pumpApp( 24 | const AppCachedNetworkImage( 25 | imageUrl: 'image_url', 26 | isLoaderShimmer: true, 27 | ), 28 | ); 29 | 30 | await tester.pump(); 31 | expect(find.byType(Shimmer), findsOneWidget); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/features/people/enums/gender_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/features/people/enums/gender.dart'; 3 | 4 | void main() { 5 | test('Female gender is equivalent to 1', () { 6 | expect(Gender.female.toInt, equals(1)); 7 | 8 | expect(Gender.fromInt(1), Gender.female); 9 | }); 10 | 11 | test('Male gender is equivalent to 2', () { 12 | expect(Gender.male.toInt, equals(2)); 13 | 14 | expect(Gender.fromInt(2), Gender.male); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/features/people/models/person_image_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/features/people/models/person_image.dart'; 3 | 4 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 5 | 6 | void main() { 7 | const rawExamplePersonImage = { 8 | 'aspect_ratio': 0.667, 9 | 'height': 900, 10 | 'iso_639_1': null, 11 | 'file_path': '/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg', 12 | 'vote_average': 5.828, 13 | 'vote_count': 113, 14 | 'width': 600, 15 | }; 16 | 17 | const examplePersonImage = PersonImage( 18 | aspectRatio: 0.667, 19 | height: 900, 20 | iso6391: null, 21 | filePath: '/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg', 22 | voteAverage: 5.828, 23 | voteCount: 113, 24 | width: 600, 25 | ); 26 | 27 | test('can pars data from json', () { 28 | expect( 29 | PersonImage.fromJson(rawExamplePersonImage), 30 | equals(examplePersonImage), 31 | ); 32 | }); 33 | 34 | test('can populate imageUrl & thumbnail with correct image urls', () { 35 | final examplePersonImageWithImages = 36 | examplePersonImage.populateImages(DummyConfigs.imageConfigs); 37 | 38 | final imageUrl = 39 | // ignore: lines_longer_than_80_chars 40 | '${DummyConfigs.imageConfigs.secureBaseUrl}${PersonImage.imageSize.name}${examplePersonImage.filePath}'; 41 | // https://image.tmdb.org/t/p/original/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg 42 | 43 | expect(examplePersonImageWithImages.imageUrl, equals(imageUrl)); 44 | 45 | final thumbnailUrl = 46 | // ignore: lines_longer_than_80_chars 47 | '${DummyConfigs.imageConfigs.secureBaseUrl}${PersonImage.thumbnailSize.name}${examplePersonImage.filePath}'; 48 | // https://image.tmdb.org/t/p/h632/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg 49 | 50 | expect(examplePersonImageWithImages.thumbnail, equals(thumbnailUrl)); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/features/people/models/person_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/features/media/models/media.dart'; 3 | import 'package:movies_app/features/people/enums/gender.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | 6 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 7 | 8 | void main() { 9 | const rawExamplePerson = { 10 | 'adult': false, 11 | 'gender': 1, 12 | 'id': 224513, 13 | 'known_for': [], 14 | 'known_for_department': 'Acting', 15 | 'name': 'Ana de Armas', 16 | 'popularity': 493.285, 17 | 'profile_path': '/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg', 18 | 'biography': null, 19 | 'birthday': '1988-04-30', 20 | 'deathday': '2001-04-30', 21 | 'homepage': null, 22 | 'imdb_id': null, 23 | 'place_of_birth': null, 24 | }; 25 | 26 | final examplePerson = Person( 27 | id: 224513, 28 | adult: false, 29 | gender: Gender.female, 30 | knownFor: const [], 31 | knownForDepartment: 'Acting', 32 | name: 'Ana de Armas', 33 | popularity: 493.285, 34 | profilePath: '/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg', 35 | birthday: DateTime(1988, 4, 30), 36 | deathDate: DateTime(2001, 4, 30), 37 | homepage: null, 38 | imdbId: null, 39 | placeOfBirth: null, 40 | ); 41 | 42 | test('can parse data fromJson', () { 43 | expect(Person.fromJson(rawExamplePerson), equals(examplePerson)); 44 | }); 45 | 46 | test('can convert data toJson', () { 47 | expect(examplePerson.toJson(), equals(rawExamplePerson)); 48 | }); 49 | 50 | test('returns null dates for invalid formats', () { 51 | final invalidDateExamplePerson = { 52 | ...rawExamplePerson, 53 | 'deathday': 'invalid!', 54 | 'birthday': 'invalid!', 55 | }; 56 | 57 | expect(Person.fromJson(invalidDateExamplePerson).birthday, isNull); 58 | expect(Person.fromJson(invalidDateExamplePerson).deathDate, isNull); 59 | }); 60 | 61 | test('can populate avatar and cover from profilePath with correct image urls', 62 | () { 63 | final personWithGeneratedImages = 64 | examplePerson.populateImages(DummyConfigs.imageConfigs); 65 | 66 | final avatarUrl = 67 | // ignore: lines_longer_than_80_chars 68 | '${DummyConfigs.imageConfigs.secureBaseUrl}${Person.avatarSize.name}${examplePerson.profilePath}'; 69 | // https://image.tmdb.org/t/p/h632/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg 70 | 71 | expect( 72 | personWithGeneratedImages.avatar, 73 | equals(avatarUrl), 74 | ); 75 | 76 | final coverUrl = 77 | // ignore: lines_longer_than_80_chars 78 | '${DummyConfigs.imageConfigs.secureBaseUrl}${Person.coverSize.name}${examplePerson.profilePath}'; 79 | // https://image.tmdb.org/t/p/original/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg 80 | 81 | expect( 82 | personWithGeneratedImages.cover, 83 | equals(coverUrl), 84 | ); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /test/features/people/providers/current_popular_person_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/features/people/providers/current_popular_person_provider.dart'; 4 | 5 | void main() { 6 | test('current popular person provider throws UnimplementedError initially', 7 | () { 8 | final providerContainer = ProviderContainer(); 9 | 10 | addTearDown(providerContainer.dispose); 11 | 12 | expect( 13 | () => providerContainer.read(currentPopularPersonProvider), 14 | throwsA(isA()), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/features/people/providers/paginated_popular_people_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/core/models/paginated_response.dart'; 5 | import 'package:movies_app/features/people/models/person.dart'; 6 | import 'package:movies_app/features/people/providers/paginated_popular_people_provider.dart'; 7 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 8 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 9 | 10 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 11 | import '../../../test-utils/dummy-data/dummy_people.dart'; 12 | import '../../../test-utils/mocks.dart'; 13 | 14 | void main() { 15 | final PeopleRepository mockPeopleRepository = MockPeopleRepository(); 16 | 17 | setUp(() { 18 | when( 19 | () => mockPeopleRepository.getPopularPeople( 20 | forceRefresh: false, 21 | page: 1, 22 | imageConfigs: DummyConfigs.imageConfigs, 23 | ), 24 | ).thenAnswer((_) async => DummyPeople.paginatedPopularPeopleResponse); 25 | }); 26 | 27 | test('fetches paginated popular people', () async { 28 | final popularPeopleListener = 29 | Listener>>(); 30 | 31 | final providerContainer = ProviderContainer( 32 | overrides: [ 33 | peopleRepositoryProvider.overrideWithValue(mockPeopleRepository), 34 | tmdbConfigsProvider.overrideWithProvider(dummyTmdbConfigsProvider), 35 | ], 36 | ); 37 | 38 | addTearDown(providerContainer.dispose); 39 | 40 | providerContainer.listen>>( 41 | paginatedPopularPeopleProvider(0), 42 | popularPeopleListener, 43 | fireImmediately: true, 44 | ); 45 | 46 | // Perform first reading, expects loading state 47 | final firstReading = 48 | providerContainer.read(paginatedPopularPeopleProvider(0)); 49 | expect(firstReading, const AsyncValue>.loading()); 50 | 51 | // Listener was fired from `null` to loading AsyncValue 52 | verify( 53 | () => popularPeopleListener( 54 | null, 55 | const AsyncValue>.loading(), 56 | ), 57 | ).called(1); 58 | 59 | // Perform second reading, by waiting for the request, expects fetched data 60 | final secondReading = 61 | await providerContainer.read(paginatedPopularPeopleProvider(0).future); 62 | expect(secondReading, DummyPeople.paginatedPopularPeopleResponse); 63 | 64 | // Listener was fired from loading to fetched values 65 | verify( 66 | () => popularPeopleListener( 67 | const AsyncValue>.loading(), 68 | AsyncValue>.data( 69 | DummyPeople.paginatedPopularPeopleResponse, 70 | ), 71 | ), 72 | ).called(1); 73 | 74 | // No more further listener events fired 75 | verifyNoMoreInteractions(popularPeopleListener); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /test/features/people/providers/person_details_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | import 'package:movies_app/features/people/providers/person_details_provider.dart'; 6 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 7 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 8 | 9 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 10 | import '../../../test-utils/dummy-data/dummy_people.dart'; 11 | import '../../../test-utils/mocks.dart'; 12 | 13 | void main() { 14 | final PeopleRepository mockPeopleRepository = MockPeopleRepository(); 15 | 16 | setUp(() { 17 | when( 18 | () => mockPeopleRepository.getPersonDetails( 19 | DummyPeople.person1.id!, 20 | forceRefresh: false, 21 | imageConfigs: DummyConfigs.imageConfigs, 22 | ), 23 | ).thenAnswer((_) async => DummyPeople.person1); 24 | }); 25 | 26 | test('fetches person details', () async { 27 | final personListener = Listener>(); 28 | 29 | final providerContainer = ProviderContainer( 30 | overrides: [ 31 | peopleRepositoryProvider.overrideWithValue(mockPeopleRepository), 32 | tmdbConfigsProvider.overrideWithProvider(dummyTmdbConfigsProvider), 33 | ], 34 | ); 35 | 36 | addTearDown(providerContainer.dispose); 37 | 38 | providerContainer.listen>( 39 | personDetailsProvider(DummyPeople.person1.id!), 40 | personListener, 41 | fireImmediately: true, 42 | ); 43 | 44 | // Perform first reading, expects loading state 45 | final firstReading = 46 | providerContainer.read(personDetailsProvider(DummyPeople.person1.id!)); 47 | expect(firstReading, const AsyncValue.loading()); 48 | 49 | // Listener was fired from `null` to loading AsyncValue 50 | verify( 51 | () => personListener( 52 | null, 53 | const AsyncValue.loading(), 54 | ), 55 | ).called(1); 56 | 57 | // Perform second reading, by waiting for the request, expects fetched data 58 | final secondReading = await providerContainer 59 | .read(personDetailsProvider(DummyPeople.person1.id!).future); 60 | expect(secondReading, DummyPeople.person1); 61 | 62 | // Listener was fired from loading to fetched values 63 | verify( 64 | () => personListener( 65 | const AsyncValue.loading(), 66 | AsyncValue.data(DummyPeople.person1), 67 | ), 68 | ).called(1); 69 | 70 | // No more further listener events fired 71 | verifyNoMoreInteractions(personListener); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/features/people/providers/person_images_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/features/people/models/person_image.dart'; 5 | import 'package:movies_app/features/people/providers/person_images_provider.dart'; 6 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 7 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 8 | 9 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 10 | import '../../../test-utils/dummy-data/dummy_people.dart'; 11 | import '../../../test-utils/mocks.dart'; 12 | 13 | void main() { 14 | final PeopleRepository mockPeopleRepository = MockPeopleRepository(); 15 | 16 | setUp(() { 17 | when( 18 | () => mockPeopleRepository.getPersonImages( 19 | DummyPeople.person1.id!, 20 | forceRefresh: false, 21 | imageConfigs: DummyConfigs.imageConfigs, 22 | ), 23 | ).thenAnswer((_) async => DummyPeople.personImages); 24 | }); 25 | 26 | test('fetches paginated popular people', () async { 27 | final personImagesListener = Listener>>(); 28 | 29 | final providerContainer = ProviderContainer( 30 | overrides: [ 31 | peopleRepositoryProvider.overrideWithValue(mockPeopleRepository), 32 | tmdbConfigsProvider.overrideWithProvider(dummyTmdbConfigsProvider), 33 | ], 34 | ); 35 | 36 | addTearDown(providerContainer.dispose); 37 | 38 | providerContainer.listen>>( 39 | personImagesProvider(DummyPeople.person1.id!), 40 | personImagesListener, 41 | fireImmediately: true, 42 | ); 43 | 44 | // Perform first reading, expects loading state 45 | final firstReading = 46 | providerContainer.read(personImagesProvider(DummyPeople.person1.id!)); 47 | expect(firstReading, const AsyncValue>.loading()); 48 | 49 | // Listener was fired from `null` to loading AsyncValue 50 | verify( 51 | () => personImagesListener( 52 | null, 53 | const AsyncValue>.loading(), 54 | ), 55 | ).called(1); 56 | 57 | // Perform second reading, by waiting for the request, expects fetched data 58 | final secondReading = await providerContainer 59 | .read(personImagesProvider(DummyPeople.person1.id!).future); 60 | expect(secondReading, DummyPeople.personImages); 61 | 62 | // Listener was fired from loading to fetched values 63 | verify( 64 | () => personImagesListener( 65 | const AsyncValue>.loading(), 66 | AsyncValue>.data(DummyPeople.personImages), 67 | ), 68 | ).called(1); 69 | 70 | // No more further listener events fired 71 | verifyNoMoreInteractions(personImagesListener); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/features/people/providers/popular_people_count_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/models/paginated_response.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | import 'package:movies_app/features/people/providers/paginated_popular_people_provider.dart'; 6 | import 'package:movies_app/features/people/providers/popular_people_count_provider.dart'; 7 | 8 | import '../../../test-utils/dummy-data/dummy_people.dart'; 9 | 10 | void main() { 11 | test('returns correct results count', () async { 12 | final providerContainer = ProviderContainer( 13 | overrides: [ 14 | paginatedPopularPeopleProvider(0).overrideWithProvider( 15 | FutureProvider>( 16 | (ref) async => 17 | Future.value(DummyPeople.paginatedPopularPeopleResponse), 18 | ), 19 | ), 20 | ], 21 | ); 22 | 23 | addTearDown(providerContainer.dispose); 24 | 25 | providerContainer.read(popularPeopleCountProvider); 26 | await Future.delayed(Duration.zero); 27 | final count = providerContainer.read(popularPeopleCountProvider).value; 28 | 29 | expect( 30 | count, 31 | equals(DummyPeople.paginatedPopularPeopleResponse.totalResults), 32 | ); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/features/people/providers/popular_people_scroll_controller_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:movies_app/features/people/providers/popular_people_list_scroll_controller_provider.dart'; 5 | 6 | void main() { 7 | test('popularPeopleScrollControllerProvider is a ScrollController', () { 8 | final container = ProviderContainer(); 9 | 10 | final scrollController = 11 | container.read(popularPeopleScrollControllerProvider); 12 | 13 | expect(scrollController, isA()); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/features/people/repositories/http_people_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:movies_app/core/services/http/http_service.dart'; 4 | import 'package:movies_app/features/people/repositories/http_people_repository.dart'; 5 | 6 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 7 | import '../../../test-utils/dummy-data/dummy_people.dart'; 8 | import '../../../test-utils/mocks.dart'; 9 | 10 | void main() { 11 | final HttpService mockHttpService = MockHttpService(); 12 | final httpPeopleRepository = HttpPeopleRepository( 13 | mockHttpService, 14 | ); 15 | 16 | test('fetches paginated popular people', () async { 17 | const page = 1; 18 | when( 19 | () => mockHttpService.get( 20 | '${httpPeopleRepository.path}/popular', 21 | queryParameters: { 22 | 'page': page, 23 | 'api_key': '', 24 | }, 25 | ), 26 | ).thenAnswer( 27 | (_) async => { 28 | 'page': page, 29 | 'results': DummyPeople.rawPopularPeople1, 30 | 'total_pages': 1, 31 | 'total_results': 10, 32 | }, 33 | ); 34 | 35 | final paginatedPopularPeople = await httpPeopleRepository.getPopularPeople( 36 | page: 1, 37 | imageConfigs: DummyConfigs.imageConfigs, 38 | ); 39 | 40 | expect( 41 | paginatedPopularPeople.results, 42 | equals(DummyPeople.popularPeople1), 43 | ); 44 | }); 45 | 46 | test('fetches person details', () async { 47 | when( 48 | () => mockHttpService.get( 49 | '${httpPeopleRepository.path}/${DummyPeople.person1.id}', 50 | queryParameters: { 51 | 'api_key': '', 52 | }, 53 | ), 54 | ).thenAnswer( 55 | (_) async => DummyPeople.rawPerson1, 56 | ); 57 | 58 | final person = await httpPeopleRepository.getPersonDetails( 59 | DummyPeople.person1.id!, 60 | imageConfigs: DummyConfigs.imageConfigs, 61 | ); 62 | expect(person, equals(DummyPeople.person1)); 63 | }); 64 | 65 | test('fetches person images', () async { 66 | when( 67 | () => mockHttpService.get( 68 | '${httpPeopleRepository.path}/${DummyPeople.person1.id}/images', 69 | queryParameters: { 70 | 'api_key': '', 71 | }, 72 | ), 73 | ).thenAnswer( 74 | (_) async => { 75 | 'profiles': [ 76 | DummyPeople.rawDummyPersonImage1, 77 | DummyPeople.rawDummyPersonImage2, 78 | ], 79 | }, 80 | ); 81 | 82 | final personImages = await httpPeopleRepository.getPersonImages( 83 | DummyPeople.person1.id!, 84 | imageConfigs: DummyConfigs.imageConfigs, 85 | ); 86 | 87 | expect( 88 | personImages, 89 | equals([ 90 | DummyPeople.dummyPersonImage1, 91 | DummyPeople.dummyPersonImage2, 92 | ]), 93 | ); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /test/features/people/repositories/people_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/features/people/repositories/http_people_repository.dart'; 4 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 5 | 6 | void main() { 7 | test('peopleRepositoryProvider is a HttpPeopleRepository', () { 8 | final providerContainer = ProviderContainer(); 9 | 10 | addTearDown(providerContainer.dispose); 11 | 12 | expect( 13 | providerContainer.read(peopleRepositoryProvider), 14 | isA(), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/features/people/views/pages/person_details_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/widgets/error_view.dart'; 4 | import 'package:movies_app/features/people/models/person.dart'; 5 | import 'package:movies_app/features/people/providers/person_details_provider.dart'; 6 | import 'package:movies_app/features/people/views/pages/person_details_page.dart'; 7 | 8 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 9 | import '../../../../test-utils/pump_app.dart'; 10 | 11 | void main() { 12 | testWidgets('renders ErrorView on error', (WidgetTester tester) async { 13 | await tester.pumpProviderApp( 14 | PersonDetailsPage( 15 | personId: DummyPeople.person1.id!, 16 | personName: DummyPeople.person1.name, 17 | personAvatar: null, 18 | personGender: DummyPeople.person1.gender, 19 | ), 20 | overrides: [ 21 | personDetailsProvider(DummyPeople.person1.id!).overrideWithProvider( 22 | FutureProvider( 23 | (ref) => throw Exception(), 24 | ), 25 | ), 26 | ], 27 | ); 28 | 29 | await tester.pumpAndSettle(); 30 | expect(find.byType(ErrorView), findsOneWidget); 31 | }); 32 | 33 | testWidgets('renders person details', (WidgetTester tester) async { 34 | await tester.pumpProviderApp( 35 | PersonDetailsPage( 36 | personId: DummyPeople.person1.id!, 37 | personName: DummyPeople.person1.name, 38 | personAvatar: null, 39 | personGender: DummyPeople.person1.gender, 40 | ), 41 | overrides: [ 42 | personDetailsProvider(DummyPeople.person1.id!).overrideWithProvider( 43 | FutureProvider( 44 | (ref) async => Future.value(DummyPeople.person1), 45 | ), 46 | ), 47 | ], 48 | ); 49 | 50 | await tester.pumpAndSettle(); 51 | 52 | expect(find.text(DummyPeople.person1.knownForDepartment!), findsOneWidget); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/features/people/views/pages/popular_people_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:movies_app/features/people/providers/popular_people_count_provider.dart'; 5 | import 'package:movies_app/features/people/providers/popular_people_list_scroll_controller_provider.dart'; 6 | import 'package:movies_app/features/people/views/pages/popular_people_page.dart'; 7 | import 'package:movies_app/features/people/views/widgets/popular_people_app_bar.dart'; 8 | 9 | import '../../../../test-utils/pump_app.dart'; 10 | 11 | void main() { 12 | testWidgets('can scroll to top', (WidgetTester tester) async { 13 | await tester.pumpProviderApp( 14 | const PopularPeoplePage(), 15 | overrides: [ 16 | popularPeopleCountProvider.overrideWithValue( 17 | const AsyncValue.data(20), 18 | ), 19 | ], 20 | ); 21 | 22 | await tester.pumpAndSettle(); 23 | 24 | final ref = tester 25 | .element(find.byType(PopularPeopleAppBar)); 26 | 27 | final scrollController = ref.watch(popularPeopleScrollControllerProvider); 28 | 29 | // Make sure the scroll controller has clients 30 | expect(scrollController.hasClients, isTrue); 31 | scrollController.jumpTo(300); 32 | 33 | expect(scrollController.offset, equals(300)); 34 | 35 | final appBarTitleGestureDetectorFinder = 36 | find.byKey(const ValueKey('__app_bar_title_gesture_detector__')); 37 | expect(appBarTitleGestureDetectorFinder, findsOneWidget); 38 | 39 | await tester.tap(appBarTitleGestureDetectorFinder); 40 | await tester.pump(const Duration(milliseconds: 300)); 41 | await tester.pumpAndSettle(); 42 | 43 | expect(scrollController.offset, equals(0)); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_avatar_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/views/widgets/person_avatar.dart'; 4 | 5 | import '../../../../test-utils/pump_app.dart'; 6 | 7 | void main() { 8 | testWidgets( 9 | 'renders asset image for null avatarUrl', 10 | (WidgetTester tester) async { 11 | await tester.pumpApp( 12 | const PersonAvatar(null), 13 | ); 14 | 15 | await tester.pumpAndSettle(); 16 | expect( 17 | find.byType(AppCachedNetworkImage), 18 | findsNothing, 19 | ); 20 | }, 21 | ); 22 | 23 | testWidgets( 24 | 'renders network image for an avatarUrl', 25 | (WidgetTester tester) async { 26 | await tester.pumpApp( 27 | const PersonAvatar('avatar_url'), 28 | ); 29 | 30 | await tester.pump(); 31 | expect( 32 | find.byType(AppCachedNetworkImage), 33 | findsOneWidget, 34 | ); 35 | }, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_cover_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/views/widgets/person_cover.dart'; 4 | 5 | import '../../../../test-utils/pump_app.dart'; 6 | 7 | void main() { 8 | testWidgets( 9 | 'renders asset image for null cover url', 10 | (WidgetTester tester) async { 11 | await tester.pumpApp( 12 | const PersonCover(null), 13 | ); 14 | 15 | await tester.pumpAndSettle(); 16 | 17 | expect( 18 | find.byType(AppCachedNetworkImage), 19 | findsNothing, 20 | ); 21 | }, 22 | ); 23 | 24 | testWidgets( 25 | 'renders network image for a cover url', 26 | (WidgetTester tester) async { 27 | await tester.pumpApp( 28 | const PersonCover('avatar_url'), 29 | ); 30 | 31 | await tester.pump(); 32 | 33 | expect( 34 | find.byType(AppCachedNetworkImage), 35 | findsOneWidget, 36 | ); 37 | }, 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_images_grid_item_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/models/person_image.dart'; 4 | import 'package:movies_app/features/people/views/widgets/person_images_grid_item.dart'; 5 | 6 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 7 | import '../../../../test-utils/pump_app.dart'; 8 | 9 | void main() { 10 | testWidgets( 11 | 'renders asset image for null thumbnail', 12 | (WidgetTester tester) async { 13 | await tester.pumpApp( 14 | PersonImagesGridItem( 15 | PersonImage.fromJson(DummyPeople.rawDummyPersonImage1), 16 | ), 17 | ); 18 | 19 | await tester.pumpAndSettle(); 20 | expect( 21 | find.byType(AppCachedNetworkImage), 22 | findsNothing, 23 | ); 24 | }, 25 | ); 26 | 27 | testWidgets( 28 | 'renders network image for a thumbnail url', 29 | (WidgetTester tester) async { 30 | await tester.pumpApp( 31 | PersonImagesGridItem(DummyPeople.dummyPersonImage1), 32 | ); 33 | 34 | await tester.pump(); 35 | expect( 36 | find.byType(AppCachedNetworkImage), 37 | findsOneWidget, 38 | ); 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_images_grid_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/features/people/views/pages/person_images_slider_page.dart'; 4 | import 'package:movies_app/features/people/views/widgets/person_images_grid.dart'; 5 | 6 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 7 | import '../../../../test-utils/pump_app.dart'; 8 | 9 | void main() { 10 | testWidgets( 11 | 'navigates to PersonImagesSliderPage', 12 | (WidgetTester tester) async { 13 | await tester.pumpApp( 14 | PersonImagesGrid(DummyPeople.personImagesWithoutImages), 15 | ); 16 | 17 | await tester.pumpAndSettle(); 18 | 19 | await tester.tap(find.byType(GestureDetector).first); 20 | await tester.pumpAndSettle(); 21 | 22 | expect(find.byType(PersonImagesSliderPage), findsOneWidget); 23 | }, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_images_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/widgets/error_view.dart'; 4 | import 'package:movies_app/features/people/models/person_image.dart'; 5 | import 'package:movies_app/features/people/providers/person_images_provider.dart'; 6 | import 'package:movies_app/features/people/views/widgets/person_images.dart'; 7 | import 'package:movies_app/features/people/views/widgets/person_images_grid.dart'; 8 | 9 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 10 | import '../../../../test-utils/pump_app.dart'; 11 | 12 | void main() { 13 | testWidgets('renders ErrorView on error', (WidgetTester tester) async { 14 | await tester.pumpProviderApp( 15 | PersonImages(DummyPeople.person1.id!), 16 | overrides: [ 17 | personImagesProvider(DummyPeople.person1.id!).overrideWithProvider( 18 | FutureProvider>( 19 | (ref) => throw Exception(), 20 | ), 21 | ), 22 | ], 23 | ); 24 | 25 | await tester.pumpAndSettle(); 26 | expect(find.byType(ErrorView), findsOneWidget); 27 | }); 28 | 29 | testWidgets('renders person images grid', (WidgetTester tester) async { 30 | await tester.pumpProviderApp( 31 | PersonImages(DummyPeople.person1.id!), 32 | overrides: [ 33 | personImagesProvider(DummyPeople.person1.id!).overrideWithProvider( 34 | FutureProvider>( 35 | (ref) async => Future.value(DummyPeople.personImagesWithoutImages), 36 | ), 37 | ), 38 | ], 39 | ); 40 | 41 | await tester.pumpAndSettle(); 42 | expect(find.byType(PersonImagesGrid), findsOneWidget); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/person_media_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movies_app/core/widgets/app_cached_network_image.dart'; 3 | import 'package:movies_app/features/people/models/person.dart'; 4 | import 'package:movies_app/features/people/views/widgets/person_media.dart'; 5 | 6 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 7 | import '../../../../test-utils/pump_app.dart'; 8 | 9 | void main() { 10 | testWidgets('renders list of person media', (WidgetTester tester) async { 11 | await tester.pumpApp( 12 | PersonMedia(DummyPeople.popularPeople1[0].knownFor), 13 | ); 14 | 15 | await tester.pump(); 16 | expect( 17 | find.byType(AppCachedNetworkImage), 18 | findsNWidgets(DummyPeople.popularPeople1[0].knownFor.length), 19 | ); 20 | }); 21 | 22 | testWidgets( 23 | 'renders list of person media without images when not available', 24 | (WidgetTester tester) async { 25 | await tester.pumpApp( 26 | PersonMedia(Person.fromJson(DummyPeople.rawPopularPeople1[0]).knownFor), 27 | ); 28 | 29 | await tester.pump(); 30 | expect( 31 | find.byType(AppCachedNetworkImage), 32 | findsNothing, 33 | ); 34 | }, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/popular_people_list_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/core/widgets/error_view.dart'; 4 | import 'package:movies_app/features/people/providers/popular_people_count_provider.dart'; 5 | import 'package:movies_app/features/people/views/widgets/popular_people_list.dart'; 6 | 7 | import '../../../../test-utils/pump_app.dart'; 8 | 9 | void main() { 10 | testWidgets( 11 | 'Renders ErrorView on provider error', 12 | (WidgetTester tester) async { 13 | await tester.pumpProviderApp( 14 | const PopularPeopleList(), 15 | overrides: [ 16 | popularPeopleCountProvider.overrideWithProvider( 17 | Provider>((ref) => throw Exception()), 18 | ), 19 | ], 20 | ); 21 | 22 | await tester.pumpAndSettle(); 23 | expect(find.byType(ErrorView), findsOneWidget); 24 | }, 25 | // Todo: make this test work 26 | skip: true, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/popular_person_list_item_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:movies_app/core/widgets/error_view.dart'; 5 | import 'package:movies_app/features/people/models/person.dart'; 6 | import 'package:movies_app/features/people/providers/current_popular_person_provider.dart'; 7 | import 'package:movies_app/features/people/views/pages/person_details_page.dart'; 8 | import 'package:movies_app/features/people/views/widgets/popular_person_list_item.dart'; 9 | 10 | import '../../../../test-utils/dummy-data/dummy_people.dart'; 11 | import '../../../../test-utils/pump_app.dart'; 12 | 13 | void main() { 14 | testWidgets( 15 | 'renders ErrorView on FormatException', 16 | (WidgetTester tester) async { 17 | await tester.pumpProviderApp( 18 | const PopularPersonListItem(), 19 | overrides: [ 20 | currentPopularPersonProvider.overrideWithProvider( 21 | Provider>( 22 | (ref) => throw const FormatException(), 23 | ), 24 | ), 25 | ], 26 | ); 27 | 28 | await tester.pumpAndSettle(); 29 | expect(find.byType(ErrorView), findsOneWidget); 30 | }, 31 | // Todo: make this test work 32 | skip: true, 33 | ); 34 | 35 | testWidgets('renders person data', (WidgetTester tester) async { 36 | await tester.pumpProviderApp( 37 | const Material( 38 | child: PopularPersonListItem(), 39 | ), 40 | overrides: [ 41 | currentPopularPersonProvider.overrideWithValue( 42 | AsyncValue.data(Person.fromJson(DummyPeople.rawPerson1)), 43 | ), 44 | ], 45 | ); 46 | 47 | await tester.pumpAndSettle(); 48 | expect(find.text(DummyPeople.person1.name), findsOneWidget); 49 | }); 50 | 51 | testWidgets('navigates to person details page', (WidgetTester tester) async { 52 | await tester.pumpProviderApp( 53 | const Material( 54 | child: PopularPersonListItem(), 55 | ), 56 | overrides: [ 57 | currentPopularPersonProvider.overrideWithValue( 58 | AsyncValue.data(Person.fromJson(DummyPeople.rawPerson1)), 59 | ), 60 | ], 61 | ); 62 | 63 | await tester.pumpAndSettle(); 64 | final inkWell = find.byType(InkWell); 65 | 66 | expect(inkWell, findsOneWidget); 67 | 68 | await tester.tap(inkWell); 69 | await tester.pumpAndSettle(); 70 | 71 | expect(find.byType(PersonDetailsPage), findsOneWidget); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/features/people/views/widgets/save_image_slider_action_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/core/services/media/media_service.dart'; 5 | import 'package:movies_app/features/people/views/widgets/save_image_slider_action.dart'; 6 | import 'package:movies_app/features/people/views/widgets/slider_action.dart'; 7 | 8 | import '../../../../test-utils/mocks.dart'; 9 | import '../../../../test-utils/pump_app.dart'; 10 | 11 | void main() { 12 | final mockMediaServiceProvider = MockMediaServiceProvider(); 13 | 14 | testWidgets('can save image to gallery', (WidgetTester tester) async { 15 | when(() => mockMediaServiceProvider.saveNetworkImageToGallery('image_url')) 16 | .thenAnswer((_) async {}); 17 | 18 | await tester.pumpProviderApp( 19 | const Scaffold( 20 | body: SaveImageSliderAction(imageUrl: 'image_url'), 21 | ), 22 | overrides: [ 23 | mediaServiceProvider.overrideWithValue(mockMediaServiceProvider), 24 | ], 25 | ); 26 | 27 | await tester.pumpAndSettle(); 28 | await tester.tap(find.byType(SliderAction)); 29 | await tester.pump(); 30 | expect(find.byType(SnackBar), findsOneWidget); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/features/tmdb-configs/providers/tmdb_configs_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 5 | import 'package:movies_app/features/tmdb-configs/providers/tmdb_configs_provider.dart'; 6 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 7 | 8 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 9 | import '../../../test-utils/mocks.dart'; 10 | 11 | void main() { 12 | final TMDBConfigsRepository mockTMDBConfigsRepository = 13 | MockTMDBConfigsRepository(); 14 | 15 | test('fetches TMDB configs', () async { 16 | when(() => mockTMDBConfigsRepository.get(forceRefresh: false)) 17 | .thenAnswer((_) async => DummyConfigs.tmdbConfigs); 18 | 19 | final tmdbConfigsListener = Listener>(); 20 | 21 | final providerContainer = ProviderContainer( 22 | overrides: [ 23 | // Replace the TMDB Configs repository with the Mock Repository 24 | tmdbConfigsRepositoryProvider 25 | .overrideWithValue(mockTMDBConfigsRepository), 26 | ], 27 | ); 28 | 29 | addTearDown(providerContainer.dispose); 30 | 31 | providerContainer.listen>( 32 | tmdbConfigsProvider, 33 | tmdbConfigsListener, 34 | fireImmediately: true, 35 | ); 36 | 37 | // Perform first reading, expects loading state 38 | final firstReading = providerContainer.read(tmdbConfigsProvider); 39 | expect(firstReading, const AsyncValue.loading()); 40 | 41 | // Listener was fired from `null` to loading AsyncValue 42 | verify( 43 | () => tmdbConfigsListener( 44 | null, 45 | const AsyncValue.loading(), 46 | ), 47 | ).called(1); 48 | 49 | // Perform second reading, by waiting for the request, expects fetched data 50 | final secondReading = 51 | await providerContainer.read(tmdbConfigsProvider.future); 52 | expect(secondReading, DummyConfigs.tmdbConfigs); 53 | 54 | // Listener was fired from loading to fetched values 55 | verify( 56 | () => tmdbConfigsListener( 57 | const AsyncValue.loading(), 58 | const AsyncValue.data(DummyConfigs.tmdbConfigs), 59 | ), 60 | ).called(1); 61 | 62 | // No more further listener events fired 63 | verifyNoMoreInteractions(tmdbConfigsListener); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/features/tmdb-configs/repositories/http_tmdb_configs_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:movies_app/core/services/http/http_service.dart'; 4 | import 'package:movies_app/features/tmdb-configs/models/tmdb_configs.dart'; 5 | import 'package:movies_app/features/tmdb-configs/repositories/http_tmdb_configs_repository.dart'; 6 | 7 | import '../../../test-utils/dummy-data/dummy_configs.dart'; 8 | import '../../../test-utils/mocks.dart'; 9 | 10 | void main() { 11 | final HttpService mockHttpService = MockHttpService(); 12 | final httpTMDBConfigsRepository = HttpTMDBConfigsRepository(mockHttpService); 13 | 14 | test('fetches TMDB configs', () async { 15 | when( 16 | () => mockHttpService.get( 17 | httpTMDBConfigsRepository.path, 18 | queryParameters: { 19 | 'api_key': '', 20 | }, 21 | ), 22 | ).thenAnswer( 23 | (_) async => { 24 | 'images': DummyConfigs.rawImageConfigs, 25 | }, 26 | ); 27 | 28 | final configs = await httpTMDBConfigsRepository.get(); 29 | 30 | expect( 31 | configs, 32 | equals(const TMDBConfigs(images: DummyConfigs.imageConfigs)), 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/features/tmdb-configs/repositories/tmdb_configs_repository_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:movies_app/features/tmdb-configs/repositories/http_tmdb_configs_repository.dart'; 4 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 5 | 6 | void main() { 7 | test('tmdbConfigsRepositoryProvider is a HttpTMDBConfigsRepository', () { 8 | final providerContainer = ProviderContainer(); 9 | 10 | addTearDown(providerContainer.dispose); 11 | 12 | expect( 13 | providerContainer.read(tmdbConfigsRepositoryProvider), 14 | isA(), 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/movies_app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:movies_app/core/widgets/app_loader.dart'; 5 | import 'package:movies_app/core/widgets/error_view.dart'; 6 | import 'package:movies_app/features/people/views/pages/popular_people_page.dart'; 7 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 8 | import 'package:movies_app/movies_app.dart'; 9 | 10 | import 'test-utils/dummy-data/dummy_configs.dart'; 11 | import 'test-utils/mocks.dart'; 12 | 13 | void main() { 14 | final TMDBConfigsRepository mockTMDBConfigsRepository = 15 | MockTMDBConfigsRepository(); 16 | 17 | testWidgets('renders ErrorView for request error', 18 | (WidgetTester tester) async { 19 | when(() => mockTMDBConfigsRepository.get(forceRefresh: false)) 20 | .thenThrow('An Error Occurred!'); 21 | 22 | await tester.pumpWidget( 23 | ProviderScope( 24 | overrides: [ 25 | // Replace the TMDB Configs repository with the Mock Repository 26 | tmdbConfigsRepositoryProvider 27 | .overrideWithValue(mockTMDBConfigsRepository), 28 | ], 29 | child: const MoviesApp(), 30 | ), 31 | ); 32 | 33 | // Initially loading 34 | expect(find.byType(AppLoader), findsOneWidget); 35 | 36 | // Re-render to make sure fetching is finished 37 | await tester.pumpAndSettle(); 38 | 39 | // Shows error view 40 | expect(find.byType(ErrorView), findsOneWidget); 41 | }); 42 | 43 | testWidgets( 44 | 'renders PopularPeoplePage widget on request success', 45 | (WidgetTester tester) async { 46 | when(() => mockTMDBConfigsRepository.get(forceRefresh: false)) 47 | .thenAnswer((_) async => DummyConfigs.tmdbConfigs); 48 | 49 | await tester.pumpWidget( 50 | ProviderScope( 51 | overrides: [ 52 | // Replace the TMDB Configs repository with the Mock Repository 53 | tmdbConfigsRepositoryProvider 54 | .overrideWithValue(mockTMDBConfigsRepository), 55 | ], 56 | child: const MoviesApp(), 57 | ), 58 | ); 59 | 60 | // Initially loading 61 | expect(find.byType(AppLoader), findsOneWidget); 62 | 63 | // Re-render to make sure fetching is finished 64 | await tester.pumpAndSettle(); 65 | 66 | expect(find.byType(PopularPeoplePage), findsOneWidget); 67 | }, 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /test/test-utils/dummy-data/json/example_configuration_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "base_url": "http://image.tmdb.org/t/p/", 4 | "secure_base_url": "https://image.tmdb.org/t/p/", 5 | "backdrop_sizes": [ 6 | "w300", 7 | "w780", 8 | "w1280", 9 | "original" 10 | ], 11 | "logo_sizes": [ 12 | "w45", 13 | "w92", 14 | "w154", 15 | "w185", 16 | "w300", 17 | "w500", 18 | "original" 19 | ], 20 | "poster_sizes": [ 21 | "w92", 22 | "w154", 23 | "w185", 24 | "w342", 25 | "w500", 26 | "w780", 27 | "original" 28 | ], 29 | "profile_sizes": [ 30 | "w45", 31 | "w185", 32 | "h632", 33 | "original" 34 | ], 35 | "still_sizes": [ 36 | "w92", 37 | "w185", 38 | "w300", 39 | "original" 40 | ] 41 | }, 42 | "change_keys": [ 43 | "adult", 44 | "air_date", 45 | "also_known_as", 46 | "alternative_titles", 47 | "biography", 48 | "birthday", 49 | "budget", 50 | "cast", 51 | "certifications", 52 | "character_names", 53 | "created_by", 54 | "crew", 55 | "deathday", 56 | "episode", 57 | "episode_number", 58 | "episode_run_time", 59 | "freebase_id", 60 | "freebase_mid", 61 | "general", 62 | "genres", 63 | "guest_stars", 64 | "homepage", 65 | "images", 66 | "imdb_id", 67 | "languages", 68 | "name", 69 | "network", 70 | "origin_country", 71 | "original_name", 72 | "original_title", 73 | "overview", 74 | "parts", 75 | "place_of_birth", 76 | "plot_keywords", 77 | "production_code", 78 | "production_companies", 79 | "production_countries", 80 | "releases", 81 | "revenue", 82 | "runtime", 83 | "season", 84 | "season_number", 85 | "season_regular", 86 | "spoken_languages", 87 | "status", 88 | "tagline", 89 | "title", 90 | "translations", 91 | "tvdb_id", 92 | "tvrage_id", 93 | "type", 94 | "video", 95 | "videos" 96 | ] 97 | } -------------------------------------------------------------------------------- /test/test-utils/dummy-data/json/example_person_details_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "adult": false, 3 | "also_known_as": [ 4 | "Ana Celia de Armas", 5 | "Ana Celia de Armas Caso", 6 | "아나 디 아르마스", 7 | "Ана де Армас", 8 | "Άνα ντε Άρμας", 9 | "安娜·德·阿玛斯", 10 | "アナ・デ・アルマス" 11 | ], 12 | "biography": "Ana de Armas was born in Cuba on April 30, 1988. At the age of 14, she began her studies at the National Theatre School of Havana, where she graduated after 4 years. She made her film debut with Una rosa de Francia (2006), which was directed by Manuel Gutiérrez Aragón. In 2006 she moved to Spain where she continued her film career, and started doing television. She currently lives between Madrid and Barcelona. Ana is known for her roles in Blade Runner 2049 (2017), Knives Out (2019), and No Time to Die (2021).", 13 | "birthday": "1988-04-30", 14 | "deathday": null, 15 | "gender": 1, 16 | "homepage": null, 17 | "id": 224513, 18 | "imdb_id": "nm1869101", 19 | "known_for_department": "Acting", 20 | "name": "Ana de Armas", 21 | "place_of_birth": "Santa Cruz del Norte, Cuba", 22 | "popularity": 493.285, 23 | "profile_path": "/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg" 24 | } -------------------------------------------------------------------------------- /test/test-utils/dummy-data/json/example_person_images_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 224513, 3 | "profiles": [ 4 | { 5 | "aspect_ratio": 0.667, 6 | "height": 900, 7 | "iso_639_1": null, 8 | "file_path": "/14uxt0jH28J9zn4vNQNTae3Bmr7.jpg", 9 | "vote_average": 5.828, 10 | "vote_count": 113, 11 | "width": 600 12 | }, 13 | { 14 | "aspect_ratio": 0.667, 15 | "height": 1366, 16 | "iso_639_1": null, 17 | "file_path": "/xRk889LiJsKlijIVp8KfHiZWw7X.jpg", 18 | "vote_average": 5.802, 19 | "vote_count": 30, 20 | "width": 911 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /test/test-utils/golden_test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | import 'package:file/local.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:platform/platform.dart'; 5 | 6 | class GoldenTestUtils { 7 | /// Loads the cached material icon font. 8 | /// Relies on the tool updating cached assets before running tests. 9 | static Future loadMaterialIconsFont() async { 10 | const FileSystem fs = LocalFileSystem(); 11 | const Platform platform = LocalPlatform(); 12 | final flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT']); 13 | 14 | final iconFont = flutterRoot.childFile( 15 | fs.path.join( 16 | 'bin', 17 | 'cache', 18 | 'artifacts', 19 | 'material_fonts', 20 | 'MaterialIcons-Regular.otf', 21 | ), 22 | ); 23 | 24 | final bytes = 25 | Future.value(iconFont.readAsBytesSync().buffer.asByteData()); 26 | 27 | await (FontLoader('MaterialIcons')..addFont(bytes)).load(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/test-utils/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:movies_app/core/services/http/http_service.dart'; 4 | import 'package:movies_app/core/services/media/media_service.dart'; 5 | import 'package:movies_app/core/services/storage/storage_service.dart'; 6 | import 'package:movies_app/features/people/repositories/people_repository.dart'; 7 | import 'package:movies_app/features/tmdb-configs/repositories/tmdb_configs_repository.dart'; 8 | 9 | class MockStorageService extends Mock implements StorageService {} 10 | 11 | class MockPeopleRepository extends Mock implements PeopleRepository {} 12 | 13 | class MockHttpService extends Mock implements HttpService {} 14 | 15 | class MockTMDBConfigsRepository extends Mock implements TMDBConfigsRepository {} 16 | 17 | class Listener extends Mock { 18 | void call(T? previous, T value); 19 | } 20 | 21 | class MockNavigatorObserver extends Mock implements NavigatorObserver {} 22 | 23 | class MockRoute extends Mock implements Route {} 24 | 25 | class MockMediaServiceProvider extends Mock implements MediaService {} 26 | -------------------------------------------------------------------------------- /test/test-utils/pump_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:movies_app/core/configs/styles/app_themes.dart'; 5 | 6 | extension PumpApp on WidgetTester { 7 | Future pumpApp( 8 | Widget widget, { 9 | NavigatorObserver? navigatorObserver, 10 | }) async { 11 | return pumpWidget( 12 | MaterialApp( 13 | home: widget, 14 | navigatorObservers: [ 15 | if (navigatorObserver != null) navigatorObserver, 16 | ], 17 | ), 18 | ); 19 | } 20 | 21 | Future pumpProviderApp( 22 | Widget widget, { 23 | List overrides = const [], 24 | }) async { 25 | return pumpWidget( 26 | ProviderScope( 27 | overrides: overrides, 28 | child: MaterialApp( 29 | theme: AppThemes.darkTheme, 30 | home: widget, 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | --------------------------------------------------------------------------------