├── .env ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_clean_architecture │ │ │ │ └── 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 └── icons │ ├── facebook.svg │ ├── imdb.svg │ ├── instagram.svg │ ├── tiktok.svg │ ├── twitter.svg │ └── youtube.svg ├── coverage └── lcov.info ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── 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 └── RunnerTests │ └── RunnerTests.swift ├── lib ├── main.dart └── src │ ├── config │ ├── gen │ │ └── assets.gen.dart │ └── router │ │ ├── app_router.dart │ │ └── app_router.gr.dart │ ├── core │ ├── components │ │ ├── bottom_sheet │ │ │ ├── _mixin │ │ │ │ └── base_bottom_sheet_mixin.dart │ │ │ └── social_media_bottom_sheet.dart │ │ ├── buttons │ │ │ ├── bookmark_button.dart │ │ │ ├── retry_button.dart │ │ │ └── social_button.dart │ │ ├── card │ │ │ └── movie_card.dart │ │ ├── image │ │ │ └── base_network_image.dart │ │ └── indicator │ │ │ └── base_indicator.dart │ ├── constants │ │ ├── app_constants.dart │ │ ├── image_constants.dart │ │ ├── path_constants.dart │ │ └── url_constants.dart │ ├── database │ │ └── local_database.dart │ ├── exceptions │ │ ├── database │ │ │ └── database_exception.dart │ │ └── network │ │ │ └── network_exception.dart │ ├── extensions │ │ └── int_extensions.dart │ ├── network │ │ ├── dio_client.dart │ │ └── model │ │ │ ├── error_model.dart │ │ │ └── error_model.g.dart │ ├── theme │ │ ├── app_theme.dart │ │ └── cubit │ │ │ ├── theme_cubit.dart │ │ │ └── theme_state.dart │ └── utils │ │ └── url_launcher_manager.dart │ ├── data │ ├── datasources │ │ ├── _mappers │ │ │ └── entity_convertable.dart │ │ ├── export_datasources.dart │ │ ├── local │ │ │ ├── _collections │ │ │ │ ├── export_collections.dart │ │ │ │ └── movie_detail │ │ │ │ │ ├── movie_detail_collection.dart │ │ │ │ │ └── movie_detail_collection.g.dart │ │ │ └── movie │ │ │ │ ├── movie_local_data_source.dart │ │ │ │ └── movie_local_data_source_impl.dart │ │ └── remote │ │ │ ├── actor │ │ │ ├── actor_remote_data_source.dart │ │ │ └── actor_remote_data_source_impl.dart │ │ │ └── movie │ │ │ ├── movie_remote_data_source.dart │ │ │ └── movie_remote_data_source_impl.dart │ ├── models │ │ ├── actor_detail │ │ │ ├── actor_detail_model.dart │ │ │ └── actor_detail_model.g.dart │ │ ├── actor_social_media │ │ │ ├── actor_social_media_model.dart │ │ │ └── actor_social_media_model.g.dart │ │ ├── export_models.dart │ │ ├── movie_credit │ │ │ ├── cast_model.dart │ │ │ ├── cast_model.g.dart │ │ │ ├── crew_model.dart │ │ │ ├── crew_model.g.dart │ │ │ ├── movie_credit_model.dart │ │ │ └── movie_credit_model.g.dart │ │ ├── movie_detail │ │ │ ├── movie_detail_model.dart │ │ │ └── movie_detail_model.g.dart │ │ └── movie_listings │ │ │ ├── movie_listings_model.dart │ │ │ └── movie_listings_model.g.dart │ └── repositories │ │ ├── actor │ │ └── actor_repository_impl.dart │ │ ├── export_repository_impls.dart │ │ └── movie │ │ └── movie_repository_impl.dart │ ├── domain │ ├── entities │ │ ├── actor_detail │ │ │ └── actor_detail_entity.dart │ │ ├── actor_social_media │ │ │ └── actor_social_medias_entity.dart │ │ ├── export_entities.dart │ │ ├── movie_credit │ │ │ ├── cast_entity.dart │ │ │ ├── crew_entity.dart │ │ │ └── movie_credit_entity.dart │ │ ├── movie_detail │ │ │ └── movie_detail_entity.dart │ │ └── movie_listings │ │ │ └── movie_listings_entity.dart │ ├── repositories │ │ ├── actor │ │ │ └── actor_repository.dart │ │ ├── export_repositories.dart │ │ └── movie │ │ │ └── movie_repository.dart │ └── usecases │ │ ├── actor │ │ └── actor_usecases.dart │ │ ├── export_usecases.dart │ │ └── movie │ │ └── movie_usecases.dart │ ├── injector.dart │ └── presentation │ ├── _widget │ ├── movie_detail │ │ ├── actor_card.dart │ │ └── tag_container.dart │ └── movies │ │ └── movie_listing_widget.dart │ ├── cubit │ ├── actor │ │ ├── export_actor_cubits.dart │ │ ├── get_actor_detail │ │ │ ├── get_actor_detail_cubit.dart │ │ │ └── get_actor_detail_state.dart │ │ └── get_actor_social_media │ │ │ ├── get_actor_social_media_cubit.dart │ │ │ └── get_actor_social_media_state.dart │ └── movie │ │ ├── export_movie_cubits.dart │ │ ├── get_movie_credits │ │ ├── get_movie_credits_cubit.dart │ │ └── get_movie_credits_state.dart │ │ ├── get_popular_movies │ │ ├── get_popular_movies_cubit.dart │ │ └── get_popular_movies_state.dart │ │ ├── get_saved_movies │ │ ├── get_saved_movies_cubit.dart │ │ └── get_saved_movies_state.dart │ │ ├── get_top_rated_movies │ │ ├── get_top_rated_movies_cubit.dart │ │ └── get_top_rated_movies_state.dart │ │ └── toggle_bookmark │ │ ├── toggle_bookmark_cubit.dart │ │ └── toggle_bookmark_state.dart │ └── view │ ├── bookmarks_view.dart │ ├── master_view.dart │ ├── movie_detail_view.dart │ └── movies_view.dart ├── pubspec.lock ├── pubspec.yaml ├── scripts └── refresh_script.sh └── test ├── _utils ├── _dummy │ ├── actor_detail_dummy_data.json │ ├── actor_social_media_data.json │ ├── movie_credit_dummy_data.json │ └── movie_listings_dummy_data.json ├── json_reader.dart └── mocks │ ├── mocks.dart │ └── mocks.mocks.dart ├── data └── remote │ ├── actor │ └── actor_remote_data_source_test.dart │ └── movie │ └── movie_remote_data_source_test.dart ├── domain ├── repositories │ ├── actor │ │ └── actor_repository_test.dart │ └── movie │ │ └── movie_repository_test.dart └── usecases │ ├── actor │ └── actor_usecases_test.dart │ └── movie │ └── movie_usecases_test.dart └── presentation └── cubit ├── actor ├── get_actor_detail_cubit_test.dart └── get_actor_social_media_test.dart └── movie ├── get_movie_credits_cubit_test.dart ├── get_popular_movies_cubit_test.dart ├── get_saved_movies_cubit_test.dart ├── get_top_rated_movies_cubit_test.dart └── toggle_bookmark_cubit_test.dart /.env: -------------------------------------------------------------------------------- 1 | BASE_URL = "https://api.themoviedb.org/3" 2 | 3 | #* your TMDB token. (sign up here https://developer.themoviedb.org/docs) 4 | API_TOKEN = "your tmdb token is here" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: "ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a" 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: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 17 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 18 | - platform: android 19 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 20 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 21 | - platform: ios 22 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 23 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 24 | - platform: linux 25 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 26 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 27 | - platform: macos 28 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 29 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 30 | - platform: web 31 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 32 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 33 | - platform: windows 34 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 35 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Enes Akbal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Clean Architecture Example 2 | 3 | ### About This Project 4 | 5 | This repository is the companion code for my 5-part Medium article series titled "Flutter Clean Architecture." The series offers an extensive guide on applying clean architecture principles in Flutter projects. Each part focuses on a different layer of architecture, backed by practical examples from this repository. 6 | 7 | Feel free to fork this repository and submit a pull request if you have suggestions or improvements. 8 | 9 | ### Article Series Overview 10 | 11 | 12 | #### Part 1: Introduction 13 | - Introduction to the project and the fundamental concepts 14 | - Why should we Use Clean Architecture in Flutter? 15 | - Advantages of The Clean Architecture 16 | - [Read Part 1 on Medium](https://medium.com/@enesakbal00/flutter-clean-architecture-part-1-introduction-f5dadf1bf3ee) 17 | #### Part 2: The Domain Layer 18 | - Insights into the core logic and business rules 19 | - Usecases 20 | - [Read Part 2 on Medium](https://medium.com/@enesakbal00/flutter-clean-architecture-part-2-domain-layer-f55007bd1ade) 21 | #### Part 3: The Data Layer 22 | - Handling data and repository patterns 23 | - Datasources 24 | - Isar 25 | - [Read Part 3 on Medium](https://medium.com/@enesakbal00/flutter-clean-architecture-part-3-data-layer-d9c5c63dc767) 26 | #### Part 4: The Presentation Layer 27 | - Designing the UI and managing state 28 | - BLoC Pattern 29 | - [Read Part 4 on Medium](https://medium.com/@enesakbal00/flutter-clean-architecture-part-4-presentation-layer-ba51445fad83) 30 | #### Part 5: Testing Each Layer 31 | - Implementing tests for robust and reliable code 32 | - Unit Testing 33 | - [Read Part 5 on Medium](https://medium.com/@enesakbal00/flutter-clean-architecture-part-5-unit-test-a1bb7791899f) 34 | 35 | --- 36 | 37 | ### UI Design 38 | - 39 |

40 | 41 | 42 | 43 | 44 | 45 |

46 | 47 | 48 | 49 | 50 | https://github.com/enesakbal/Flutter-Clean-Architecture-Example/assets/60822023/bbe8f4e6-e460-47fc-9893-fa36881df65e 51 | 52 | 53 | --- 54 | 55 | ### Set Up 56 | - Clone Project 57 | ```bash 58 | git clone https://github.com/enesakbal/Flutter-Clean-Architecture-Example.git 59 | ``` 60 | 61 | - Sign up [TMDB](https://developer.themoviedb.org/docs) and don't forget to replace the ```API_TOKEN``` value in the .env file with your own. 62 | 63 | - ```flutter run``` 64 | 65 | --- 66 | 67 | 68 | ## Contact Me 69 | [![LinkedIn](https://img.shields.io/badge/linkedin-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/enesakbl/) 70 | [![Medium](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@enesakbal00) 71 | 72 | enesakbal00@gmail.com 73 | 74 | 75 | 76 | created by ea. 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /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 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.flutter_clean_architecture" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.flutter_clean_architecture" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies {} 68 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_clean_architecture/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_clean_architecture 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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 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 | tasks.register("clean", 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 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | plugins { 14 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 15 | } 16 | } 17 | 18 | include ":app" 19 | 20 | apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" 21 | -------------------------------------------------------------------------------- /assets/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/imdb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 12.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, '12.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 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - isar_flutter_libs (1.0.0): 4 | - Flutter 5 | - path_provider_foundation (0.0.1): 6 | - Flutter 7 | - FlutterMacOS 8 | - sqflite_darwin (0.0.4): 9 | - Flutter 10 | - FlutterMacOS 11 | - url_launcher_ios (0.0.1): 12 | - Flutter 13 | 14 | DEPENDENCIES: 15 | - Flutter (from `Flutter`) 16 | - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) 17 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 18 | - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) 19 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 20 | 21 | EXTERNAL SOURCES: 22 | Flutter: 23 | :path: Flutter 24 | isar_flutter_libs: 25 | :path: ".symlinks/plugins/isar_flutter_libs/ios" 26 | path_provider_foundation: 27 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 28 | sqflite_darwin: 29 | :path: ".symlinks/plugins/sqflite_darwin/darwin" 30 | url_launcher_ios: 31 | :path: ".symlinks/plugins/url_launcher_ios/ios" 32 | 33 | SPEC CHECKSUMS: 34 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 35 | isar_flutter_libs: 9fc2cfb928c539e1b76c481ba5d143d556d94920 36 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 37 | sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 38 | url_launcher_ios: 694010445543906933d732453a59da0a173ae33d 39 | 40 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 41 | 42 | COCOAPODS: 1.16.2 43 | -------------------------------------------------------------------------------- /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.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /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 | @main 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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enesakbal/Flutter-Clean-Architecture-Example/f28a5a44f4df90c1b27d36816ea30d049a0bfa6e/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 | LSApplicationQueriesSchemes 6 | 7 | sms 8 | tel 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleDisplayName 13 | Flutter Clean Architecture 14 | CFBundleExecutable 15 | $(EXECUTABLE_NAME) 16 | CFBundleIdentifier 17 | $(PRODUCT_BUNDLE_IDENTIFIER) 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | CFBundleName 21 | flutter_clean_architecture 22 | CFBundlePackageType 23 | APPL 24 | CFBundleShortVersionString 25 | $(FLUTTER_BUILD_NAME) 26 | CFBundleSignature 27 | ???? 28 | CFBundleVersion 29 | $(FLUTTER_BUILD_NUMBER) 30 | LSRequiresIPhoneOS 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | CADisableMinimumFrameDurationOnPhone 50 | 51 | UIApplicationSupportsIndirectInputEvents 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | import 'package:get_it/get_it.dart'; 7 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 8 | import 'package:path_provider/path_provider.dart'; 9 | 10 | import 'src/config/router/app_router.dart'; 11 | import 'src/core/database/local_database.dart'; 12 | import 'src/core/network/dio_client.dart'; 13 | import 'src/core/theme/app_theme.dart'; 14 | import 'src/core/theme/cubit/theme_cubit.dart'; 15 | import 'src/data/datasources/export_datasources.dart'; 16 | import 'src/data/repositories/export_repository_impls.dart'; 17 | import 'src/domain/repositories/actor/actor_repository.dart'; 18 | import 'src/domain/repositories/movie/movie_repository.dart'; 19 | import 'src/domain/usecases/export_usecases.dart'; 20 | import 'src/presentation/cubit/actor/export_actor_cubits.dart'; 21 | import 'src/presentation/cubit/movie/export_movie_cubits.dart'; 22 | import 'src/presentation/cubit/movie/get_movie_credits/get_movie_credits_cubit.dart'; 23 | 24 | part './src/injector.dart'; 25 | 26 | final router = AppRouter(); 27 | 28 | void main() async { 29 | WidgetsFlutterBinding.ensureInitialized(); 30 | await dotenv.load(); 31 | await init(); 32 | 33 | await injector().initialize(); 34 | 35 | final directory = HydratedStorageDirectory((await getApplicationDocumentsDirectory()).path); 36 | 37 | HydratedBloc.storage = await HydratedStorage.build(storageDirectory: directory); 38 | 39 | runApp(const MainApp()); 40 | } 41 | 42 | class MainApp extends StatelessWidget { 43 | const MainApp({super.key}); 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return MultiBlocProvider( 48 | providers: [ 49 | BlocProvider(create: (context) => injector()), 50 | BlocProvider(create: (context) => injector()), 51 | BlocProvider(create: (context) => injector()..getSavedMovieDetails()), 52 | ], 53 | child: ScreenUtilInit( 54 | builder: (context, child) { 55 | return BlocBuilder( 56 | builder: (context, themeState) { 57 | return MaterialApp.router( 58 | themeMode: themeState.themeMode, 59 | theme: AppTheme.lightTheme, 60 | darkTheme: AppTheme.darkTheme, 61 | routerDelegate: AutoRouterDelegate(router), 62 | routeInformationParser: router.defaultRouteParser(), 63 | debugShowCheckedModeBanner: false, 64 | ); 65 | }, 66 | ); 67 | }, 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/config/gen/assets.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | // coverage:ignore-file 7 | // ignore_for_file: type=lint 8 | // ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use 9 | 10 | import 'package:flutter/widgets.dart'; 11 | import 'package:flutter_svg/flutter_svg.dart'; 12 | import 'package:flutter/services.dart'; 13 | 14 | class $AssetsIconsGen { 15 | const $AssetsIconsGen(); 16 | 17 | /// File path: assets/icons/facebook.svg 18 | SvgGenImage get facebook => const SvgGenImage('assets/icons/facebook.svg'); 19 | 20 | /// File path: assets/icons/imdb.svg 21 | SvgGenImage get imdb => const SvgGenImage('assets/icons/imdb.svg'); 22 | 23 | /// File path: assets/icons/instagram.svg 24 | SvgGenImage get instagram => const SvgGenImage('assets/icons/instagram.svg'); 25 | 26 | /// File path: assets/icons/tiktok.svg 27 | SvgGenImage get tiktok => const SvgGenImage('assets/icons/tiktok.svg'); 28 | 29 | /// File path: assets/icons/twitter.svg 30 | SvgGenImage get twitter => const SvgGenImage('assets/icons/twitter.svg'); 31 | 32 | /// File path: assets/icons/youtube.svg 33 | SvgGenImage get youtube => const SvgGenImage('assets/icons/youtube.svg'); 34 | 35 | /// List of all assets 36 | List get values => 37 | [facebook, imdb, instagram, tiktok, twitter, youtube]; 38 | } 39 | 40 | class Assets { 41 | Assets._(); 42 | 43 | static const $AssetsIconsGen icons = $AssetsIconsGen(); 44 | } 45 | 46 | class SvgGenImage { 47 | const SvgGenImage(this._assetName); 48 | 49 | final String _assetName; 50 | 51 | SvgPicture svg({ 52 | Key? key, 53 | bool matchTextDirection = false, 54 | AssetBundle? bundle, 55 | String? package, 56 | double? width, 57 | double? height, 58 | BoxFit fit = BoxFit.contain, 59 | AlignmentGeometry alignment = Alignment.center, 60 | bool allowDrawingOutsideViewBox = false, 61 | WidgetBuilder? placeholderBuilder, 62 | String? semanticsLabel, 63 | bool excludeFromSemantics = false, 64 | SvgTheme theme = const SvgTheme(), 65 | ColorFilter? colorFilter, 66 | Clip clipBehavior = Clip.hardEdge, 67 | @deprecated Color? color, 68 | @deprecated BlendMode colorBlendMode = BlendMode.srcIn, 69 | @deprecated bool cacheColorFilter = false, 70 | }) { 71 | return SvgPicture.asset( 72 | _assetName, 73 | key: key, 74 | matchTextDirection: matchTextDirection, 75 | bundle: bundle, 76 | package: package, 77 | width: width, 78 | height: height, 79 | fit: fit, 80 | alignment: alignment, 81 | allowDrawingOutsideViewBox: allowDrawingOutsideViewBox, 82 | placeholderBuilder: placeholderBuilder, 83 | semanticsLabel: semanticsLabel, 84 | excludeFromSemantics: excludeFromSemantics, 85 | theme: theme, 86 | colorFilter: colorFilter, 87 | color: color, 88 | colorBlendMode: colorBlendMode, 89 | clipBehavior: clipBehavior, 90 | cacheColorFilter: cacheColorFilter, 91 | ); 92 | } 93 | 94 | String get path => _assetName; 95 | 96 | String get keyName => _assetName; 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/config/router/app_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../core/constants/path_constants.dart'; 5 | import 'app_router.gr.dart'; 6 | 7 | @AutoRouterConfig(replaceInRouteName: 'View|Widget,Route') 8 | 9 | /// Holds all the routes that are defined in the app 10 | /// Used to generate the Router object 11 | final class AppRouter extends $AppRouter { 12 | AppRouter(); 13 | 14 | @override 15 | List get routes => [ 16 | AdaptiveRoute( 17 | page: MasterRoute.page, 18 | path: PathConstants.master, 19 | initial: true, 20 | children: [ 21 | AdaptiveRoute( 22 | page: MoviesRoute.page, 23 | path: PathConstants.movies, 24 | title: (_, __) => 'Movies', 25 | ), 26 | AdaptiveRoute( 27 | page: BookmarksRoute.page, 28 | path: PathConstants.bookmarks, 29 | title: (_, __) => 'Bookmarks', 30 | ), 31 | ], 32 | ), 33 | CustomRoute( 34 | page: MovieDetailRoute.page, 35 | path: PathConstants.movieDetail, 36 | durationInMilliseconds: 800, 37 | reverseDurationInMilliseconds: 800, 38 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 39 | return FadeTransition( 40 | opacity: animation, 41 | child: child, 42 | ); 43 | }, 44 | ), 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/core/components/bottom_sheet/_mixin/base_bottom_sheet_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | mixin BaseBottomSheetMixin on StatelessWidget { 5 | Future show(BuildContext context) { 6 | return showModalBottomSheet( 7 | context: context, 8 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: const Radius.circular(20).r)), 9 | isScrollControlled: true, 10 | builder: (context) { 11 | return Padding( 12 | padding: EdgeInsets.fromLTRB( 13 | 0.05.sw, 14 | 0.05.sw, 15 | 0.05.sw, 16 | 0, 17 | ).r, 18 | child: Column( 19 | mainAxisSize: MainAxisSize.min, 20 | children: [ 21 | //* Drag Indicator 22 | Divider( 23 | thickness: 2.h, 24 | endIndent: 0.35.sw, 25 | indent: 0.35.sw, 26 | color: Theme.of(context).disabledColor, 27 | ), 28 | 29 | 20.verticalSpace, 30 | 31 | //* Content 32 | this, 33 | ], 34 | ), 35 | ); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/core/components/bottom_sheet/social_media_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | import '../../../presentation/cubit/actor/export_actor_cubits.dart'; 7 | import '../buttons/retry_button.dart'; 8 | import '../buttons/social_button.dart'; 9 | import '../indicator/base_indicator.dart'; 10 | import '_mixin/base_bottom_sheet_mixin.dart'; 11 | 12 | class SocialMediaBottomSheet extends StatelessWidget with BaseBottomSheetMixin { 13 | const SocialMediaBottomSheet({super.key, required this.actorId, required this.actorName}); 14 | 15 | final String? actorId; 16 | final String? actorName; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return BlocProvider( 21 | create: (context) => GetIt.I()..getActorSocialMedia(actorId: actorId ?? '0'), 22 | child: _SocialMediaBottomSheet(actorId ?? '0', actorName: actorName), 23 | ); 24 | } 25 | } 26 | 27 | class _SocialMediaBottomSheet extends StatelessWidget { 28 | const _SocialMediaBottomSheet(this.actorId, {required this.actorName}); 29 | final String actorId; 30 | final String? actorName; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return SafeArea( 35 | child: SizedBox( 36 | width: 1.sw, 37 | height: 0.25.sh, 38 | child: BlocBuilder( 39 | builder: (context, state) { 40 | if (state is GetActorSocialMediaError) { 41 | return RetryButton( 42 | text: state.message, 43 | retryAction: () => context.read().getActorSocialMedia(actorId: actorId), 44 | ); 45 | } 46 | 47 | if (state is! GetActorSocialMediaLoaded) return const BaseIndicator(); 48 | 49 | final list = [ 50 | state.instagramId, 51 | state.twitterId, 52 | state.facebookId, 53 | state.youtubeId, 54 | state.imdbId, 55 | state.tiktokId, 56 | ]; 57 | 58 | if (list.every((element) => element == null)) { 59 | return const Center(child: Text('Social media account not found')); 60 | } 61 | 62 | return Column( 63 | crossAxisAlignment: CrossAxisAlignment.start, 64 | children: [ 65 | Text( 66 | "$actorName's social accounts", 67 | style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), 68 | ), 69 | const Divider(), 70 | Expanded( 71 | child: Row( 72 | children: [ 73 | if (list[0] != null) Expanded(child: SocialButton.instagram(id: state.instagramId)), 74 | if (list[1] != null) Expanded(child: SocialButton.twitter(id: state.twitterId)), 75 | if (list[2] != null) Expanded(child: SocialButton.facebook(id: state.facebookId)), 76 | if (list[3] != null) Expanded(child: SocialButton.youtube(id: state.youtubeId)), 77 | if (list[4] != null) Expanded(child: SocialButton.imdb(id: state.imdbId)), 78 | if (list[5] != null) Expanded(child: SocialButton.tiktok(id: state.tiktokId)), 79 | ], 80 | ), 81 | ), 82 | ], 83 | ); 84 | }, 85 | ), 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/core/components/buttons/bookmark_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | import '../../../domain/entities/export_entities.dart'; 7 | import '../../../presentation/cubit/movie/get_saved_movies/get_saved_movies_cubit.dart'; 8 | import '../../../presentation/cubit/movie/toggle_bookmark/toggle_bookmark_cubit.dart'; 9 | 10 | class BookmarkButton extends StatelessWidget { 11 | const BookmarkButton({super.key, required this.movieDetailEntity}) : _buttonStyle = null; 12 | 13 | BookmarkButton.filled({super.key, required this.movieDetailEntity}) 14 | : _buttonStyle = ButtonStyle( 15 | backgroundColor: MaterialStateProperty.all(Colors.white), 16 | shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8).r)), 17 | elevation: MaterialStateProperty.all(1.5), 18 | shadowColor: MaterialStateProperty.all(Colors.black87), 19 | ); 20 | 21 | final MovieDetailEntity? movieDetailEntity; 22 | 23 | final ButtonStyle? _buttonStyle; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return BlocBuilder( 28 | builder: (_, getSavedMoviesState) { 29 | if (getSavedMoviesState is GetSavedMoviesLoaded) { 30 | bool isBookmarked = 31 | getSavedMoviesState.movies?.any((element) => element.id == movieDetailEntity?.id) ?? false; 32 | 33 | return IconButton( 34 | style: _buttonStyle, 35 | padding: EdgeInsets.zero, 36 | onPressed: () async { 37 | await GetIt.I() 38 | .toggleBookmark(movieDetailEntity: movieDetailEntity) 39 | .then((_) => context.read().getSavedMovieDetails()); 40 | 41 | isBookmarked = !isBookmarked; 42 | }, 43 | icon: AnimatedCrossFade( 44 | firstChild: Icon( 45 | Icons.bookmark, 46 | size: 30, 47 | color: Theme.of(context).primaryColor, 48 | ), 49 | secondChild: Icon( 50 | Icons.bookmark_border, 51 | size: 30, 52 | color: Theme.of(context).primaryColor, 53 | ), 54 | crossFadeState: isBookmarked ? CrossFadeState.showFirst : CrossFadeState.showSecond, 55 | duration: kThemeAnimationDuration, 56 | ), 57 | ); 58 | } 59 | 60 | return const SizedBox(); 61 | }, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/core/components/buttons/retry_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RetryButton extends StatelessWidget { 4 | const RetryButton({super.key, required this.retryAction, required this.text}); 5 | 6 | final void Function() retryAction; 7 | final String text; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Column( 12 | children: [ 13 | Text( 14 | text, 15 | style: Theme.of(context).textTheme.titleLarge, 16 | ), 17 | const Divider(), 18 | Expanded( 19 | child: Center( 20 | child: TextButton( 21 | onPressed: () => retryAction.call(), 22 | child: const Text('Retry'), 23 | ), 24 | ), 25 | ), 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/core/components/buttons/social_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../config/gen/assets.gen.dart'; 4 | import '../../utils/url_launcher_manager.dart'; 5 | 6 | class SocialButton extends StatelessWidget { 7 | final Widget icon; 8 | final void Function() onPressed; 9 | final String? id; 10 | 11 | SocialButton.instagram({super.key, required this.id}) 12 | : icon = Assets.icons.instagram.svg(height: 60), 13 | onPressed = (() async => UrlLauncherManager.redirectToInstagramById(id ?? '')); 14 | 15 | SocialButton.twitter({super.key, required this.id}) 16 | : icon = Assets.icons.twitter.svg(height: 60), 17 | onPressed = (() async => UrlLauncherManager.redirectToTwitterById(id ?? '')); 18 | 19 | SocialButton.facebook({super.key, required this.id}) 20 | : icon = Assets.icons.facebook.svg(height: 60), 21 | onPressed = (() async => UrlLauncherManager.redirectToFacebookById(id ?? '')); 22 | 23 | SocialButton.youtube({super.key, required this.id}) 24 | : icon = Assets.icons.youtube.svg(height: 60), 25 | onPressed = (() async => UrlLauncherManager.redirectToYoutubeById(id ?? '')); 26 | 27 | SocialButton.imdb({super.key, required this.id}) 28 | : icon = Assets.icons.imdb.svg(height: 60), 29 | onPressed = (() async => UrlLauncherManager.redirectToImdbById(id)); 30 | 31 | SocialButton.tiktok({super.key, required this.id}) 32 | : icon = Assets.icons.tiktok.svg(height: 60), 33 | onPressed = (() async => UrlLauncherManager.redirectToTiktokById(id)); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return IconButton(onPressed: onPressed, icon: icon); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/core/components/card/movie_card.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | 7 | import '../../../domain/entities/export_entities.dart'; 8 | import '../../../presentation/cubit/movie/export_movie_cubits.dart'; 9 | import '../buttons/bookmark_button.dart'; 10 | import '../image/base_network_image.dart'; 11 | 12 | class MovieCard extends StatelessWidget { 13 | const MovieCard({super.key, this.movie}); 14 | final MovieDetailEntity? movie; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | margin: const EdgeInsets.all(8), 20 | child: Stack( 21 | children: [ 22 | BaseNetworkImage.originalImageSize(movie?.posterPath), 23 | Column( 24 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 25 | children: [ 26 | Align( 27 | alignment: Alignment.topRight, 28 | child: BlocBuilder( 29 | builder: (context, state) { 30 | if (state is GetSavedMoviesLoaded) { 31 | return BookmarkButton( 32 | movieDetailEntity: movie, 33 | ); 34 | } 35 | 36 | return const SizedBox.shrink(); 37 | }, 38 | ), 39 | ), 40 | ClipRRect( 41 | borderRadius: const BorderRadius.only( 42 | bottomLeft: Radius.circular(8), 43 | bottomRight: Radius.circular(8), 44 | ), 45 | child: BackdropFilter( 46 | filter: ImageFilter.blur(sigmaX: 5, sigmaY: 10), 47 | child: Container( 48 | alignment: Alignment.centerLeft, 49 | width: 1.sw, 50 | height: 60, 51 | padding: const EdgeInsets.all(4), 52 | decoration: BoxDecoration( 53 | gradient: LinearGradient( 54 | colors: [Colors.black.withOpacity(0.8), Colors.black.withOpacity(0.0)], 55 | begin: Alignment.bottomCenter, 56 | end: Alignment.topCenter, 57 | ), 58 | ), 59 | child: Text( 60 | movie?.title ?? '', 61 | maxLines: 2, 62 | overflow: TextOverflow.ellipsis, 63 | style: Theme.of(context) 64 | .textTheme 65 | .titleMedium 66 | ?.copyWith(color: Colors.white, fontWeight: FontWeight.bold), 67 | ), 68 | ), 69 | ), 70 | ), 71 | ], 72 | ) 73 | ], 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/core/components/image/base_network_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../constants/image_constants.dart'; 5 | import '../indicator/base_indicator.dart'; 6 | 7 | class BaseNetworkImage extends StatelessWidget { 8 | const BaseNetworkImage(this.url, {super.key, this.hasRadius = true}); 9 | 10 | BaseNetworkImage.originalImageSize(String? targetUrl, {super.key, this.hasRadius = true}) 11 | : url = ImageConstants.originalImage(targetUrl); 12 | 13 | final String? url; 14 | final bool hasRadius; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return CachedNetworkImage( 19 | imageUrl: url ?? '', 20 | progressIndicatorBuilder: (context, url, progress) => const BaseIndicator(), 21 | errorWidget: (_, __, ___) => const BaseIndicator(), 22 | imageBuilder: (context, imageProvider) { 23 | return Container( 24 | decoration: BoxDecoration( 25 | borderRadius: hasRadius ? BorderRadius.circular(8) : null, 26 | image: DecorationImage( 27 | image: imageProvider, 28 | fit: BoxFit.cover, 29 | ), 30 | ), 31 | ); 32 | }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/core/components/indicator/base_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | class BaseIndicator extends StatelessWidget { 5 | const BaseIndicator({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Center( 10 | child: CircularProgressIndicator.adaptive( 11 | strokeWidth: 2.5.r, 12 | ), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/core/constants/app_constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 2 | 3 | class AppConstants { 4 | static final baseUrl = '${dotenv.env['BASE_URL']}'; //* https://api.themoviedb.org/3 5 | static final apiToken = '${dotenv.env['API_TOKEN']}'; //* your TMDB token 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/core/constants/image_constants.dart: -------------------------------------------------------------------------------- 1 | class ImageConstants { 2 | static String? originalImage(String? posterPath) => 'https://image.tmdb.org/t/p/original$posterPath'; 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/core/constants/path_constants.dart: -------------------------------------------------------------------------------- 1 | class PathConstants { 2 | //* Master 3 | static const String master = '/master'; 4 | static const String movies = 'movies'; 5 | static const String bookmarks = 'bookmarks'; 6 | 7 | //* Detail 8 | static const String movieDetail = '/movie_detail'; 9 | 10 | //* More.. 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/core/constants/url_constants.dart: -------------------------------------------------------------------------------- 1 | class UrlConstants { 2 | //* Movie 3 | static const popularMovies = '/movie/popular'; 4 | static const topRatedMovies = '/movie/top_rated'; 5 | static const movieCredits = '/movie/{movie_id}/credits'; 6 | 7 | //* Actor 8 | static const actorDetail = '/person/{person_id}'; 9 | static const actorSocialMedia = '/person/{person_id}/external_ids'; 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/core/database/local_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:isar/isar.dart'; 2 | import 'package:path_provider/path_provider.dart'; 3 | 4 | import '../../data/datasources/local/_collections/movie_detail/movie_detail_collection.dart'; 5 | /// A class representing a local database. 6 | /// 7 | /// This class provides methods to initialize and access the Isar database. 8 | class LocalDatabase { 9 | late final Isar _isar; 10 | bool _isInitialized = false; 11 | 12 | /// Returns the initialized Isar database instance. 13 | /// 14 | /// Throws an [IsarError] if the database has not been initialized. 15 | Isar get db => _isInitialized ? _isar : throw IsarError('Isar has not been initialized.'); 16 | 17 | /// Initializes the Isar database. 18 | /// 19 | /// Throws an [IsarError] if the database has already been initialized. 20 | Future initialize() async { 21 | if (_isInitialized) throw IsarError('Isar has already been initialized.'); 22 | 23 | final directory = await getApplicationDocumentsDirectory(); 24 | _isar = await Isar.open([MovieDetailCollectionSchema], directory: directory.path); 25 | 26 | _isInitialized = true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/core/exceptions/database/database_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:isar/isar.dart'; 3 | /// A custom exception class for database errors. 4 | /// 5 | /// This class extends the `Equatable` and `Exception` classes. 6 | /// 7 | /// The `message` property is a string that holds the error message. 8 | /// 9 | /// The `DatabaseException.fromIsarError` constructor takes an `IsarError` object 10 | /// and initializes the `message` property with the error message from the `IsarError`. 11 | /// 12 | /// The `props` getter returns a list containing the `message` property. 13 | class DatabaseException extends Equatable implements Exception { 14 | late final String message; 15 | 16 | DatabaseException.fromIsarError(IsarError isarError) : message = isarError.message; 17 | 18 | @override 19 | List get props => [message]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/core/exceptions/network/network_exception.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:equatable/equatable.dart'; 5 | 6 | import '../../network/model/error_model.dart'; 7 | 8 | /// 9 | /// This class extends [Equatable] and implements [Exception]. 10 | /// It contains a [message] and a [statusCode] property. 11 | /// The [message] property contains the error message and the [statusCode] 12 | /// property contains the HTTP status code of the response. 13 | /// 14 | /// This class has a constructor [fromDioError] which takes a [DioException] 15 | /// as a parameter and sets the [statusCode] and [message] properties based on 16 | /// the type of the [DioException]. 17 | /// 18 | /// This class also overrides the [props] getter from [Equatable] to compare 19 | /// instances of this class based on the [message] and [statusCode] properties. 20 | /// 21 | /// Example usage: 22 | /// ```dart 23 | /// try { 24 | /// // some network request 25 | /// } on DioException catch (e) { 26 | /// throw NetworkException.fromDioError(e); 27 | /// } 28 | /// ``` 29 | class NetworkException extends Equatable implements Exception { 30 | late final String message; 31 | late final int? statusCode; 32 | 33 | NetworkException.fromDioError(DioException dioException) { 34 | statusCode = dioException.response?.statusCode; 35 | 36 | switch (dioException.type) { 37 | case DioExceptionType.cancel: 38 | message = 'Request to API server was cancelled'; 39 | break; 40 | 41 | case DioExceptionType.connectionTimeout: 42 | message = 'Connection timeout with API server'; 43 | break; 44 | 45 | case DioExceptionType.receiveTimeout: 46 | message = 'Receive timeout in connection with API server'; 47 | break; 48 | 49 | case DioExceptionType.sendTimeout: 50 | message = 'Send timeout in connection with API server'; 51 | break; 52 | 53 | case DioExceptionType.connectionError: 54 | if (dioException.error.runtimeType == SocketException) { 55 | message = 'Please check your internet connection'; 56 | break; 57 | } else { 58 | message = 'Unexpected error occurred'; 59 | break; 60 | } 61 | 62 | case DioExceptionType.badCertificate: 63 | message = 'Bad Certificate'; 64 | break; 65 | 66 | case DioExceptionType.badResponse: 67 | final model = NetworkErrorModel.fromJson(dioException.response?.data as Map); 68 | message = model.statusMessage ?? 'Unexpected bad response'; 69 | break; 70 | 71 | case DioExceptionType.unknown: 72 | message = 'Unexpected error occurred'; 73 | break; 74 | } 75 | } 76 | 77 | @override 78 | List get props => [message, statusCode]; 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/core/extensions/int_extensions.dart: -------------------------------------------------------------------------------- 1 | extension Genre on int { 2 | String getGenreFromNumber() { 3 | switch (this) { 4 | case 28: 5 | return 'Action'; 6 | case 12: 7 | return 'Adventure'; 8 | case 16: 9 | return 'Animation'; 10 | case 35: 11 | return 'Comedy'; 12 | case 80: 13 | return 'Crime'; 14 | case 99: 15 | return 'Documentary'; 16 | case 18: 17 | return 'Drama'; 18 | case 10751: 19 | return 'Family'; 20 | case 14: 21 | return 'Fantasy'; 22 | case 36: 23 | return 'History'; 24 | case 37: 25 | return 'Western'; 26 | case 27: 27 | return 'Horror'; 28 | case 10402: 29 | return 'Music'; 30 | case 9648: 31 | return 'Mystrey'; 32 | case 10749: 33 | return 'Romance'; 34 | case 878: 35 | return 'Science Fiction'; 36 | case 10770: 37 | return 'TV Movie'; 38 | case 53: 39 | return 'Thriller'; 40 | case 10752: 41 | return 'War'; 42 | default: 43 | } 44 | return ''; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/core/network/model/error_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'error_model.g.dart'; 5 | 6 | @JsonSerializable() 7 | class NetworkErrorModel extends Equatable { 8 | @JsonKey(name: 'status_code') 9 | final int? statusCode; 10 | @JsonKey(name: 'status_message') 11 | final String? statusMessage; 12 | 13 | const NetworkErrorModel({this.statusCode, this.statusMessage}); 14 | 15 | factory NetworkErrorModel.fromJson(Map json) { 16 | return _$NetworkErrorModelFromJson(json); 17 | } 18 | 19 | Map toJson() => _$NetworkErrorModelToJson(this); 20 | 21 | @override 22 | List get props => [statusCode, statusMessage]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/core/network/model/error_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'error_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NetworkErrorModel _$NetworkErrorModelFromJson(Map json) => 10 | NetworkErrorModel( 11 | statusCode: json['status_code'] as int?, 12 | statusMessage: json['status_message'] as String?, 13 | ); 14 | 15 | Map _$NetworkErrorModelToJson(NetworkErrorModel instance) => 16 | { 17 | 'status_code': instance.statusCode, 18 | 'status_message': instance.statusMessage, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/core/theme/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | 5 | class AppTheme { 6 | static ThemeData lightTheme = FlexThemeData.light( 7 | useMaterial3: true, 8 | scheme: FlexScheme.green, 9 | appBarStyle: FlexAppBarStyle.primary, 10 | tabBarStyle: FlexTabBarStyle.forAppBar, 11 | swapColors: true, 12 | subThemesData: const FlexSubThemesData( 13 | bottomAppBarSchemeColor: SchemeColor.primary, 14 | ), 15 | textTheme: TextTheme( 16 | bodySmall: TextStyle(fontSize: 14.sp), 17 | titleSmall: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.normal), 18 | titleMedium: TextStyle(fontSize: 13.sp, fontWeight: FontWeight.normal), 19 | titleLarge: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.normal), 20 | labelSmall: TextStyle(fontSize: 8.sp, fontWeight: FontWeight.w400), 21 | labelMedium: TextStyle(fontSize: 10.sp, fontWeight: FontWeight.w400), 22 | labelLarge: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.w400), 23 | ), 24 | ); 25 | 26 | static ThemeData darkTheme = FlexThemeData.dark( 27 | useMaterial3: true, 28 | scheme: FlexScheme.bigStone, 29 | appBarStyle: FlexAppBarStyle.primary, 30 | tabBarStyle: FlexTabBarStyle.forAppBar, 31 | swapColors: true, 32 | subThemesData: const FlexSubThemesData(bottomAppBarSchemeColor: SchemeColor.primary), 33 | textTheme: TextTheme( 34 | bodySmall: TextStyle(fontSize: 14.sp), 35 | titleSmall: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.normal), 36 | titleMedium: TextStyle(fontSize: 13.sp, fontWeight: FontWeight.normal), 37 | titleLarge: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.normal), 38 | labelSmall: TextStyle(fontSize: 8.sp, fontWeight: FontWeight.w400), 39 | labelMedium: TextStyle(fontSize: 10.sp, fontWeight: FontWeight.w400), 40 | labelLarge: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.w400), 41 | ), 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/core/theme/cubit/theme_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 4 | 5 | part 'theme_state.dart'; 6 | 7 | class ThemeCubit extends HydratedCubit { 8 | ThemeCubit() : super(const ThemeState()); 9 | 10 | void toggleTheme({required Brightness brightness}) { 11 | brightness == Brightness.dark 12 | ? emit(const ThemeState(themeMode: ThemeMode.light)) 13 | : emit(const ThemeState(themeMode: ThemeMode.dark)); 14 | } 15 | 16 | @override 17 | ThemeState? fromJson(Map json) { 18 | return ThemeState.fromMap(json); 19 | } 20 | 21 | @override 22 | Map? toJson(ThemeState state) { 23 | return state.toMap(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/core/theme/cubit/theme_state.dart: -------------------------------------------------------------------------------- 1 | part of 'theme_cubit.dart'; 2 | 3 | final class ThemeState extends Equatable { 4 | const ThemeState({this.themeMode = ThemeMode.system}); 5 | 6 | factory ThemeState.fromMap(Map map) { 7 | return ThemeState(themeMode: ThemeMode.values[map['themeMode'] as int]); 8 | } 9 | 10 | final ThemeMode themeMode; 11 | 12 | Map toMap() { 13 | return {'themeMode': themeMode.index}; 14 | } 15 | 16 | @override 17 | List get props => [themeMode]; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/core/utils/url_launcher_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:url_launcher/url_launcher_string.dart'; 4 | 5 | class UrlLauncherManager { 6 | static Future redirectUrl(String url) async { 7 | if (await canLaunchUrlString(url)) { 8 | await launchUrlString(url); 9 | } else { 10 | log('Could not launch $url'); 11 | } 12 | } 13 | 14 | static Future redirectToInstagramById(String? id) async { 15 | final url = 'https://www.instagram.com/$id'; 16 | 17 | await redirectUrl(url); 18 | } 19 | 20 | static Future redirectToTwitterById(String? id) async { 21 | final url = 'https://twitter.com/$id'; 22 | 23 | await redirectUrl(url); 24 | } 25 | 26 | static Future redirectToFacebookById(String? id) async { 27 | final url = 'https://www.facebook.com/$id'; 28 | 29 | await redirectUrl(url); 30 | } 31 | 32 | static Future redirectToYoutubeById(String? id) async { 33 | final url = 'https://www.youtube.com/channel/$id'; 34 | 35 | await redirectUrl(url); 36 | } 37 | 38 | static Future redirectToImdbById(String? id) async { 39 | final url = 'https://www.imdb.com/name/$id'; 40 | 41 | await redirectUrl(url); 42 | } 43 | 44 | static Future redirectToTiktokById(String? id) async { 45 | final url = 'https://www.tiktok.com/@$id'; 46 | 47 | await redirectUrl(url); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/data/datasources/_mappers/entity_convertable.dart: -------------------------------------------------------------------------------- 1 | 2 | /// A mixin that provides conversion methods between entity and model objects. 3 | /// 4 | /// The `EntityConvertible` mixin defines two methods: 5 | /// - `toEntity()`: Converts the implementing object to an entity object. 6 | /// - `fromEntity()`: Converts an entity object to the implementing object. 7 | /// 8 | /// Example usage: 9 | /// ```dart 10 | /// class UserEntity { 11 | /// final String id; 12 | /// final String name; 13 | /// 14 | /// UserEntity(this.id, this.name); 15 | /// } 16 | /// 17 | /// class UserModel with EntityConvertible { 18 | /// final String id; 19 | /// final String name; 20 | /// 21 | /// UserModel(this.id, this.name); 22 | /// 23 | /// @override 24 | /// UserEntity toEntity() { 25 | /// return UserEntity(id, name); 26 | /// } 27 | /// 28 | /// @override 29 | /// UserModel fromEntity(UserEntity entity) { 30 | /// return UserModel(entity.id, entity.name); 31 | /// } 32 | /// } 33 | /// ``` 34 | mixin EntityConvertible { 35 | O toEntity(); 36 | I fromEntity(O model) => throw UnimplementedError(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/data/datasources/export_datasources.dart: -------------------------------------------------------------------------------- 1 | export 'local/movie/movie_local_data_source.dart'; 2 | export 'local/movie/movie_local_data_source_impl.dart'; 3 | export 'remote/actor/actor_remote_data_source.dart'; 4 | export 'remote/actor/actor_remote_data_source_impl.dart'; 5 | export 'remote/movie/movie_remote_data_source.dart'; 6 | export 'remote/movie/movie_remote_data_source_impl.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/data/datasources/local/_collections/export_collections.dart: -------------------------------------------------------------------------------- 1 | export 'movie_detail/movie_detail_collection.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/data/datasources/local/_collections/movie_detail/movie_detail_collection.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | import 'package:isar/isar.dart'; 3 | 4 | import '../../../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../../_mappers/entity_convertable.dart'; 6 | 7 | part 'movie_detail_collection.g.dart'; 8 | 9 | @collection 10 | class MovieDetailCollection with EntityConvertible { 11 | final Id? id; 12 | final bool? adult; 13 | final String? backdropPath; 14 | final List? genreIds; 15 | final String? originalLanguage; 16 | final String? originalTitle; 17 | final String? overview; 18 | final double? popularity; 19 | final String? posterPath; 20 | final String? releaseDate; 21 | final String? title; 22 | final bool? video; 23 | final double? voteAverage; 24 | final int? voteCount; 25 | 26 | MovieDetailCollection({ 27 | this.id, 28 | this.adult, 29 | this.backdropPath, 30 | this.genreIds, 31 | this.originalLanguage, 32 | this.originalTitle, 33 | this.overview, 34 | this.popularity, 35 | this.posterPath, 36 | this.releaseDate, 37 | this.title, 38 | this.video, 39 | this.voteAverage, 40 | this.voteCount, 41 | }); 42 | 43 | @override 44 | MovieDetailEntity toEntity() { 45 | return MovieDetailEntity( 46 | adult: adult, 47 | backdropPath: backdropPath, 48 | genreIds: genreIds, 49 | id: id, 50 | originalLanguage: originalLanguage, 51 | originalTitle: originalTitle, 52 | overview: overview, 53 | popularity: popularity, 54 | posterPath: posterPath, 55 | releaseDate: releaseDate, 56 | title: title, 57 | video: video, 58 | voteAverage: voteAverage, 59 | voteCount: voteCount, 60 | ); 61 | } 62 | 63 | @override 64 | MovieDetailCollection fromEntity(MovieDetailEntity? model) { 65 | return MovieDetailCollection( 66 | adult: model?.adult, 67 | backdropPath: model?.backdropPath, 68 | genreIds: model?.genreIds, 69 | id: model?.id, 70 | originalLanguage: model?.originalLanguage, 71 | originalTitle: model?.originalTitle, 72 | overview: model?.overview, 73 | popularity: model?.popularity, 74 | posterPath: model?.posterPath, 75 | releaseDate: model?.releaseDate, 76 | title: model?.title, 77 | video: model?.video, 78 | voteAverage: model?.voteAverage, 79 | voteCount: model?.voteCount, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/data/datasources/local/movie/movie_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../_collections/movie_detail/movie_detail_collection.dart'; 2 | 3 | abstract class MovieLocalDataSource { 4 | const MovieLocalDataSource(); 5 | 6 | /// Saves the [movieDetailCollection] to the local data source. 7 | Future saveMovieDetail({required MovieDetailCollection movieDetailCollection}); 8 | 9 | /// Deletes the movie detail with the given [movieId] from the local data source. 10 | Future deleteMovieDetail({required int? movieId}); 11 | 12 | /// Returns a boolean indicating whether the movie detail with the given [movieId] is saved in the local data source. 13 | Future isSavedMovieDetail({required int? movieId}); 14 | 15 | /// Returns a list of all saved movie details from the local data source. 16 | Future> getSavedMovieDetails(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/data/datasources/local/movie/movie_local_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:isar/isar.dart'; 2 | 3 | import '../../../../core/database/local_database.dart'; 4 | import '../_collections/movie_detail/movie_detail_collection.dart'; 5 | import 'movie_local_data_source.dart'; 6 | 7 | class MovieLocalDataSourceImpl implements MovieLocalDataSource { 8 | MovieLocalDataSourceImpl(this.localDatabase); 9 | 10 | final LocalDatabase localDatabase; 11 | 12 | /// Deletes the movie detail with the given [movieId] from the local database. 13 | @override 14 | Future deleteMovieDetail({required int? movieId}) async { 15 | try { 16 | final db = localDatabase.db; 17 | await db.writeTxn(() async => db.movieDetailCollections.filter().idEqualTo(movieId).deleteAll()); 18 | } catch (_) { 19 | rethrow; 20 | } 21 | } 22 | 23 | /// Retrieves all saved movie details from the local database. 24 | @override 25 | Future> getSavedMovieDetails() async { 26 | try { 27 | final list = await localDatabase.db.movieDetailCollections.where().findAll(); 28 | 29 | return list; 30 | } catch (_) { 31 | rethrow; 32 | } 33 | } 34 | 35 | /// Saves the given [movieDetailCollection] to the local database. 36 | @override 37 | Future saveMovieDetail({required MovieDetailCollection movieDetailCollection}) async { 38 | try { 39 | final db = localDatabase.db; 40 | 41 | await db.writeTxn(() async => db.movieDetailCollections.put(movieDetailCollection)); 42 | } catch (_) { 43 | rethrow; 44 | } 45 | } 46 | 47 | /// Checks if the movie detail with the given [movieId] is saved in the local database. 48 | @override 49 | Future isSavedMovieDetail({required int? movieId}) async { 50 | try { 51 | final db = localDatabase.db; 52 | final isSaved = await db.movieDetailCollections.filter().idEqualTo(movieId).isNotEmpty(); 53 | 54 | return isSaved; 55 | } catch (_) { 56 | rethrow; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/data/datasources/remote/actor/actor_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../../../models/actor_detail/actor_detail_model.dart'; 2 | import '../../../models/actor_social_media/actor_social_media_model.dart'; 3 | 4 | /// Abstract class for remote data source of actor entity. 5 | abstract class ActorRemoteDataSource { 6 | /// Returns the actor detail model for the given actor id. 7 | Future getActorDetail({required String actorId}); 8 | 9 | /// Returns the actor social media model for the given actor id. 10 | Future getActorSocialMedia({required String actorId}); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/data/datasources/remote/actor/actor_remote_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import '../../../../core/constants/url_constants.dart'; 2 | import '../../../../core/network/dio_client.dart'; 3 | import '../../../models/actor_detail/actor_detail_model.dart'; 4 | import '../../../models/actor_social_media/actor_social_media_model.dart'; 5 | import 'actor_remote_data_source.dart'; 6 | 7 | class ActorRemoteDataSourceImpl implements ActorRemoteDataSource { 8 | final DioClient _dioClient; 9 | 10 | const ActorRemoteDataSourceImpl(this._dioClient); 11 | 12 | /// Retrieves the details of the actor with the given [actorId] from the remote data source. 13 | @override 14 | Future getActorDetail({required String actorId}) async { 15 | try { 16 | final response = await _dioClient.get(UrlConstants.actorDetail.replaceAll('{person_id}', actorId)); 17 | 18 | final model = ActorDetailModel.fromJson(response.data as Map); 19 | 20 | return model; 21 | } catch (_) { 22 | rethrow; 23 | } 24 | } 25 | 26 | /// Retrieves the social media accounts of the actor with the given [actorId] from the remote data source. 27 | @override 28 | Future getActorSocialMedia({required String actorId}) async { 29 | try { 30 | final response = await _dioClient.get(UrlConstants.actorSocialMedia.replaceAll('{person_id}', actorId)); 31 | 32 | final model = ActorSocialMediaModel.fromJson(response.data as Map); 33 | 34 | return model; 35 | } catch (_) { 36 | rethrow; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/data/datasources/remote/movie/movie_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import '../../../models/movie_credit/movie_credit_model.dart'; 2 | import '../../../models/movie_listings/movie_listings_model.dart'; 3 | 4 | /// Abstract class that defines the methods for fetching popular and top rated movies from a remote data source. 5 | abstract class MovieRemoteDataSource { 6 | /// Fetches a list of popular movies from the remote data source. 7 | Future getPopularMovies({required int page}); 8 | 9 | /// Fetches a list of top rated movies from the remote data source. 10 | Future getTopRatedMovies({required int page}); 11 | 12 | /// Retrieves the movie credits for a given movie ID from the remote data source. 13 | Future getMovieCredits({required int movieId}); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/data/datasources/remote/movie/movie_remote_data_source_impl.dart: -------------------------------------------------------------------------------- 1 | import '../../../../core/constants/url_constants.dart'; 2 | import '../../../../core/network/dio_client.dart'; 3 | import '../../../models/movie_credit/movie_credit_model.dart'; 4 | import '../../../models/movie_listings/movie_listings_model.dart'; 5 | import 'movie_remote_data_source.dart'; 6 | 7 | class MovieRemoteDataSourceImpl implements MovieRemoteDataSource { 8 | final DioClient _dioClient; 9 | 10 | const MovieRemoteDataSourceImpl(this._dioClient); 11 | 12 | /// Retrieves a list of popular movies from the remote data source. 13 | @override 14 | Future getPopularMovies({required int page}) async { 15 | try { 16 | final response = await _dioClient.get(UrlConstants.popularMovies, queryParameters: {'page': page}); 17 | 18 | final model = MovieListingsModel.fromJson(response.data as Map); 19 | 20 | return model; 21 | } catch (_) { 22 | rethrow; 23 | } 24 | } 25 | 26 | /// Retrieves a list of top rated movies from the remote data source. 27 | @override 28 | Future getTopRatedMovies({required int page}) async { 29 | try { 30 | final response = await _dioClient.get(UrlConstants.topRatedMovies, queryParameters: {'page': page}); 31 | 32 | final model = MovieListingsModel.fromJson(response.data as Map); 33 | 34 | return model; 35 | } catch (_) { 36 | rethrow; 37 | } 38 | } 39 | 40 | /// Retrieves the movie credits for a given movie ID from the remote data source. 41 | @override 42 | Future getMovieCredits({required int movieId}) async { 43 | try { 44 | final response = await _dioClient.get(UrlConstants.movieCredits.replaceAll('{movie_id}', movieId.toString())); 45 | 46 | final model = MovieCreditModel.fromJson(response.data as Map); 47 | 48 | return model; 49 | } catch (_) { 50 | rethrow; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/data/models/actor_detail/actor_detail_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/actor_detail/actor_detail_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | 7 | part 'actor_detail_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class ActorDetailModel extends Equatable with EntityConvertible { 11 | final bool? adult; 12 | @JsonKey(name: 'also_known_as') 13 | final List? alsoKnownAs; 14 | final String? biography; 15 | final String? birthday; 16 | final String? deathday; 17 | final int? gender; 18 | final String? homepage; 19 | final int? id; 20 | @JsonKey(name: 'imdb_id') 21 | final String? imdbId; 22 | @JsonKey(name: 'known_for_department') 23 | final String? knownForDepartment; 24 | final String? name; 25 | @JsonKey(name: 'place_of_birth') 26 | final String? placeOfBirth; 27 | final double? popularity; 28 | @JsonKey(name: 'profile_path') 29 | final String? profilePath; 30 | 31 | const ActorDetailModel({ 32 | this.adult, 33 | this.alsoKnownAs, 34 | this.biography, 35 | this.birthday, 36 | this.deathday, 37 | this.gender, 38 | this.homepage, 39 | this.id, 40 | this.imdbId, 41 | this.knownForDepartment, 42 | this.name, 43 | this.placeOfBirth, 44 | this.popularity, 45 | this.profilePath, 46 | }); 47 | 48 | factory ActorDetailModel.fromJson(Map json) { 49 | return _$ActorDetailModelFromJson(json); 50 | } 51 | 52 | Map toJson() => _$ActorDetailModelToJson(this); 53 | 54 | @override 55 | List get props { 56 | return [ 57 | adult, 58 | alsoKnownAs, 59 | biography, 60 | birthday, 61 | deathday, 62 | gender, 63 | homepage, 64 | id, 65 | imdbId, 66 | knownForDepartment, 67 | name, 68 | placeOfBirth, 69 | popularity, 70 | profilePath, 71 | ]; 72 | } 73 | 74 | @override 75 | ActorDetailEntity toEntity() => ActorDetailEntity( 76 | adult: adult, 77 | alsoKnownAs: alsoKnownAs, 78 | biography: biography, 79 | birthday: birthday, 80 | deathday: deathday, 81 | gender: gender, 82 | homepage: homepage, 83 | id: id, 84 | imdbId: imdbId, 85 | knownForDepartment: knownForDepartment, 86 | name: name, 87 | placeOfBirth: placeOfBirth, 88 | popularity: popularity, 89 | profilePath: profilePath, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/data/models/actor_detail/actor_detail_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'actor_detail_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ActorDetailModel _$ActorDetailModelFromJson(Map json) => 10 | ActorDetailModel( 11 | adult: json['adult'] as bool?, 12 | alsoKnownAs: (json['also_known_as'] as List?) 13 | ?.map((e) => e as String) 14 | .toList(), 15 | biography: json['biography'] as String?, 16 | birthday: json['birthday'] as String?, 17 | deathday: json['deathday'] as String?, 18 | gender: json['gender'] as int?, 19 | homepage: json['homepage'] as String?, 20 | id: json['id'] as int?, 21 | imdbId: json['imdb_id'] as String?, 22 | knownForDepartment: json['known_for_department'] as String?, 23 | name: json['name'] as String?, 24 | placeOfBirth: json['place_of_birth'] as String?, 25 | popularity: (json['popularity'] as num?)?.toDouble(), 26 | profilePath: json['profile_path'] as String?, 27 | ); 28 | 29 | Map _$ActorDetailModelToJson(ActorDetailModel instance) => 30 | { 31 | 'adult': instance.adult, 32 | 'also_known_as': instance.alsoKnownAs, 33 | 'biography': instance.biography, 34 | 'birthday': instance.birthday, 35 | 'deathday': instance.deathday, 36 | 'gender': instance.gender, 37 | 'homepage': instance.homepage, 38 | 'id': instance.id, 39 | 'imdb_id': instance.imdbId, 40 | 'known_for_department': instance.knownForDepartment, 41 | 'name': instance.name, 42 | 'place_of_birth': instance.placeOfBirth, 43 | 'popularity': instance.popularity, 44 | 'profile_path': instance.profilePath, 45 | }; 46 | -------------------------------------------------------------------------------- /lib/src/data/models/actor_social_media/actor_social_media_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/actor_social_media/actor_social_medias_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | 7 | part 'actor_social_media_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class ActorSocialMediaModel extends Equatable with EntityConvertible { 11 | final int? id; 12 | @JsonKey(name: 'freebase_mid') 13 | final String? freebaseMid; 14 | @JsonKey(name: 'freebase_id') 15 | final String? freebaseId; 16 | @JsonKey(name: 'imdb_id') 17 | final String? imdbId; 18 | @JsonKey(name: 'tvrage_id') 19 | final int? tvrageId; 20 | @JsonKey(name: 'wikidata_id') 21 | final String? wikidataId; 22 | @JsonKey(name: 'facebook_id') 23 | final String? facebookId; 24 | @JsonKey(name: 'instagram_id') 25 | final String? instagramId; 26 | @JsonKey(name: 'tiktok_id') 27 | final String? tiktokId; 28 | @JsonKey(name: 'twitter_id') 29 | final String? twitterId; 30 | @JsonKey(name: 'youtube_id') 31 | final String? youtubeId; 32 | 33 | const ActorSocialMediaModel({ 34 | this.id, 35 | this.freebaseMid, 36 | this.freebaseId, 37 | this.imdbId, 38 | this.tvrageId, 39 | this.wikidataId, 40 | this.facebookId, 41 | this.instagramId, 42 | this.tiktokId, 43 | this.twitterId, 44 | this.youtubeId, 45 | }); 46 | 47 | factory ActorSocialMediaModel.fromJson(Map json) { 48 | return _$ActorSocialMediaModelFromJson(json); 49 | } 50 | 51 | @override 52 | ActorSocialMediaEntity toEntity() => ActorSocialMediaEntity( 53 | facebookId: facebookId, 54 | freebaseId: freebaseId, 55 | freebaseMid: freebaseMid, 56 | id: id, 57 | imdbId: imdbId, 58 | instagramId: instagramId, 59 | tiktokId: tiktokId, 60 | tvrageId: tvrageId, 61 | twitterId: twitterId, 62 | wikidataId: wikidataId, 63 | youtubeId: youtubeId, 64 | ); 65 | 66 | Map toJson() => _$ActorSocialMediaModelToJson(this); 67 | 68 | @override 69 | List get props { 70 | return [ 71 | id, 72 | freebaseMid, 73 | freebaseId, 74 | imdbId, 75 | tvrageId, 76 | wikidataId, 77 | facebookId, 78 | instagramId, 79 | tiktokId, 80 | twitterId, 81 | youtubeId, 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/data/models/actor_social_media/actor_social_media_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'actor_social_media_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ActorSocialMediaModel _$ActorSocialMediaModelFromJson( 10 | Map json) => 11 | ActorSocialMediaModel( 12 | id: json['id'] as int?, 13 | freebaseMid: json['freebase_mid'] as String?, 14 | freebaseId: json['freebase_id'] as String?, 15 | imdbId: json['imdb_id'] as String?, 16 | tvrageId: json['tvrage_id'] as int?, 17 | wikidataId: json['wikidata_id'] as String?, 18 | facebookId: json['facebook_id'] as String?, 19 | instagramId: json['instagram_id'] as String?, 20 | tiktokId: json['tiktok_id'] as String?, 21 | twitterId: json['twitter_id'] as String?, 22 | youtubeId: json['youtube_id'] as String?, 23 | ); 24 | 25 | Map _$ActorSocialMediaModelToJson( 26 | ActorSocialMediaModel instance) => 27 | { 28 | 'id': instance.id, 29 | 'freebase_mid': instance.freebaseMid, 30 | 'freebase_id': instance.freebaseId, 31 | 'imdb_id': instance.imdbId, 32 | 'tvrage_id': instance.tvrageId, 33 | 'wikidata_id': instance.wikidataId, 34 | 'facebook_id': instance.facebookId, 35 | 'instagram_id': instance.instagramId, 36 | 'tiktok_id': instance.tiktokId, 37 | 'twitter_id': instance.twitterId, 38 | 'youtube_id': instance.youtubeId, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/src/data/models/export_models.dart: -------------------------------------------------------------------------------- 1 | export 'actor_detail/actor_detail_model.dart'; 2 | export 'actor_social_media/actor_social_media_model.dart'; 3 | export 'movie_credit/cast_model.dart'; 4 | export 'movie_credit/crew_model.dart'; 5 | export 'movie_credit/movie_credit_model.dart'; 6 | export 'movie_detail/movie_detail_model.dart'; 7 | export 'movie_listings/movie_listings_model.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/cast_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/movie_credit/cast_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | 7 | part 'cast_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class CastModel extends Equatable with EntityConvertible { 11 | final bool? adult; 12 | final int? gender; 13 | final int? id; 14 | @JsonKey(name: 'known_for_department') 15 | final String? knownForDepartment; 16 | final String? name; 17 | @JsonKey(name: 'original_name') 18 | final String? originalName; 19 | final double? popularity; 20 | @JsonKey(name: 'profile_path') 21 | final String? profilePath; 22 | @JsonKey(name: 'cast_id') 23 | final int? castId; 24 | final String? character; 25 | @JsonKey(name: 'credit_id') 26 | final String? creditId; 27 | final int? order; 28 | 29 | const CastModel({ 30 | this.adult, 31 | this.gender, 32 | this.id, 33 | this.knownForDepartment, 34 | this.name, 35 | this.originalName, 36 | this.popularity, 37 | this.profilePath, 38 | this.castId, 39 | this.character, 40 | this.creditId, 41 | this.order, 42 | }); 43 | 44 | factory CastModel.fromJson(Map json) => _$CastModelFromJson(json); 45 | 46 | Map toJson() => _$CastModelToJson(this); 47 | 48 | @override 49 | List get props { 50 | return [ 51 | adult, 52 | gender, 53 | id, 54 | knownForDepartment, 55 | name, 56 | originalName, 57 | popularity, 58 | profilePath, 59 | castId, 60 | character, 61 | creditId, 62 | order, 63 | ]; 64 | } 65 | 66 | @override 67 | CastEntity toEntity() => CastEntity( 68 | adult: adult, 69 | castId: castId, 70 | character: character, 71 | creditId: creditId, 72 | gender: gender, 73 | id: id, 74 | knownForDepartment: knownForDepartment, 75 | name: name, 76 | order: order, 77 | originalName: originalName, 78 | popularity: popularity, 79 | profilePath: profilePath, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/cast_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'cast_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CastModel _$CastModelFromJson(Map json) => CastModel( 10 | adult: json['adult'] as bool?, 11 | gender: json['gender'] as int?, 12 | id: json['id'] as int?, 13 | knownForDepartment: json['known_for_department'] as String?, 14 | name: json['name'] as String?, 15 | originalName: json['original_name'] as String?, 16 | popularity: (json['popularity'] as num?)?.toDouble(), 17 | profilePath: json['profile_path'] as String?, 18 | castId: json['cast_id'] as int?, 19 | character: json['character'] as String?, 20 | creditId: json['credit_id'] as String?, 21 | order: json['order'] as int?, 22 | ); 23 | 24 | Map _$CastModelToJson(CastModel instance) => { 25 | 'adult': instance.adult, 26 | 'gender': instance.gender, 27 | 'id': instance.id, 28 | 'known_for_department': instance.knownForDepartment, 29 | 'name': instance.name, 30 | 'original_name': instance.originalName, 31 | 'popularity': instance.popularity, 32 | 'profile_path': instance.profilePath, 33 | 'cast_id': instance.castId, 34 | 'character': instance.character, 35 | 'credit_id': instance.creditId, 36 | 'order': instance.order, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/crew_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/movie_credit/crew_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | 7 | part 'crew_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class CrewModel extends Equatable with EntityConvertible { 11 | final bool? adult; 12 | final int? gender; 13 | final int? id; 14 | @JsonKey(name: 'known_for_department') 15 | final String? knownForDepartment; 16 | final String? name; 17 | @JsonKey(name: 'original_name') 18 | final String? originalName; 19 | final double? popularity; 20 | @JsonKey(name: 'profile_path') 21 | final String? profilePath; 22 | @JsonKey(name: 'credit_id') 23 | final String? creditId; 24 | final String? department; 25 | final String? job; 26 | 27 | const CrewModel({ 28 | this.adult, 29 | this.gender, 30 | this.id, 31 | this.knownForDepartment, 32 | this.name, 33 | this.originalName, 34 | this.popularity, 35 | this.profilePath, 36 | this.creditId, 37 | this.department, 38 | this.job, 39 | }); 40 | 41 | factory CrewModel.fromJson(Map json) => _$CrewModelFromJson(json); 42 | 43 | Map toJson() => _$CrewModelToJson(this); 44 | 45 | @override 46 | List get props { 47 | return [ 48 | adult, 49 | gender, 50 | id, 51 | knownForDepartment, 52 | name, 53 | originalName, 54 | popularity, 55 | profilePath, 56 | creditId, 57 | department, 58 | job, 59 | ]; 60 | } 61 | 62 | @override 63 | CrewEntity toEntity() { 64 | return CrewEntity( 65 | adult: adult, 66 | creditId: creditId, 67 | department: department, 68 | gender: gender, 69 | id: id, 70 | job: job, 71 | knownForDepartment: knownForDepartment, 72 | name: name, 73 | originalName: originalName, 74 | popularity: popularity, 75 | profilePath: profilePath, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/crew_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'crew_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CrewModel _$CrewModelFromJson(Map json) => CrewModel( 10 | adult: json['adult'] as bool?, 11 | gender: json['gender'] as int?, 12 | id: json['id'] as int?, 13 | knownForDepartment: json['known_for_department'] as String?, 14 | name: json['name'] as String?, 15 | originalName: json['original_name'] as String?, 16 | popularity: (json['popularity'] as num?)?.toDouble(), 17 | profilePath: json['profile_path'] as String?, 18 | creditId: json['credit_id'] as String?, 19 | department: json['department'] as String?, 20 | job: json['job'] as String?, 21 | ); 22 | 23 | Map _$CrewModelToJson(CrewModel instance) => { 24 | 'adult': instance.adult, 25 | 'gender': instance.gender, 26 | 'id': instance.id, 27 | 'known_for_department': instance.knownForDepartment, 28 | 'name': instance.name, 29 | 'original_name': instance.originalName, 30 | 'popularity': instance.popularity, 31 | 'profile_path': instance.profilePath, 32 | 'credit_id': instance.creditId, 33 | 'department': instance.department, 34 | 'job': instance.job, 35 | }; 36 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/movie_credit_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/export_entities.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | import 'cast_model.dart'; 7 | import 'crew_model.dart'; 8 | 9 | part 'movie_credit_model.g.dart'; 10 | 11 | @JsonSerializable() 12 | class MovieCreditModel extends Equatable with EntityConvertible { 13 | final int? id; 14 | final List? cast; 15 | final List? crew; 16 | 17 | const MovieCreditModel({this.id, this.cast, this.crew}); 18 | 19 | factory MovieCreditModel.fromJson(Map json) { 20 | return _$MovieCreditModelFromJson(json); 21 | } 22 | 23 | Map toJson() => _$MovieCreditModelToJson(this); 24 | 25 | @override 26 | List get props => [id, cast, crew]; 27 | 28 | @override 29 | MovieCreditEntity toEntity() => MovieCreditEntity( 30 | id: id, 31 | cast: cast?.map((e) => e.toEntity()).toList(), 32 | crew: crew?.map((e) => e.toEntity()).toList(), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_credit/movie_credit_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'movie_credit_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MovieCreditModel _$MovieCreditModelFromJson(Map json) => 10 | MovieCreditModel( 11 | id: json['id'] as int?, 12 | cast: (json['cast'] as List?) 13 | ?.map((e) => CastModel.fromJson(e as Map)) 14 | .toList(), 15 | crew: (json['crew'] as List?) 16 | ?.map((e) => CrewModel.fromJson(e as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$MovieCreditModelToJson(MovieCreditModel instance) => 21 | { 22 | 'id': instance.id, 23 | 'cast': instance.cast, 24 | 'crew': instance.crew, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_detail/movie_detail_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | 7 | part 'movie_detail_model.g.dart'; 8 | 9 | @JsonSerializable() 10 | class MovieDetailModel extends Equatable with EntityConvertible { 11 | final bool? adult; 12 | @JsonKey(name: 'backdrop_path') 13 | final String? backdropPath; 14 | @JsonKey(name: 'genre_ids') 15 | final List? genreIds; 16 | final int? id; 17 | @JsonKey(name: 'original_language') 18 | final String? originalLanguage; 19 | @JsonKey(name: 'original_title') 20 | final String? originalTitle; 21 | final String? overview; 22 | final double? popularity; 23 | @JsonKey(name: 'poster_path') 24 | final String? posterPath; 25 | @JsonKey(name: 'release_date') 26 | final String? releaseDate; 27 | final String? title; 28 | final bool? video; 29 | @JsonKey(name: 'vote_average') 30 | final double? voteAverage; 31 | @JsonKey(name: 'vote_count') 32 | final int? voteCount; 33 | 34 | const MovieDetailModel({ 35 | this.adult, 36 | this.backdropPath, 37 | this.genreIds, 38 | this.id, 39 | this.originalLanguage, 40 | this.originalTitle, 41 | this.overview, 42 | this.popularity, 43 | this.posterPath, 44 | this.releaseDate, 45 | this.title, 46 | this.video, 47 | this.voteAverage, 48 | this.voteCount, 49 | }); 50 | 51 | factory MovieDetailModel.fromJson(Map json) { 52 | return _$MovieDetailModelFromJson(json); 53 | } 54 | 55 | @override 56 | MovieDetailEntity toEntity() => MovieDetailEntity( 57 | adult: adult, 58 | backdropPath: backdropPath, 59 | genreIds: genreIds, 60 | id: id, 61 | originalLanguage: originalLanguage, 62 | originalTitle: originalTitle, 63 | overview: overview, 64 | popularity: popularity, 65 | posterPath: posterPath, 66 | releaseDate: releaseDate, 67 | title: title, 68 | video: video, 69 | voteAverage: voteAverage, 70 | voteCount: voteCount, 71 | ); 72 | 73 | Map toJson() => _$MovieDetailModelToJson(this); 74 | 75 | @override 76 | List get props { 77 | return [ 78 | adult, 79 | backdropPath, 80 | genreIds, 81 | id, 82 | originalLanguage, 83 | originalTitle, 84 | overview, 85 | popularity, 86 | posterPath, 87 | releaseDate, 88 | title, 89 | video, 90 | voteAverage, 91 | voteCount, 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_detail/movie_detail_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'movie_detail_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MovieDetailModel _$MovieDetailModelFromJson(Map json) => 10 | MovieDetailModel( 11 | adult: json['adult'] as bool?, 12 | backdropPath: json['backdrop_path'] as String?, 13 | genreIds: 14 | (json['genre_ids'] as List?)?.map((e) => e as int).toList(), 15 | id: json['id'] as int?, 16 | originalLanguage: json['original_language'] as String?, 17 | originalTitle: json['original_title'] as String?, 18 | overview: json['overview'] as String?, 19 | popularity: (json['popularity'] as num?)?.toDouble(), 20 | posterPath: json['poster_path'] as String?, 21 | releaseDate: json['release_date'] as String?, 22 | title: json['title'] as String?, 23 | video: json['video'] as bool?, 24 | voteAverage: (json['vote_average'] as num?)?.toDouble(), 25 | voteCount: json['vote_count'] as int?, 26 | ); 27 | 28 | Map _$MovieDetailModelToJson(MovieDetailModel instance) => 29 | { 30 | 'adult': instance.adult, 31 | 'backdrop_path': instance.backdropPath, 32 | 'genre_ids': instance.genreIds, 33 | 'id': instance.id, 34 | 'original_language': instance.originalLanguage, 35 | 'original_title': instance.originalTitle, 36 | 'overview': instance.overview, 37 | 'popularity': instance.popularity, 38 | 'poster_path': instance.posterPath, 39 | 'release_date': instance.releaseDate, 40 | 'title': instance.title, 41 | 'video': instance.video, 42 | 'vote_average': instance.voteAverage, 43 | 'vote_count': instance.voteCount, 44 | }; 45 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_listings/movie_listings_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | import '../../../domain/entities/movie_listings/movie_listings_entity.dart'; 5 | import '../../datasources/_mappers/entity_convertable.dart'; 6 | import '../movie_detail/movie_detail_model.dart'; 7 | 8 | part 'movie_listings_model.g.dart'; 9 | 10 | @JsonSerializable() 11 | class MovieListingsModel extends Equatable with EntityConvertible { 12 | final int? page; 13 | @JsonKey(name: 'results') 14 | final List? movies; 15 | @JsonKey(name: 'total_pages') 16 | final int? totalPages; 17 | @JsonKey(name: 'total_results') 18 | final int? totalResults; 19 | 20 | const MovieListingsModel({ 21 | this.page, 22 | this.movies, 23 | this.totalPages, 24 | this.totalResults, 25 | }); 26 | 27 | factory MovieListingsModel.fromJson(Map json) { 28 | return _$MovieListingsModelFromJson(json); 29 | } 30 | 31 | @override 32 | MovieListingsEntity toEntity() => MovieListingsEntity( 33 | movies: movies?.map((e) => e.toEntity()).toList(), 34 | page: page, 35 | totalPages: totalPages, 36 | totalResults: totalResults, 37 | ); 38 | 39 | Map toJson() => _$MovieListingsModelToJson(this); 40 | 41 | @override 42 | List get props => [page, movies, totalPages, totalResults]; 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/data/models/movie_listings/movie_listings_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'movie_listings_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MovieListingsModel _$MovieListingsModelFromJson(Map json) => 10 | MovieListingsModel( 11 | page: json['page'] as int?, 12 | movies: (json['results'] as List?) 13 | ?.map((e) => MovieDetailModel.fromJson(e as Map)) 14 | .toList(), 15 | totalPages: json['total_pages'] as int?, 16 | totalResults: json['total_results'] as int?, 17 | ); 18 | 19 | Map _$MovieListingsModelToJson(MovieListingsModel instance) => 20 | { 21 | 'page': instance.page, 22 | 'results': instance.movies, 23 | 'total_pages': instance.totalPages, 24 | 'total_results': instance.totalResults, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/data/repositories/actor/actor_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:fpdart/fpdart.dart'; 3 | 4 | import '../../../core/exceptions/network/network_exception.dart'; 5 | import '../../../domain/entities/actor_detail/actor_detail_entity.dart'; 6 | import '../../../domain/entities/actor_social_media/actor_social_medias_entity.dart'; 7 | import '../../../domain/repositories/actor/actor_repository.dart'; 8 | import '../../datasources/export_datasources.dart'; 9 | 10 | class ActorRepositoryImpl implements ActorRepository { 11 | final ActorRemoteDataSource _actorRemoteDataSource; 12 | 13 | ActorRepositoryImpl(this._actorRemoteDataSource); 14 | 15 | @override 16 | Future> getActorDetail({required String actorId}) async { 17 | try { 18 | final result = await _actorRemoteDataSource.getActorDetail(actorId: actorId); 19 | 20 | return Right(result.toEntity()); 21 | } on DioException catch (e) { 22 | return Left(NetworkException.fromDioError(e)); 23 | } 24 | } 25 | 26 | @override 27 | Future> getActorSocialMedia({required String actorId}) async { 28 | try { 29 | final result = await _actorRemoteDataSource.getActorSocialMedia(actorId: actorId); 30 | 31 | return Right(result.toEntity()); 32 | } on DioException catch (e) { 33 | return Left(NetworkException.fromDioError(e)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/data/repositories/export_repository_impls.dart: -------------------------------------------------------------------------------- 1 | export 'actor/actor_repository_impl.dart'; 2 | export 'movie/movie_repository_impl.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/data/repositories/movie/movie_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:fpdart/fpdart.dart'; 3 | import 'package:isar/isar.dart'; 4 | 5 | import '../../../core/exceptions/database/database_exception.dart'; 6 | import '../../../core/exceptions/network/network_exception.dart'; 7 | import '../../../domain/entities/export_entities.dart'; 8 | import '../../../domain/repositories/movie/movie_repository.dart'; 9 | import '../../datasources/export_datasources.dart'; 10 | import '../../datasources/local/_collections/movie_detail/movie_detail_collection.dart'; 11 | 12 | class MovieRepositoryImpl implements MovieRepository { 13 | final MovieRemoteDataSource _movieRemoteDataSource; 14 | final MovieLocalDataSource _movieLocalDataSource; 15 | 16 | MovieRepositoryImpl(this._movieRemoteDataSource, this._movieLocalDataSource); 17 | 18 | //* REMOTE 19 | @override 20 | Future> getPopularMovies({required int page}) async { 21 | try { 22 | final result = await _movieRemoteDataSource.getPopularMovies(page: page); 23 | 24 | return Right(result.toEntity()); 25 | } on DioException catch (e) { 26 | return Left(NetworkException.fromDioError(e)); 27 | } 28 | } 29 | 30 | @override 31 | Future> getTopRatedMovies({required int page}) async { 32 | try { 33 | final result = await _movieRemoteDataSource.getTopRatedMovies(page: page); 34 | 35 | return Right(result.toEntity()); 36 | } on DioException catch (e) { 37 | return Left(NetworkException.fromDioError(e)); 38 | } 39 | } 40 | 41 | @override 42 | Future> getMovieCredits({required int movieId}) async { 43 | try { 44 | final result = await _movieRemoteDataSource.getMovieCredits(movieId: movieId); 45 | 46 | return Right(result.toEntity()); 47 | } on DioException catch (e) { 48 | return Left(NetworkException.fromDioError(e)); 49 | } 50 | } 51 | 52 | //* LOCAL 53 | @override 54 | Future>> getSavedMovieDetails() async { 55 | try { 56 | final result = await _movieLocalDataSource.getSavedMovieDetails(); 57 | 58 | return Right(result.map((e) => e.toEntity()).toList()); 59 | } on IsarError catch (e) { 60 | return Left(DatabaseException.fromIsarError(e)); 61 | } 62 | } 63 | 64 | @override 65 | Future> saveMovieDetails({required MovieDetailEntity? movieDetailEntity}) async { 66 | try { 67 | final result = await _movieLocalDataSource.saveMovieDetail( 68 | movieDetailCollection: MovieDetailCollection().fromEntity(movieDetailEntity), 69 | ); 70 | 71 | return Right(result); 72 | } on IsarError catch (e) { 73 | return Left(DatabaseException.fromIsarError(e)); 74 | } 75 | } 76 | 77 | @override 78 | Future> deleteMovieDetail({required int? movieId}) async { 79 | try { 80 | final result = await _movieLocalDataSource.deleteMovieDetail(movieId: movieId); 81 | 82 | return Right(result); 83 | } on IsarError catch (e) { 84 | return Left(DatabaseException.fromIsarError(e)); 85 | } 86 | } 87 | 88 | @override 89 | Future> isSavedMovieDetail({required int? movieId}) async { 90 | try { 91 | final result = await _movieLocalDataSource.isSavedMovieDetail(movieId: movieId); 92 | 93 | return Right(result); 94 | } on IsarError catch (e) { 95 | return Left(DatabaseException.fromIsarError(e)); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/domain/entities/actor_detail/actor_detail_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class ActorDetailEntity extends Equatable { 4 | final bool? adult; 5 | final List? alsoKnownAs; 6 | final String? biography; 7 | final String? birthday; 8 | final String? deathday; 9 | final int? gender; 10 | final String? homepage; 11 | final int? id; 12 | final String? imdbId; 13 | final String? knownForDepartment; 14 | final String? name; 15 | final String? placeOfBirth; 16 | final double? popularity; 17 | final String? profilePath; 18 | 19 | const ActorDetailEntity({ 20 | this.adult, 21 | this.alsoKnownAs, 22 | this.biography, 23 | this.birthday, 24 | this.deathday, 25 | this.gender, 26 | this.homepage, 27 | this.id, 28 | this.imdbId, 29 | this.knownForDepartment, 30 | this.name, 31 | this.placeOfBirth, 32 | this.popularity, 33 | this.profilePath, 34 | }); 35 | 36 | @override 37 | List get props { 38 | return [ 39 | adult, 40 | alsoKnownAs, 41 | biography, 42 | birthday, 43 | deathday, 44 | gender, 45 | homepage, 46 | id, 47 | imdbId, 48 | knownForDepartment, 49 | name, 50 | placeOfBirth, 51 | popularity, 52 | profilePath, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/domain/entities/actor_social_media/actor_social_medias_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class ActorSocialMediaEntity extends Equatable { 4 | final int? id; 5 | final String? freebaseMid; 6 | final String? freebaseId; 7 | final String? imdbId; 8 | final int? tvrageId; 9 | final String? wikidataId; 10 | final String? facebookId; 11 | final String? instagramId; 12 | final String? tiktokId; 13 | final String? twitterId; 14 | final String? youtubeId; 15 | 16 | const ActorSocialMediaEntity({ 17 | this.id, 18 | this.freebaseMid, 19 | this.freebaseId, 20 | this.imdbId, 21 | this.tvrageId, 22 | this.wikidataId, 23 | this.facebookId, 24 | this.instagramId, 25 | this.tiktokId, 26 | this.twitterId, 27 | this.youtubeId, 28 | }); 29 | 30 | @override 31 | List get props { 32 | return [ 33 | id, 34 | freebaseMid, 35 | freebaseId, 36 | imdbId, 37 | tvrageId, 38 | wikidataId, 39 | facebookId, 40 | instagramId, 41 | tiktokId, 42 | twitterId, 43 | youtubeId, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/domain/entities/export_entities.dart: -------------------------------------------------------------------------------- 1 | export 'actor_detail/actor_detail_entity.dart'; 2 | export 'actor_social_media/actor_social_medias_entity.dart'; 3 | export 'movie_credit/cast_entity.dart'; 4 | export 'movie_credit/crew_entity.dart'; 5 | export 'movie_credit/movie_credit_entity.dart'; 6 | export 'movie_detail/movie_detail_entity.dart'; 7 | export 'movie_listings/movie_listings_entity.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/domain/entities/movie_credit/cast_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class CastEntity extends Equatable { 4 | final bool? adult; 5 | final int? gender; 6 | final int? id; 7 | final String? knownForDepartment; 8 | final String? name; 9 | final String? originalName; 10 | final double? popularity; 11 | final String? profilePath; 12 | final int? castId; 13 | final String? character; 14 | final String? creditId; 15 | final int? order; 16 | 17 | const CastEntity({ 18 | this.adult, 19 | this.gender, 20 | this.id, 21 | this.knownForDepartment, 22 | this.name, 23 | this.originalName, 24 | this.popularity, 25 | this.profilePath, 26 | this.castId, 27 | this.character, 28 | this.creditId, 29 | this.order, 30 | }); 31 | 32 | @override 33 | List get props { 34 | return [ 35 | adult, 36 | gender, 37 | id, 38 | knownForDepartment, 39 | name, 40 | originalName, 41 | popularity, 42 | profilePath, 43 | castId, 44 | character, 45 | creditId, 46 | order, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/domain/entities/movie_credit/crew_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class CrewEntity extends Equatable { 4 | final bool? adult; 5 | final int? gender; 6 | final int? id; 7 | final String? knownForDepartment; 8 | final String? name; 9 | final String? originalName; 10 | final double? popularity; 11 | final String? profilePath; 12 | final String? creditId; 13 | final String? department; 14 | final String? job; 15 | 16 | const CrewEntity({ 17 | this.adult, 18 | this.gender, 19 | this.id, 20 | this.knownForDepartment, 21 | this.name, 22 | this.originalName, 23 | this.popularity, 24 | this.profilePath, 25 | this.creditId, 26 | this.department, 27 | this.job, 28 | }); 29 | 30 | @override 31 | List get props => [ 32 | adult, 33 | gender, 34 | id, 35 | knownForDepartment, 36 | name, 37 | originalName, 38 | popularity, 39 | profilePath, 40 | creditId, 41 | department, 42 | job, 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/domain/entities/movie_credit/movie_credit_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'cast_entity.dart'; 4 | import 'crew_entity.dart'; 5 | 6 | class MovieCreditEntity extends Equatable { 7 | final int? id; 8 | final List? cast; 9 | final List? crew; 10 | 11 | const MovieCreditEntity({this.id, this.cast, this.crew}); 12 | 13 | @override 14 | List get props => [id, cast, crew]; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/domain/entities/movie_detail/movie_detail_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class MovieDetailEntity extends Equatable { 4 | final String? posterPath; 5 | final bool? adult; 6 | final String? overview; 7 | final String? releaseDate; 8 | final List? genreIds; 9 | final int? id; 10 | final String? originalTitle; 11 | final String? originalLanguage; 12 | final String? title; 13 | final String? backdropPath; 14 | final double? popularity; 15 | final int? voteCount; 16 | final bool? video; 17 | final double? voteAverage; 18 | 19 | const MovieDetailEntity({ 20 | this.posterPath, 21 | this.adult, 22 | this.overview, 23 | this.releaseDate, 24 | this.genreIds, 25 | this.id, 26 | this.originalTitle, 27 | this.originalLanguage, 28 | this.title, 29 | this.backdropPath, 30 | this.popularity, 31 | this.voteCount, 32 | this.video, 33 | this.voteAverage, 34 | }); 35 | 36 | @override 37 | List get props { 38 | return [ 39 | posterPath, 40 | adult, 41 | overview, 42 | releaseDate, 43 | genreIds, 44 | id, 45 | originalTitle, 46 | originalLanguage, 47 | title, 48 | backdropPath, 49 | popularity, 50 | voteCount, 51 | video, 52 | voteAverage, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/domain/entities/movie_listings/movie_listings_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../movie_detail/movie_detail_entity.dart'; 4 | 5 | class MovieListingsEntity extends Equatable { 6 | final int? page; 7 | final List? movies; 8 | final int? totalPages; 9 | final int? totalResults; 10 | 11 | const MovieListingsEntity({ 12 | this.page, 13 | this.movies, 14 | this.totalPages, 15 | this.totalResults, 16 | }); 17 | 18 | @override 19 | List get props => [page, movies, totalPages, totalResults]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/domain/repositories/actor/actor_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | 3 | import '../../../core/exceptions/network/network_exception.dart'; 4 | import '../../entities/actor_detail/actor_detail_entity.dart'; 5 | import '../../entities/actor_social_media/actor_social_medias_entity.dart'; 6 | 7 | abstract class ActorRepository { 8 | /// Retrieves the detailed information of an actor. 9 | Future> getActorDetail({required String actorId}); 10 | 11 | /// Retrieves the social media information of an actor. 12 | Future> getActorSocialMedia({required String actorId}); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/domain/repositories/export_repositories.dart: -------------------------------------------------------------------------------- 1 | export 'actor/actor_repository.dart'; 2 | export 'movie/movie_repository.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/domain/repositories/movie/movie_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | 3 | import '../../../core/exceptions/database/database_exception.dart'; 4 | import '../../../core/exceptions/network/network_exception.dart'; 5 | import '../../entities/export_entities.dart'; 6 | 7 | abstract class MovieRepository { 8 | //* Remote Data Source 9 | /// Retrieves the top rated movies from the server. 10 | Future> getTopRatedMovies({required int page}); 11 | 12 | /// Retrieves the popular movies from the server. 13 | Future> getPopularMovies({required int page}); 14 | 15 | /// Retrieves the credits for a specific movie from the server. 16 | Future> getMovieCredits({required int movieId}); 17 | 18 | //* Local Data Source 19 | /// Retrieves the saved movie details from the database. 20 | Future>> getSavedMovieDetails(); 21 | 22 | /// Saves the movie details to the database. 23 | Future> saveMovieDetails({required MovieDetailEntity? movieDetailEntity}); 24 | 25 | /// Deletes the movie detail from the database. 26 | Future> deleteMovieDetail({required int? movieId}); 27 | 28 | /// Checks if the movie detail is saved in the database. 29 | Future> isSavedMovieDetail({required int? movieId}); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/domain/usecases/actor/actor_usecases.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | 3 | import '../../../core/exceptions/network/network_exception.dart'; 4 | import '../../entities/actor_detail/actor_detail_entity.dart'; 5 | import '../../entities/actor_social_media/actor_social_medias_entity.dart'; 6 | import '../../repositories/actor/actor_repository.dart'; 7 | 8 | class ActorUsecases { 9 | final ActorRepository _actorRepository; 10 | 11 | const ActorUsecases(this._actorRepository); 12 | 13 | /// This method gets actor detail from the remote data source. 14 | Future> getActorDetail({required String actorId}) async { 15 | return _actorRepository.getActorDetail(actorId: actorId); 16 | } 17 | 18 | /// This method gets actor social media from the remote data source. 19 | Future> getActorSocialMedia({required String actorId}) async { 20 | return _actorRepository.getActorSocialMedia(actorId: actorId); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/domain/usecases/export_usecases.dart: -------------------------------------------------------------------------------- 1 | export 'actor/actor_usecases.dart'; 2 | export 'movie/movie_usecases.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/domain/usecases/movie/movie_usecases.dart: -------------------------------------------------------------------------------- 1 | import 'package:fpdart/fpdart.dart'; 2 | 3 | import '../../../core/exceptions/database/database_exception.dart'; 4 | import '../../../core/exceptions/network/network_exception.dart'; 5 | import '../../entities/export_entities.dart'; 6 | import '../../repositories/movie/movie_repository.dart'; 7 | 8 | class MovieUsecases { 9 | final MovieRepository _movieRepository; 10 | 11 | const MovieUsecases(this._movieRepository); 12 | 13 | //* REMOTE 14 | /// This method gets popular movies from the remote data source. 15 | Future> getPopularMovies({required int page}) async { 16 | return _movieRepository.getPopularMovies(page: page); 17 | } 18 | 19 | /// This method gets top rated movies from the remote data source. 20 | Future> getTopRatedMovies({required int page}) async { 21 | return _movieRepository.getTopRatedMovies(page: page); 22 | } 23 | 24 | /// Retrieves the movie credits for a given movie ID. 25 | Future> getMovieCredits({required int movieId}) async { 26 | return _movieRepository.getMovieCredits(movieId: movieId); 27 | } 28 | 29 | //* LOCAL 30 | /// This method gets saved movie details from the local data source. 31 | Future>> getSavedMovieDetails() async { 32 | return _movieRepository.getSavedMovieDetails(); 33 | } 34 | 35 | /// This method toggles bookmark for a movie in the local data source. 36 | Future> toggleBookmark({required MovieDetailEntity? movieDetailEntity}) async { 37 | final isSaved = await _movieRepository.isSavedMovieDetail(movieId: movieDetailEntity?.id); 38 | 39 | return isSaved.fold( 40 | (error) { 41 | return Left(error); 42 | }, 43 | (isSaved) { 44 | if (isSaved) { 45 | return _movieRepository.deleteMovieDetail(movieId: movieDetailEntity?.id); 46 | } else { 47 | return _movieRepository.saveMovieDetails(movieDetailEntity: movieDetailEntity); 48 | } 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/injector.dart: -------------------------------------------------------------------------------- 1 | part of '../main.dart'; 2 | 3 | final injector = GetIt.instance; 4 | 5 | Future init() async { 6 | injector 7 | //* Network 8 | ..registerLazySingleton(DioClient.new) 9 | 10 | //* Database 11 | ..registerLazySingleton(LocalDatabase.new) 12 | 13 | //* Data Sources 14 | ..registerLazySingleton(() => MovieRemoteDataSourceImpl(injector())) 15 | ..registerLazySingleton(() => MovieLocalDataSourceImpl(injector())) 16 | ..registerLazySingleton(() => ActorRemoteDataSourceImpl(injector())) 17 | 18 | //* Repositories 19 | ..registerLazySingleton(() => MovieRepositoryImpl(injector(), injector())) 20 | ..registerLazySingleton(() => ActorRepositoryImpl(injector())) 21 | 22 | //* Usecases 23 | ..registerLazySingleton(() => MovieUsecases(injector())) 24 | ..registerLazySingleton(() => ActorUsecases(injector())) 25 | 26 | //* Cubits 27 | ..registerLazySingleton(ThemeCubit.new) 28 | ..registerLazySingleton(() => GetPopularMoviesCubit(injector())) 29 | ..registerLazySingleton(() => GetTopRatedMoviesCubit(injector())) 30 | ..registerLazySingleton(() => GetSavedMoviesCubit(injector())) 31 | ..registerFactory(() => ToggleBookmarkCubit(injector())) 32 | ..registerFactory(() => GetMovieCreditsCubit(injector())) 33 | ..registerLazySingleton(() => GetActorDetailCubit(injector())) 34 | ..registerFactory(() => GetActorSocialMediaCubit(injector())); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/presentation/_widget/movie_detail/actor_card.dart: -------------------------------------------------------------------------------- 1 | part of '../../view/movie_detail_view.dart'; 2 | 3 | class _ActorCard extends StatelessWidget { 4 | const _ActorCard({required this.castEntity}); 5 | 6 | final CastEntity? castEntity; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return InkWell( 11 | onTap: () async => SocialMediaBottomSheet( 12 | actorId: castEntity?.id?.toString(), 13 | actorName: castEntity?.name, 14 | ).show(context), 15 | borderRadius: BorderRadius.circular(12.r), 16 | child: DecoratedBox( 17 | decoration: BoxDecoration( 18 | borderRadius: BorderRadius.circular(12.r), 19 | color: Theme.of(context).disabledColor, 20 | ), 21 | child: Row( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | ClipRRect( 25 | borderRadius: BorderRadius.only( 26 | topLeft: Radius.circular(12.r), 27 | bottomLeft: Radius.circular(12.r), 28 | ), 29 | child: SizedBox( 30 | width: 65.w, 31 | child: BaseNetworkImage.originalImageSize( 32 | castEntity?.profilePath, 33 | hasRadius: false, 34 | ), 35 | ), 36 | ), 37 | Expanded( 38 | child: Padding( 39 | padding: EdgeInsets.symmetric(horizontal: 16.w), 40 | child: Column( 41 | mainAxisAlignment: MainAxisAlignment.center, 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [ 44 | Text( 45 | '${castEntity?.name}', 46 | maxLines: 2, 47 | style: Theme.of(context).textTheme.titleMedium, 48 | ), 49 | 5.verticalSpace, 50 | Text( 51 | '${castEntity?.character}', 52 | maxLines: 1, 53 | style: Theme.of(context).textTheme.labelMedium, 54 | ), 55 | ], 56 | ), 57 | ), 58 | ) 59 | ], 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/presentation/_widget/movie_detail/tag_container.dart: -------------------------------------------------------------------------------- 1 | part of '../../view/movie_detail_view.dart'; 2 | 3 | class _TagContainer extends StatelessWidget { 4 | const _TagContainer(this.tag); 5 | 6 | final String tag; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | height: 24, 12 | padding: const EdgeInsets.symmetric(horizontal: 15), 13 | alignment: Alignment.center, 14 | decoration: BoxDecoration( 15 | color: Theme.of(context).primaryColor, 16 | borderRadius: BorderRadius.circular(12), 17 | ), 18 | child: Text(tag, style: Theme.of(context).textTheme.labelLarge?.copyWith(color: Colors.white)), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/presentation/_widget/movies/movie_listing_widget.dart: -------------------------------------------------------------------------------- 1 | part of '../../view/movies_view.dart'; 2 | 3 | class _MovieListingWidget extends HookWidget { 4 | const _MovieListingWidget({ 5 | required this.movies, 6 | required this.whenScrollBottom, 7 | required this.hasReachedMax, 8 | }); 9 | 10 | final List? movies; 11 | final void Function() whenScrollBottom; 12 | final bool hasReachedMax; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final scrollController = useScrollController(); 17 | useAutomaticKeepAlive(); 18 | 19 | final listener = useMemoized( 20 | () => () async { 21 | final isBottom = scrollController.position.maxScrollExtent == scrollController.offset && 22 | scrollController.position.pixels == scrollController.position.maxScrollExtent; 23 | 24 | if (isBottom) { 25 | whenScrollBottom.call(); 26 | } 27 | }, 28 | ); 29 | 30 | useEffect( 31 | () { 32 | scrollController.addListener(listener); 33 | 34 | return () => scrollController.removeListener(listener); 35 | }, 36 | [], 37 | ); 38 | 39 | return ListView( 40 | shrinkWrap: true, 41 | controller: scrollController, 42 | padding: EdgeInsets.zero, 43 | physics: const BouncingScrollPhysics(), 44 | children: [ 45 | GridView.builder( 46 | padding: EdgeInsets.symmetric(vertical: 10.h), 47 | itemCount: movies?.length ?? 0, 48 | physics: const NeverScrollableScrollPhysics(), 49 | shrinkWrap: true, 50 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 51 | crossAxisCount: 2, 52 | childAspectRatio: 0.7, 53 | crossAxisSpacing: 5, 54 | mainAxisSpacing: 10, 55 | ), 56 | itemBuilder: (_, index) { 57 | final tag = UniqueKey(); 58 | 59 | return GestureDetector( 60 | onTap: () => context.router.push(MovieDetailRoute(movieDetail: movies?[index], heroTag: tag)), 61 | child: Hero(tag: tag, child: MovieCard(movie: movies?[index])), 62 | ); 63 | }, 64 | ), 65 | if (!hasReachedMax) const BaseIndicator(), 66 | ], 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/actor/export_actor_cubits.dart: -------------------------------------------------------------------------------- 1 | export 'get_actor_detail/get_actor_detail_cubit.dart'; 2 | export 'get_actor_social_media/get_actor_social_media_cubit.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/actor/get_actor_detail/get_actor_detail_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/actor_detail/actor_detail_entity.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'get_actor_detail_state.dart'; 8 | 9 | class GetActorDetailCubit extends Cubit { 10 | GetActorDetailCubit(this._actorUsecases) : super(GetActorDetailInitial()); 11 | 12 | Future getActorDetail({required String actorId}) async { 13 | try { 14 | emit(const GetActorDetailLoading()); 15 | 16 | final result = await _actorUsecases.getActorDetail(actorId: actorId); 17 | 18 | result.fold( 19 | (error) => emit(GetActorDetailError(message: error.message)), 20 | (success) => emit(GetActorDetailLoaded(actor: success)), 21 | ); 22 | } catch (_) { 23 | rethrow; 24 | } 25 | } 26 | 27 | final ActorUsecases _actorUsecases; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/actor/get_actor_detail/get_actor_detail_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_actor_detail_cubit.dart'; 2 | 3 | sealed class GetActorDetailState extends Equatable { 4 | const GetActorDetailState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetActorDetailInitial extends GetActorDetailState {} 11 | 12 | final class GetActorDetailLoading extends GetActorDetailState { 13 | const GetActorDetailLoading(); 14 | } 15 | 16 | final class GetActorDetailLoaded extends GetActorDetailState { 17 | const GetActorDetailLoaded({required this.actor}); 18 | 19 | final ActorDetailEntity actor; 20 | 21 | @override 22 | List get props => [actor]; 23 | } 24 | 25 | final class GetActorDetailError extends GetActorDetailState { 26 | const GetActorDetailError({required this.message}); 27 | 28 | final String message; 29 | 30 | @override 31 | List get props => [message]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/actor/get_actor_social_media/get_actor_social_media_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/export_entities.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'get_actor_social_media_state.dart'; 8 | 9 | class GetActorSocialMediaCubit extends Cubit { 10 | GetActorSocialMediaCubit(this._actorUsecases) : super(GetActorSocialMediaInitial()); 11 | 12 | Future getActorSocialMedia({required String actorId}) async { 13 | try { 14 | emit(const GetActorSocialMediaLoading()); 15 | 16 | final result = await _actorUsecases.getActorSocialMedia(actorId: actorId); 17 | 18 | result.fold( 19 | (error) => emit(GetActorSocialMediaError(message: error.message)), 20 | (success) => emit(GetActorSocialMediaLoaded(data: success)), 21 | ); 22 | } catch (_) { 23 | rethrow; 24 | } 25 | } 26 | 27 | final ActorUsecases _actorUsecases; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/actor/get_actor_social_media/get_actor_social_media_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_actor_social_media_cubit.dart'; 2 | 3 | sealed class GetActorSocialMediaState extends Equatable { 4 | const GetActorSocialMediaState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetActorSocialMediaInitial extends GetActorSocialMediaState {} 11 | 12 | final class GetActorSocialMediaLoading extends GetActorSocialMediaState { 13 | const GetActorSocialMediaLoading(); 14 | } 15 | 16 | final class GetActorSocialMediaLoaded extends GetActorSocialMediaState { 17 | const GetActorSocialMediaLoaded({required this.data}); 18 | 19 | final ActorSocialMediaEntity data; 20 | 21 | String? get instagramId => data.instagramId; 22 | String? get twitterId => data.twitterId; 23 | String? get facebookId => data.facebookId; 24 | String? get youtubeId => data.youtubeId; 25 | String? get imdbId => data.imdbId; 26 | String? get tiktokId => data.tiktokId; 27 | 28 | @override 29 | List get props => [data]; 30 | } 31 | 32 | final class GetActorSocialMediaError extends GetActorSocialMediaState { 33 | const GetActorSocialMediaError({required this.message}); 34 | 35 | final String message; 36 | 37 | @override 38 | List get props => [message]; 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/export_movie_cubits.dart: -------------------------------------------------------------------------------- 1 | export 'get_popular_movies/get_popular_movies_cubit.dart'; 2 | export 'get_saved_movies/get_saved_movies_cubit.dart'; 3 | export 'get_top_rated_movies/get_top_rated_movies_cubit.dart'; 4 | export 'toggle_bookmark/toggle_bookmark_cubit.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_movie_credits/get_movie_credits_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/export_entities.dart'; 5 | import '../../../../domain/usecases/movie/movie_usecases.dart'; 6 | 7 | part 'get_movie_credits_state.dart'; 8 | 9 | class GetMovieCreditsCubit extends Cubit { 10 | GetMovieCreditsCubit(this._movieUsecases) : super(const GetMovieCreditsInitial()); 11 | 12 | Future getMovieCredits(int movieId) async { 13 | emit(const GetMovieCreditsLoading()); 14 | 15 | final result = await _movieUsecases.getMovieCredits(movieId: movieId); 16 | 17 | result.fold( 18 | (error) => emit(GetMovieCreditsError(error.message)), 19 | (movieCreditEntity) => emit(GetMovieCreditsLoaded(movieCreditEntity)), 20 | ); 21 | } 22 | 23 | final MovieUsecases _movieUsecases; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_movie_credits/get_movie_credits_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_movie_credits_cubit.dart'; 2 | 3 | sealed class GetMovieCreditsState extends Equatable { 4 | const GetMovieCreditsState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetMovieCreditsInitial extends GetMovieCreditsState { 11 | const GetMovieCreditsInitial(); 12 | } 13 | 14 | final class GetMovieCreditsLoading extends GetMovieCreditsState { 15 | const GetMovieCreditsLoading(); 16 | } 17 | 18 | final class GetMovieCreditsLoaded extends GetMovieCreditsState { 19 | const GetMovieCreditsLoaded(this.movieCreditEntity); 20 | 21 | final MovieCreditEntity movieCreditEntity; 22 | 23 | @override 24 | List get props => [movieCreditEntity]; 25 | } 26 | 27 | final class GetMovieCreditsError extends GetMovieCreditsState { 28 | const GetMovieCreditsError(this.message); 29 | 30 | final String message; 31 | 32 | @override 33 | List get props => [message]; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_popular_movies/get_popular_movies_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'get_popular_movies_state.dart'; 8 | 9 | class GetPopularMoviesCubit extends Cubit { 10 | GetPopularMoviesCubit(this._movieUsecases) : super(GetPopularMoviesInitial()); 11 | 12 | /// The list of popular movie details. 13 | final List _movieList = []; 14 | 15 | /// The current page number for fetching popular movies. 16 | int _page = 1; 17 | 18 | /// A flag indicating whether the maximum number of movies has been reached. 19 | bool hasReachedMax = false; 20 | 21 | Future getPopularMovies() async { 22 | try { 23 | // Checks if the maximum limit has been reached. 24 | if (hasReachedMax) return; 25 | 26 | /// Checks if the current state is not [GetPopularMoviesLoaded]. 27 | /// If it is not, emits a [GetPopularMoviesLoading] state. 28 | if (state is! GetPopularMoviesLoaded) { 29 | emit(const GetPopularMoviesLoading()); 30 | } 31 | 32 | final result = await _movieUsecases.getPopularMovies(page: _page); 33 | 34 | result.fold( 35 | (error) => emit(GetPopularMoviesError(message: error.message)), 36 | (success) { 37 | // Increases the page number and adds the movies from the [success] response to the movie list. 38 | // If a movie already exists in the movie list, it will not be added again. 39 | // If the number of movies in the [success] response is less than 20, sets [hasReachedMax] to true. 40 | // Emits a [GetPopularMoviesLoaded] state with the updated movie list. 41 | 42 | _page++; 43 | _movieList.addAll(success.movies?.where((movie) => _movieList.contains(movie) == false) ?? []); 44 | 45 | /// Checks if the number of movies in the [success] response is less than 20. 46 | /// If so, sets [hasReachedMax] to true. 47 | if ((success.movies?.length ?? 0) < 20) { 48 | hasReachedMax = true; 49 | } 50 | 51 | emit(GetPopularMoviesLoaded(movies: List.of(_movieList))); 52 | }, 53 | ); 54 | } catch (_) { 55 | rethrow; 56 | } 57 | } 58 | 59 | final MovieUsecases _movieUsecases; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_popular_movies/get_popular_movies_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_popular_movies_cubit.dart'; 2 | 3 | sealed class GetPopularMoviesState extends Equatable { 4 | const GetPopularMoviesState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetPopularMoviesInitial extends GetPopularMoviesState {} 11 | 12 | final class GetPopularMoviesLoading extends GetPopularMoviesState { 13 | const GetPopularMoviesLoading(); 14 | } 15 | 16 | final class GetPopularMoviesLoaded extends GetPopularMoviesState { 17 | const GetPopularMoviesLoaded({required this.movies}); 18 | 19 | final List movies; 20 | 21 | @override 22 | List get props => [movies]; 23 | } 24 | 25 | final class GetPopularMoviesError extends GetPopularMoviesState { 26 | const GetPopularMoviesError({required this.message}); 27 | 28 | final String message; 29 | 30 | @override 31 | List get props => [message]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_saved_movies/get_saved_movies_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'get_saved_movies_state.dart'; 8 | 9 | class GetSavedMoviesCubit extends Cubit { 10 | GetSavedMoviesCubit(this._movieUsecases) : super(GetSavedMoviesInitial()); 11 | 12 | Future getSavedMovieDetails() async { 13 | try { 14 | if (state is! GetSavedMoviesLoaded) { 15 | emit(const GetSavedMoviesLoading()); 16 | } 17 | 18 | final result = await _movieUsecases.getSavedMovieDetails(); 19 | 20 | result.fold( 21 | (error) => emit(GetSavedMoviesError(message: error.message)), 22 | (success) => emit(GetSavedMoviesLoaded(movies: success)), 23 | ); 24 | } catch (_) { 25 | rethrow; 26 | } 27 | } 28 | 29 | final MovieUsecases _movieUsecases; 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_saved_movies/get_saved_movies_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_saved_movies_cubit.dart'; 2 | 3 | sealed class GetSavedMoviesState extends Equatable { 4 | const GetSavedMoviesState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetSavedMoviesInitial extends GetSavedMoviesState {} 11 | 12 | final class GetSavedMoviesLoading extends GetSavedMoviesState { 13 | const GetSavedMoviesLoading(); 14 | } 15 | 16 | final class GetSavedMoviesLoaded extends GetSavedMoviesState { 17 | const GetSavedMoviesLoaded({required this.movies}); 18 | 19 | final List? movies; 20 | 21 | @override 22 | List get props => [movies!]; 23 | } 24 | 25 | final class GetSavedMoviesError extends GetSavedMoviesState { 26 | const GetSavedMoviesError({required this.message}); 27 | 28 | final String message; 29 | 30 | @override 31 | List get props => [message]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_top_rated_movies/get_top_rated_movies_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'get_top_rated_movies_state.dart'; 8 | 9 | class GetTopRatedMoviesCubit extends Cubit { 10 | GetTopRatedMoviesCubit(this._movieUsecases) : super(GetTopRatedMoviesInitial()); 11 | 12 | /// The list of popular movie details. 13 | final List _movieList = []; 14 | 15 | /// The current page number for fetching popular movies. 16 | int _page = 1; 17 | 18 | /// A flag indicating whether the maximum number of movies has been reached. 19 | bool hasReachedMax = false; 20 | 21 | Future getTopRatedMovies() async { 22 | try { 23 | // Checks if the maximum limit has been reached. 24 | if (hasReachedMax) return; 25 | 26 | /// Checks if the current state is not [GetPopularMoviesLoaded]. 27 | /// If it is not, emits a [GetPopularMoviesLoading] state. 28 | if (state is! GetTopRatedMoviesLoaded) { 29 | emit(const GetTopRatedMoviesLoading()); 30 | } 31 | 32 | final result = await _movieUsecases.getTopRatedMovies(page: _page); 33 | 34 | result.fold( 35 | (error) => emit(GetTopRatedMoviesError(message: error.message)), 36 | (success) { 37 | // Increases the page number and adds the movies from the [success] response to the movie list. 38 | // If a movie already exists in the movie list, it will not be added again. 39 | // If the number of movies in the [success] response is less than 20, sets [hasReachedMax] to true. 40 | // Emits a [GetPopularMoviesLoaded] state with the updated movie list. 41 | 42 | _page++; 43 | _movieList.addAll(success.movies?.where((movie) => _movieList.contains(movie) == false) ?? []); 44 | 45 | /// Checks if the number of movies in the [success] response is less than 20. 46 | /// If so, sets [hasReachedMax] to true. 47 | if ((success.movies?.length ?? 0) < 20) { 48 | hasReachedMax = true; 49 | } 50 | 51 | emit(GetTopRatedMoviesLoaded(movies: List.of(_movieList))); 52 | }, 53 | ); 54 | } catch (_) { 55 | rethrow; 56 | } 57 | } 58 | 59 | final MovieUsecases _movieUsecases; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/get_top_rated_movies/get_top_rated_movies_state.dart: -------------------------------------------------------------------------------- 1 | part of 'get_top_rated_movies_cubit.dart'; 2 | 3 | sealed class GetTopRatedMoviesState extends Equatable { 4 | const GetTopRatedMoviesState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class GetTopRatedMoviesInitial extends GetTopRatedMoviesState {} 11 | 12 | final class GetTopRatedMoviesLoading extends GetTopRatedMoviesState { 13 | const GetTopRatedMoviesLoading(); 14 | } 15 | 16 | final class GetTopRatedMoviesLoaded extends GetTopRatedMoviesState { 17 | const GetTopRatedMoviesLoaded({required this.movies}); 18 | 19 | final List movies; 20 | 21 | @override 22 | List get props => [movies]; 23 | } 24 | 25 | final class GetTopRatedMoviesError extends GetTopRatedMoviesState { 26 | const GetTopRatedMoviesError({required this.message}); 27 | 28 | final String message; 29 | 30 | @override 31 | List get props => [message]; 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/toggle_bookmark/toggle_bookmark_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | import '../../../../domain/entities/movie_detail/movie_detail_entity.dart'; 5 | import '../../../../domain/usecases/export_usecases.dart'; 6 | 7 | part 'toggle_bookmark_state.dart'; 8 | 9 | class ToggleBookmarkCubit extends Cubit { 10 | ToggleBookmarkCubit(this._movieUsecases) : super(const ToggleBookmarkInitial()); 11 | 12 | Future toggleBookmark({required MovieDetailEntity? movieDetailEntity}) async { 13 | try { 14 | emit(const ToggleBookmarkLoading()); 15 | 16 | final result = await _movieUsecases.toggleBookmark(movieDetailEntity: movieDetailEntity); 17 | 18 | result.fold( 19 | (error) => emit(ToggleBookmarkError(message: error.message)), 20 | (success) => emit(const ToggleBookmarkSuccess()), 21 | ); 22 | } catch (_) { 23 | rethrow; 24 | } 25 | } 26 | 27 | final MovieUsecases _movieUsecases; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/presentation/cubit/movie/toggle_bookmark/toggle_bookmark_state.dart: -------------------------------------------------------------------------------- 1 | part of 'toggle_bookmark_cubit.dart'; 2 | 3 | sealed class ToggleBookmarkState extends Equatable { 4 | const ToggleBookmarkState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | final class ToggleBookmarkInitial extends ToggleBookmarkState { 11 | const ToggleBookmarkInitial(); 12 | } 13 | 14 | final class ToggleBookmarkLoading extends ToggleBookmarkState { 15 | const ToggleBookmarkLoading(); 16 | } 17 | 18 | final class ToggleBookmarkSuccess extends ToggleBookmarkState { 19 | const ToggleBookmarkSuccess(); 20 | } 21 | 22 | final class ToggleBookmarkError extends ToggleBookmarkState { 23 | const ToggleBookmarkError({required this.message}); 24 | 25 | final String message; 26 | 27 | @override 28 | List get props => [message]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/presentation/view/bookmarks_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | 6 | import '../../config/router/app_router.gr.dart'; 7 | import '../../core/components/buttons/retry_button.dart'; 8 | import '../../core/components/card/movie_card.dart'; 9 | import '../../core/components/indicator/base_indicator.dart'; 10 | import '../../domain/entities/export_entities.dart'; 11 | import '../cubit/movie/get_saved_movies/get_saved_movies_cubit.dart'; 12 | 13 | @RoutePage() 14 | class BookmarksView extends StatelessWidget { 15 | const BookmarksView({super.key}); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return BlocBuilder( 20 | builder: (_, getSavedMoviesState) { 21 | return switch (getSavedMoviesState) { 22 | GetSavedMoviesLoaded() => _BookmarksView(getSavedMoviesState.movies), 23 | GetSavedMoviesError() => Padding( 24 | padding: const EdgeInsets.all(12).r, 25 | child: RetryButton( 26 | retryAction: () => context.read().getSavedMovieDetails(), 27 | text: getSavedMoviesState.message, 28 | ), 29 | ), 30 | _ => const BaseIndicator(), 31 | }; 32 | }, 33 | ); 34 | } 35 | } 36 | 37 | class _BookmarksView extends StatelessWidget { 38 | const _BookmarksView(this.movies); 39 | 40 | final List? movies; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Scaffold( 45 | body: Builder(builder: (context) { 46 | if (movies?.isEmpty ?? true) { 47 | return Center( 48 | child: Text( 49 | 'There is no bookmarked movie yet.', 50 | style: Theme.of(context).textTheme.bodyLarge, 51 | ), 52 | ); 53 | } 54 | 55 | return GridView.builder( 56 | padding: EdgeInsets.symmetric(vertical: 10.h), 57 | itemCount: movies?.length ?? 0, 58 | shrinkWrap: true, 59 | physics: const BouncingScrollPhysics(), 60 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 61 | crossAxisCount: 2, 62 | childAspectRatio: 0.7, 63 | crossAxisSpacing: 5, 64 | mainAxisSpacing: 10, 65 | ), 66 | itemBuilder: (context, index) { 67 | final tag = UniqueKey(); 68 | 69 | return GestureDetector( 70 | onTap: () => context.router.push(MovieDetailRoute(movieDetail: movies?[index], heroTag: tag)), 71 | child: Hero(tag: tag, child: MovieCard(movie: movies?[index])), 72 | ); 73 | }, 74 | ); 75 | }), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/presentation/view/master_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:get_it/get_it.dart'; 5 | 6 | import '../../config/router/app_router.gr.dart'; 7 | import '../../core/theme/cubit/theme_cubit.dart'; 8 | import '../cubit/movie/export_movie_cubits.dart'; 9 | 10 | @RoutePage() 11 | class MasterView extends StatelessWidget { 12 | const MasterView({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return MultiBlocProvider( 17 | providers: [ 18 | BlocProvider(create: (context) => GetIt.I()..getPopularMovies()), 19 | BlocProvider(create: (context) => GetIt.I()..getTopRatedMovies()), 20 | ], 21 | child: AutoTabsScaffold( 22 | resizeToAvoidBottomInset: true, 23 | lazyLoad: false, 24 | animationDuration: const Duration(milliseconds: 100), 25 | navigatorObservers: () => [HeroController()], 26 | appBarBuilder: (context, tabsRouter) { 27 | return AppBar( 28 | title: Text(tabsRouter.current.title.call(context)), 29 | actions: [ 30 | IconButton( 31 | onPressed: () => context.read().toggleTheme(brightness: Theme.of(context).brightness), 32 | icon: Icon( 33 | Theme.of(context).brightness == Brightness.dark 34 | ? Icons.light_mode_outlined 35 | : Icons.dark_mode_outlined, 36 | ), 37 | ), 38 | ], 39 | elevation: 0, 40 | ); 41 | }, 42 | routes: const [ 43 | MoviesRoute(), 44 | BookmarksRoute(), 45 | ], 46 | bottomNavigationBuilder: (_, tabsRouter) { 47 | return BottomNavigationBar( 48 | onTap: tabsRouter.setActiveIndex, 49 | currentIndex: tabsRouter.activeIndex, 50 | items: const [ 51 | BottomNavigationBarItem( 52 | icon: Icon(Icons.movie), 53 | label: 'Movies', 54 | ), 55 | BottomNavigationBarItem( 56 | icon: Icon(Icons.bookmark), 57 | label: 'Bookmarks', 58 | ), 59 | ], 60 | ); 61 | }, 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/presentation/view/movies_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:flutter_hooks/flutter_hooks.dart'; 5 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 | 7 | import '../../config/router/app_router.gr.dart'; 8 | import '../../core/components/buttons/retry_button.dart'; 9 | import '../../core/components/card/movie_card.dart'; 10 | import '../../core/components/indicator/base_indicator.dart'; 11 | import '../../domain/entities/export_entities.dart'; 12 | import '../cubit/movie/export_movie_cubits.dart'; 13 | 14 | part '../_widget/movies/movie_listing_widget.dart'; 15 | 16 | @RoutePage() 17 | class MoviesView extends HookWidget { 18 | const MoviesView({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final tabbarController = useTabController(initialLength: 2); 23 | 24 | return Scaffold( 25 | appBar: AppBar( 26 | toolbarHeight: 0, 27 | bottom: TabBar( 28 | controller: tabbarController, 29 | tabs: const [ 30 | Tab(text: 'Popular'), 31 | Tab(text: 'Top Rated'), 32 | ], 33 | ), 34 | ), 35 | body: TabBarView( 36 | controller: tabbarController, 37 | physics: const NeverScrollableScrollPhysics(), 38 | children: [ 39 | BlocBuilder( 40 | builder: (context, getPopularMoviesState) { 41 | if (getPopularMoviesState is GetPopularMoviesError) { 42 | return Padding( 43 | padding: const EdgeInsets.all(12).r, 44 | child: RetryButton( 45 | text: getPopularMoviesState.message, 46 | retryAction: context.read().getPopularMovies, 47 | ), 48 | ); 49 | } 50 | 51 | if (getPopularMoviesState is GetPopularMoviesLoaded) { 52 | return _MovieListingWidget( 53 | hasReachedMax: context.watch().hasReachedMax, 54 | movies: getPopularMoviesState.movies, 55 | whenScrollBottom: () async => context.read().getPopularMovies(), 56 | ); 57 | } 58 | 59 | return const BaseIndicator(); 60 | }, 61 | ), 62 | BlocBuilder( 63 | builder: (context, getTopRatedMoviesState) { 64 | if (getTopRatedMoviesState is GetTopRatedMoviesError) { 65 | return Padding( 66 | padding: const EdgeInsets.all(12).r, 67 | child: RetryButton( 68 | text: getTopRatedMoviesState.message, 69 | retryAction: context.read().getTopRatedMovies, 70 | ), 71 | ); 72 | } 73 | 74 | if (getTopRatedMoviesState is GetTopRatedMoviesLoaded) { 75 | return _MovieListingWidget( 76 | hasReachedMax: context.watch().hasReachedMax, 77 | movies: getTopRatedMoviesState.movies, 78 | whenScrollBottom: () async => context.read().getTopRatedMovies(), 79 | ); 80 | } 81 | 82 | return const BaseIndicator(); 83 | }, 84 | ), 85 | ], 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_clean_architecture 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | version: 0.1.0 5 | 6 | environment: 7 | sdk: '>=3.1.0 <4.0.0' 8 | 9 | dependencies: 10 | # for routing => https://pub.dev/packages/auto_route 11 | auto_route: ^8.1.4 12 | 13 | # for caching images => https://pub.dev/packages/cached_network_image 14 | cached_network_image: ^3.4.1 15 | 16 | # for network requests => https://pub.dev/packages/dio 17 | dio: ^5.8.0+1 18 | 19 | # for equality => https://pub.dev/packages/equatable 20 | equatable: ^2.0.7 21 | 22 | # for theme => https://pub.dev/packages/flex_color_scheme 23 | flex_color_scheme: ^8.1.0 24 | 25 | flutter: 26 | sdk: flutter 27 | 28 | # for state management => https://pub.dev/packages/flutter_bloc 29 | flutter_bloc: ^9.0.0 30 | 31 | # for environments => https://pub.dev/packages/flutter_dotenv 32 | flutter_dotenv: ^5.2.1 33 | 34 | # for generating assets code => https://pub.dev/packages/flutter_gen 35 | flutter_gen: ^5.8.0 36 | 37 | # for useful hooks => https://pub.dev/packages/flutter_hooks 38 | flutter_hooks: ^0.20.5 39 | 40 | # for responsibility => https://pub.dev/packages/flutter_screenutil 41 | flutter_screenutil: ^5.9.3 42 | 43 | # for svg => https://pub.dev/packages/flutter_svg 44 | flutter_svg: ^2.0.17 45 | 46 | # for functional programming => https://pub.dev/packages/fpdart 47 | fpdart: ^1.1.1 48 | 49 | # for dependency injection => https://pub.dev/packages/get_it 50 | get_it: ^8.0.3 51 | 52 | # for theme or locale management => https://pub.dev/packages/hydrated_bloc 53 | hydrated_bloc: ^10.0.0 54 | 55 | intl: ^0.20.2 56 | 57 | # for local data source 58 | isar: ^3.1.0+1 59 | isar_flutter_libs: ^3.1.0+1 60 | 61 | # for creating api models => https://pub.dev/packages/json_annotation 62 | json_annotation: ^4.9.0 63 | 64 | # for app data directories 65 | path_provider: ^2.1.5 66 | 67 | # a dio interceptor for developer => https://pub.dev/packages/pretty_dio_logger 68 | pretty_dio_logger: ^1.4.0 69 | 70 | # for shimmer effect => https://pub.dev/packages/shimmer 71 | shimmer: ^3.0.0 72 | 73 | # for url launcher => https://pub.dev/packages/url_launcher 74 | url_launcher: ^6.3.1 75 | 76 | dev_dependencies: 77 | # auto_route generator => https://pub.dev/packages/auto_route_generator 78 | auto_route_generator: ^8.0.0 79 | 80 | # for testing bloc => https://pub.dev/packages/bloc_test 81 | bloc_test: ^10.0.0 82 | 83 | build_runner: ^2.4.13 84 | flutter_gen_runner: ^5.8.0 85 | flutter_lints: ^5.0.0 86 | flutter_test: 87 | sdk: flutter 88 | 89 | # for local data source 90 | isar_generator: ^3.1.0+1 91 | 92 | # for creating api models => https://pub.dev/packages/json_serializable 93 | json_serializable: ^6.8.0 94 | 95 | # for testing => https://pub.dev/packages/mockito 96 | mockito: ^5.4.4 97 | 98 | flutter: 99 | uses-material-design: true 100 | 101 | assets: 102 | - .env 103 | - assets/icons/ 104 | 105 | flutter_gen: 106 | output: lib/src/config/gen/ 107 | inputs: 108 | - assets/icons/ 109 | 110 | integrations: 111 | flutter_svg: true 112 | -------------------------------------------------------------------------------- /scripts/refresh_script.sh: -------------------------------------------------------------------------------- 1 | echo "Refreshing..." 2 | 3 | flutter clean 4 | 5 | flutter pub get 6 | 7 | cd ios 8 | 9 | pod install 10 | 11 | cd .. 12 | 13 | echo "Refreshed" -------------------------------------------------------------------------------- /test/_utils/_dummy/actor_detail_dummy_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "adult": false, 3 | "also_known_as": [ 4 | "Шон Эстин", 5 | "Sean Patrick Duke" 6 | ], 7 | "biography": "Sean Astin (born February 25, 1971) is an American film actor, director, and producer better known for his film roles as Mikey Walsh in The Goonies, the title character of Rudy, and Samwise Gamgee in the Lord of the Rings trilogy. In television, he appeared as Lynn McGill in the fifth season of 24. He also provided the voice for the title character in Disney's Special Agent Oso.", 8 | "birthday": "1971-02-25", 9 | "deathday": null, 10 | "gender": 2, 11 | "homepage": null, 12 | "id": 1328, 13 | "imdb_id": "nm0000276", 14 | "known_for_department": "Acting", 15 | "name": "Sean Astin", 16 | "place_of_birth": "Santa Monica, California, USA", 17 | "popularity": 41.78, 18 | "profile_path": "/5oJzy6Ra0tuMEV7mfxjtqye5qUX.jpg" 19 | } -------------------------------------------------------------------------------- /test/_utils/_dummy/actor_social_media_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1328, 3 | "freebase_mid": "/m/0svqs", 4 | "freebase_id": "/en/sean_astin", 5 | "imdb_id": "nm0000276", 6 | "tvrage_id": 11899, 7 | "wikidata_id": "Q189351", 8 | "facebook_id": null, 9 | "instagram_id": "seanastin", 10 | "tiktok_id": null, 11 | "twitter_id": null, 12 | "youtube_id": null 13 | } -------------------------------------------------------------------------------- /test/_utils/_dummy/movie_credit_dummy_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "adult": false, 3 | "backdrop_path": "/x2RS3uTcsJJ9IfjNPcgDmukoEcQ.jpg", 4 | "belongs_to_collection": { 5 | "id": 119, 6 | "name": "The Lord of the Rings Collection", 7 | "poster_path": "/oENY593nKRVL2PnxXsMtlh8izb4.jpg", 8 | "backdrop_path": "/bccR2CGTWVVSZAG0yqmy3DIvhTX.jpg" 9 | }, 10 | "budget": 93000000, 11 | "genres": [ 12 | { 13 | "id": 12, 14 | "name": "Adventure" 15 | }, 16 | { 17 | "id": 14, 18 | "name": "Fantasy" 19 | }, 20 | { 21 | "id": 28, 22 | "name": "Action" 23 | } 24 | ], 25 | "homepage": "http://www.lordoftherings.net/", 26 | "id": 120, 27 | "imdb_id": "tt0120737", 28 | "original_language": "en", 29 | "original_title": "The Lord of the Rings: The Fellowship of the Ring", 30 | "overview": "Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed.", 31 | "popularity": 128.422, 32 | "poster_path": "/6oom5QYQ2yQTMJIbnvbkBL9cHo6.jpg", 33 | "production_companies": [ 34 | { 35 | "id": 12, 36 | "logo_path": "/iaYpEp3LQmb8AfAtmTvpqd4149c.png", 37 | "name": "New Line Cinema", 38 | "origin_country": "US" 39 | }, 40 | { 41 | "id": 11, 42 | "logo_path": "/6FAuASQHybRkZUk08p9PzSs9ezM.png", 43 | "name": "WingNut Films", 44 | "origin_country": "NZ" 45 | }, 46 | { 47 | "id": 5237, 48 | "logo_path": "/mlnr7vsBHvLye8oEb5A76C0t8x9.png", 49 | "name": "The Saul Zaentz Company", 50 | "origin_country": "US" 51 | } 52 | ], 53 | "production_countries": [ 54 | { 55 | "iso_3166_1": "NZ", 56 | "name": "New Zealand" 57 | }, 58 | { 59 | "iso_3166_1": "US", 60 | "name": "United States of America" 61 | } 62 | ], 63 | "release_date": "2001-12-18", 64 | "revenue": 871368364, 65 | "runtime": 179, 66 | "spoken_languages": [ 67 | { 68 | "english_name": "English", 69 | "iso_639_1": "en", 70 | "name": "English" 71 | } 72 | ], 73 | "status": "Released", 74 | "tagline": "One ring to rule them all", 75 | "title": "The Lord of the Rings: The Fellowship of the Ring", 76 | "video": false, 77 | "vote_average": 8.407, 78 | "vote_count": 23788 79 | } -------------------------------------------------------------------------------- /test/_utils/json_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | /// Reads a JSON file with the given [name] and returns the decoded JSON object. 5 | /// The [name] should be the relative path of the JSON file from the current directory. 6 | /// 7 | /// Example usage: 8 | /// ```dart 9 | /// dynamic data = jsonReader('data.json'); 10 | /// ``` 11 | dynamic jsonReader(String name) { 12 | final dir = Directory.current.path; 13 | 14 | return jsonDecode(File('$dir/test/_utils/_dummy/$name').readAsStringSync()); 15 | } 16 | -------------------------------------------------------------------------------- /test/_utils/mocks/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_clean_architecture/src/core/network/dio_client.dart'; 2 | import 'package:flutter_clean_architecture/src/data/datasources/export_datasources.dart'; 3 | import 'package:flutter_clean_architecture/src/domain/repositories/actor/actor_repository.dart'; 4 | import 'package:flutter_clean_architecture/src/domain/repositories/movie/movie_repository.dart'; 5 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 6 | import 'package:mockito/annotations.dart'; 7 | 8 | @GenerateMocks([ 9 | DioClient, 10 | MovieRemoteDataSource, 11 | MovieLocalDataSource, 12 | ActorRemoteDataSource, 13 | MovieRepository, 14 | ActorRepository, 15 | MovieUsecases, 16 | ActorUsecases, 17 | ]) 18 | void main() {} 19 | -------------------------------------------------------------------------------- /test/presentation/cubit/actor/get_actor_detail_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_clean_architecture/src/core/exceptions/network/network_exception.dart'; 6 | import 'package:flutter_clean_architecture/src/domain/entities/export_entities.dart'; 7 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 8 | import 'package:flutter_clean_architecture/src/presentation/cubit/actor/export_actor_cubits.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:fpdart/fpdart.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import '../../../_utils/mocks/mocks.mocks.dart'; 14 | 15 | void main() { 16 | late final ActorUsecases mockActorUsecases; 17 | 18 | late final ActorDetailEntity tActorDetailEntity; 19 | 20 | setUpAll(() { 21 | mockActorUsecases = MockActorUsecases(); 22 | 23 | tActorDetailEntity = const ActorDetailEntity( 24 | id: 1, 25 | name: 'name', 26 | biography: 'biography', 27 | birthday: 'birthday', 28 | deathday: 'deathday', 29 | placeOfBirth: 'placeOfBirth', 30 | profilePath: 'profilePath', 31 | ); 32 | }); 33 | 34 | blocTest( 35 | 'should emit [GetActorDetailLoading, GetActorDetailLoaded] when success', 36 | build: () { 37 | provideDummy>(Right(tActorDetailEntity)); 38 | 39 | when(mockActorUsecases.getActorDetail(actorId: '1')).thenAnswer((_) async => Right(tActorDetailEntity)); 40 | 41 | return GetActorDetailCubit(mockActorUsecases); 42 | }, 43 | act: (bloc) => bloc.getActorDetail(actorId: '1'), 44 | expect: () => [const GetActorDetailLoading(), GetActorDetailLoaded(actor: tActorDetailEntity)], 45 | verify: (_) => verify(mockActorUsecases.getActorDetail(actorId: '1')).called(1), 46 | ); 47 | 48 | blocTest( 49 | 'should emit [GetActorDetailLoading, GetActorDetailError] when internet connection error occurs with SocketException', 50 | build: () { 51 | final dioException = DioException( 52 | requestOptions: RequestOptions(), 53 | error: const SocketException(''), 54 | type: DioExceptionType.connectionError, 55 | ); 56 | provideDummy>(Left(NetworkException.fromDioError(dioException))); 57 | 58 | when(mockActorUsecases.getActorDetail(actorId: '1')) 59 | .thenAnswer((_) async => Left(NetworkException.fromDioError(dioException))); 60 | 61 | return GetActorDetailCubit(mockActorUsecases); 62 | }, 63 | act: (bloc) => bloc.getActorDetail(actorId: '1'), 64 | expect: () => [ 65 | const GetActorDetailLoading(), 66 | const GetActorDetailError(message: 'Please check your internet connection'), 67 | ], 68 | verify: (_) => verify(mockActorUsecases.getActorDetail(actorId: '1')).called(1), 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /test/presentation/cubit/actor/get_actor_social_media_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_clean_architecture/src/core/exceptions/network/network_exception.dart'; 6 | import 'package:flutter_clean_architecture/src/domain/entities/export_entities.dart'; 7 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 8 | import 'package:flutter_clean_architecture/src/presentation/cubit/actor/export_actor_cubits.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:fpdart/fpdart.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import '../../../_utils/mocks/mocks.mocks.dart'; 14 | 15 | void main() { 16 | late final ActorUsecases mockActorUsecases; 17 | 18 | late final ActorSocialMediaEntity tActorSocialMediaEntity; 19 | 20 | setUpAll(() { 21 | mockActorUsecases = MockActorUsecases(); 22 | 23 | tActorSocialMediaEntity = const ActorSocialMediaEntity( 24 | id: 1, 25 | facebookId: 'facebookId', 26 | instagramId: 'instagramId', 27 | twitterId: 'twitterId', 28 | ); 29 | }); 30 | 31 | blocTest( 32 | 'should emit [GetActorSocialMediaLoading, GetActorSocialMediaLoaded] when success', 33 | build: () { 34 | provideDummy>(Right(tActorSocialMediaEntity)); 35 | 36 | when(mockActorUsecases.getActorSocialMedia(actorId: '1')).thenAnswer((_) async => Right(tActorSocialMediaEntity)); 37 | 38 | return GetActorSocialMediaCubit(mockActorUsecases); 39 | }, 40 | act: (bloc) => bloc.getActorSocialMedia(actorId: '1'), 41 | expect: () => [const GetActorSocialMediaLoading(), GetActorSocialMediaLoaded(data: tActorSocialMediaEntity)], 42 | verify: (_) => verify(mockActorUsecases.getActorSocialMedia(actorId: '1')).called(1), 43 | ); 44 | 45 | blocTest( 46 | 'should emit [GetActorSocialMediaLoading, GetActorSocialMediaError] when internet connection error occurs with SocketException', 47 | build: () { 48 | final dioException = DioException( 49 | requestOptions: RequestOptions(), 50 | error: const SocketException(''), 51 | type: DioExceptionType.connectionError, 52 | ); 53 | provideDummy>(Left(NetworkException.fromDioError(dioException))); 54 | 55 | when(mockActorUsecases.getActorSocialMedia(actorId: '1')) 56 | .thenAnswer((_) async => Left(NetworkException.fromDioError(dioException))); 57 | 58 | return GetActorSocialMediaCubit(mockActorUsecases); 59 | }, 60 | act: (bloc) => bloc.getActorSocialMedia(actorId: '1'), 61 | expect: () => [ 62 | const GetActorSocialMediaLoading(), 63 | const GetActorSocialMediaError(message: 'Please check your internet connection'), 64 | ], 65 | verify: (_) => verify(mockActorUsecases.getActorSocialMedia(actorId: '1')).called(1), 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /test/presentation/cubit/movie/get_movie_credits_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_clean_architecture/src/core/exceptions/network/network_exception.dart'; 6 | import 'package:flutter_clean_architecture/src/domain/entities/export_entities.dart'; 7 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 8 | import 'package:flutter_clean_architecture/src/presentation/cubit/movie/get_movie_credits/get_movie_credits_cubit.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:fpdart/fpdart.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import '../../../_utils/mocks/mocks.mocks.dart'; 14 | 15 | void main() { 16 | late final MovieUsecases mockMovieUsecases; 17 | 18 | late final MovieCreditEntity tMovieCreditEntity; 19 | 20 | setUpAll(() { 21 | mockMovieUsecases = MockMovieUsecases(); 22 | 23 | tMovieCreditEntity = const MovieCreditEntity( 24 | id: 1, 25 | cast: [ 26 | CastEntity(id: 1, name: 'name', profilePath: 'profilePath', character: 'character'), 27 | CastEntity(id: 2, name: 'name', profilePath: 'profilePath', character: 'character'), 28 | ], 29 | crew: [ 30 | CrewEntity(id: 1, name: 'name', profilePath: 'profilePath', job: 'job'), 31 | CrewEntity(id: 2, name: 'name', profilePath: 'profilePath', job: 'job'), 32 | ], 33 | ); 34 | }); 35 | 36 | blocTest( 37 | 'should emit [GetMovieCreditsLoading, GetMovieCreditsLoaded] when success', 38 | build: () { 39 | provideDummy>(Right(tMovieCreditEntity)); 40 | 41 | when(mockMovieUsecases.getMovieCredits(movieId: tMovieCreditEntity.id ?? 0)) 42 | .thenAnswer((_) async => Right(tMovieCreditEntity)); 43 | 44 | return GetMovieCreditsCubit(mockMovieUsecases); 45 | }, 46 | act: (bloc) => bloc.getMovieCredits(tMovieCreditEntity.id ?? 0), 47 | expect: () => [const GetMovieCreditsLoading(), GetMovieCreditsLoaded(tMovieCreditEntity)], 48 | verify: (_) => verify(mockMovieUsecases.getMovieCredits(movieId: tMovieCreditEntity.id ?? 0)).called(1), 49 | ); 50 | 51 | blocTest( 52 | 'should emit [GetMovieCreditsLoading, GetMovieCreditsError] when internet connection error occurs with SocketException', 53 | build: () { 54 | final dioException = DioException( 55 | requestOptions: RequestOptions(), 56 | error: const SocketException(''), 57 | type: DioExceptionType.connectionError, 58 | ); 59 | 60 | provideDummy>(Left(NetworkException.fromDioError(dioException))); 61 | 62 | when(mockMovieUsecases.getMovieCredits(movieId: tMovieCreditEntity.id ?? 0)) 63 | .thenAnswer((_) async => Left(NetworkException.fromDioError(dioException))); 64 | 65 | return GetMovieCreditsCubit(mockMovieUsecases); 66 | }, 67 | act: (bloc) => bloc.getMovieCredits(tMovieCreditEntity.id ?? 0), 68 | expect: () => [const GetMovieCreditsLoading(), const GetMovieCreditsError('Please check your internet connection')], 69 | verify: (_) => verify(mockMovieUsecases.getMovieCredits(movieId: tMovieCreditEntity.id ?? 0)).called(1), 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /test/presentation/cubit/movie/get_saved_movies_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_clean_architecture/src/core/exceptions/database/database_exception.dart'; 3 | import 'package:flutter_clean_architecture/src/domain/entities/export_entities.dart'; 4 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 5 | import 'package:flutter_clean_architecture/src/presentation/cubit/movie/export_movie_cubits.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:fpdart/fpdart.dart'; 8 | import 'package:isar/isar.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import '../../../_utils/mocks/mocks.mocks.dart'; 12 | 13 | void main() { 14 | late final MovieUsecases mockMovieUsecases; 15 | 16 | late final MovieDetailEntity tMovieDetailEntity1; 17 | late final MovieDetailEntity tMovieDetailEntity2; 18 | late final MovieDetailEntity tMovieDetailEntity3; 19 | late final MovieDetailEntity tMovieDetailEntity4; 20 | 21 | setUpAll(() { 22 | mockMovieUsecases = MockMovieUsecases(); 23 | 24 | tMovieDetailEntity1 = const MovieDetailEntity( 25 | id: 1, 26 | title: 'title', 27 | overview: 'overview', 28 | posterPath: 'posterPath', 29 | backdropPath: 'backdropPath', 30 | ); 31 | 32 | tMovieDetailEntity2 = const MovieDetailEntity( 33 | id: 2, 34 | title: 'title', 35 | overview: 'overview', 36 | posterPath: 'posterPath', 37 | backdropPath: 'backdropPath', 38 | ); 39 | 40 | tMovieDetailEntity3 = const MovieDetailEntity( 41 | id: 3, 42 | title: 'title', 43 | overview: 'overview', 44 | posterPath: 'posterPath', 45 | backdropPath: 'backdropPath', 46 | ); 47 | 48 | tMovieDetailEntity4 = const MovieDetailEntity( 49 | id: 4, 50 | title: 'title', 51 | overview: 'overview', 52 | posterPath: 'posterPath', 53 | backdropPath: 'backdropPath', 54 | ); 55 | }); 56 | 57 | blocTest( 58 | 'should emit [GetSavedMoviesLoading, GetSavedMoviesLoaded] when success', 59 | setUp: () { 60 | final tMovieList = [ 61 | tMovieDetailEntity1, 62 | tMovieDetailEntity2, 63 | tMovieDetailEntity3, 64 | tMovieDetailEntity4, 65 | ]; 66 | 67 | provideDummy>>(Right(tMovieList)); 68 | 69 | when(mockMovieUsecases.getSavedMovieDetails()).thenAnswer((_) async => Right(tMovieList)); 70 | }, 71 | build: () => GetSavedMoviesCubit(mockMovieUsecases), 72 | act: (bloc) => bloc.getSavedMovieDetails(), 73 | expect: () => [ 74 | const GetSavedMoviesLoading(), 75 | GetSavedMoviesLoaded( 76 | movies: [ 77 | tMovieDetailEntity1, 78 | tMovieDetailEntity2, 79 | tMovieDetailEntity3, 80 | tMovieDetailEntity4, 81 | ], 82 | ), 83 | ], 84 | verify: (_) => verify(mockMovieUsecases.getSavedMovieDetails()).called(1), 85 | ); 86 | 87 | blocTest( 88 | 'should emit [GetSavedMoviesLoading, GetSavedMoviesError] when failure', 89 | setUp: () { 90 | final isarError = IsarError('isarError'); 91 | 92 | provideDummy>>(Left(DatabaseException.fromIsarError(isarError))); 93 | 94 | when(mockMovieUsecases.getSavedMovieDetails()) 95 | .thenAnswer((_) async => Left(DatabaseException.fromIsarError(isarError))); 96 | }, 97 | build: () => GetSavedMoviesCubit(mockMovieUsecases), 98 | act: (bloc) => bloc.getSavedMovieDetails(), 99 | expect: () => [ 100 | const GetSavedMoviesLoading(), 101 | const GetSavedMoviesError(message: 'isarError'), 102 | ], 103 | verify: (_) => verify(mockMovieUsecases.getSavedMovieDetails()).called(1), 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /test/presentation/cubit/movie/toggle_bookmark_cubit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_clean_architecture/src/core/exceptions/database/database_exception.dart'; 3 | import 'package:flutter_clean_architecture/src/domain/entities/export_entities.dart'; 4 | import 'package:flutter_clean_architecture/src/domain/usecases/export_usecases.dart'; 5 | import 'package:flutter_clean_architecture/src/presentation/cubit/movie/export_movie_cubits.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:fpdart/fpdart.dart'; 8 | import 'package:isar/isar.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import '../../../_utils/mocks/mocks.mocks.dart'; 12 | 13 | void main() { 14 | late final MovieUsecases mockMovieUsecases; 15 | 16 | late final MovieDetailEntity tMovieDetailEntity; 17 | 18 | setUpAll(() { 19 | mockMovieUsecases = MockMovieUsecases(); 20 | 21 | tMovieDetailEntity = const MovieDetailEntity( 22 | id: 1, 23 | title: 'title', 24 | overview: 'overview', 25 | posterPath: 'posterPath', 26 | backdropPath: 'backdropPath', 27 | ); 28 | }); 29 | 30 | blocTest( 31 | 'should emit [ToggleBookmarkLoading, ToggleBookmarkLoaded] when success', 32 | setUp: () { 33 | provideDummy>(const Right(null)); 34 | 35 | when(mockMovieUsecases.toggleBookmark(movieDetailEntity: tMovieDetailEntity)) 36 | .thenAnswer((_) async => const Right(null)); 37 | }, 38 | build: () => ToggleBookmarkCubit(mockMovieUsecases), 39 | act: (ToggleBookmarkCubit cubit) => cubit.toggleBookmark(movieDetailEntity: tMovieDetailEntity), 40 | expect: () => [ 41 | const ToggleBookmarkLoading(), 42 | const ToggleBookmarkSuccess(), 43 | ], 44 | verify: (_) { 45 | verify(mockMovieUsecases.toggleBookmark(movieDetailEntity: tMovieDetailEntity)).called(1); 46 | }, 47 | ); 48 | 49 | blocTest( 50 | 'should emit [ToggleBookmarkLoading, ToggleBookmarkError] when failure', 51 | setUp: () { 52 | final isarError = IsarError('isarError'); 53 | 54 | provideDummy>(Left(DatabaseException.fromIsarError(isarError))); 55 | 56 | when(mockMovieUsecases.toggleBookmark(movieDetailEntity: tMovieDetailEntity)) 57 | .thenAnswer((_) async => Left(DatabaseException.fromIsarError(isarError))); 58 | }, 59 | build: () => ToggleBookmarkCubit(mockMovieUsecases), 60 | act: (ToggleBookmarkCubit cubit) => cubit.toggleBookmark(movieDetailEntity: tMovieDetailEntity), 61 | expect: () => [ 62 | const ToggleBookmarkLoading(), 63 | const ToggleBookmarkError(message: 'isarError'), 64 | ], 65 | verify: (_) => verify(mockMovieUsecases.toggleBookmark(movieDetailEntity: tMovieDetailEntity)).called(1), 66 | ); 67 | } 68 | --------------------------------------------------------------------------------