├── .github └── workflows │ └── flutter-ci.yaml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── about ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib │ ├── about.dart │ └── about_page.dart └── pubspec.yaml ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── dicoding │ │ │ │ └── ditonton │ │ │ │ └── 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 ├── art └── demo.gif ├── assets └── user.png ├── core ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── coverage │ └── lcov.info ├── lib │ ├── core.dart │ ├── presentation │ │ ├── pages │ │ │ ├── home_page.dart │ │ │ └── watchlist_page.dart │ │ └── provider │ │ │ └── home_notifier.dart │ ├── styles │ │ ├── colors.dart │ │ └── text_styles.dart │ └── utils │ │ ├── exception.dart │ │ ├── failure.dart │ │ ├── routes.dart │ │ ├── state_enum.dart │ │ ├── urls.dart │ │ └── utils.dart └── pubspec.yaml ├── coverage └── lcov.info ├── integration_test ├── app_test.dart └── robots │ ├── home_robot.dart │ ├── movie_detail_robot.dart │ ├── popular_movies_robot.dart │ ├── popular_tvs_robot.dart │ ├── search_robot.dart │ ├── top_rated_movies_robot.dart │ ├── top_rated_tvs_robot.dart │ ├── tv_detail_robot.dart │ └── watchlist_robot.dart ├── 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 ├── lib ├── injection.dart └── main.dart ├── movie ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib │ ├── data │ │ ├── datasources │ │ │ ├── db │ │ │ │ └── movie_database_helper.dart │ │ │ ├── movie_local_data_source.dart │ │ │ └── movie_remote_data_source.dart │ │ ├── models │ │ │ ├── genre_model.dart │ │ │ ├── media_image_model.dart │ │ │ ├── movie_detail_response.dart │ │ │ ├── movie_model.dart │ │ │ ├── movie_response.dart │ │ │ └── movie_table.dart │ │ └── repositories │ │ │ └── movie_repository_impl.dart │ ├── domain │ │ ├── entities │ │ │ ├── genre.dart │ │ │ ├── media_image.dart │ │ │ ├── movie.dart │ │ │ └── movie_detail.dart │ │ ├── repositories │ │ │ └── movie_repository.dart │ │ └── usecases │ │ │ ├── get_movie_detail.dart │ │ │ ├── get_movie_images.dart │ │ │ ├── get_movie_recommendations.dart │ │ │ ├── get_movie_watchlist_status.dart │ │ │ ├── get_now_playing_movies.dart │ │ │ ├── get_popular_movies.dart │ │ │ ├── get_top_rated_movies.dart │ │ │ ├── get_watchlist_movies.dart │ │ │ ├── remove_watchlist_movie.dart │ │ │ └── save_watchlist_movie.dart │ ├── movie.dart │ └── presentation │ │ ├── pages │ │ ├── main_movie_page.dart │ │ ├── movie_detail_page.dart │ │ ├── movie_watchlist_page.dart │ │ ├── popular_movies_page.dart │ │ └── top_rated_movies_page.dart │ │ ├── provider │ │ ├── movie_detail_notifier.dart │ │ ├── movie_images_notifier.dart │ │ ├── movie_list_notifier.dart │ │ ├── popular_movies_notifier.dart │ │ ├── top_rated_movies_notifier.dart │ │ └── watchlist_movie_notifier.dart │ │ └── widgets │ │ ├── horizontal_item_list.dart │ │ ├── item_card_list.dart │ │ ├── minimal_detail.dart │ │ └── sub_heading.dart ├── pubspec.yaml └── test │ ├── data │ ├── datasources │ │ ├── movie_local_data_source_test.dart │ │ └── movie_remote_data_source_test.dart │ ├── models │ │ ├── movie_detail_response_test.dart │ │ ├── movie_model_test.dart │ │ ├── movie_response_test.dart │ │ └── movie_table_test.dart │ └── repositories │ │ └── movie_repository_impl_test.dart │ ├── domain │ └── usecases │ │ ├── get_movie_detail_test.dart │ │ ├── get_movie_images_test.dart │ │ ├── get_movie_recommendations_test.dart │ │ ├── get_movie_watchlist_status_test.dart │ │ ├── get_now_playing_movies_test.dart │ │ ├── get_popular_movies_test.dart │ │ ├── get_top_rated_movies_test.dart │ │ ├── get_watchlist_movies_test.dart │ │ ├── remove_watchlist_movie_test.dart │ │ └── save_watchlist_movie_test.dart │ ├── helpers │ ├── dummy_objects.dart │ ├── dummy_responses │ │ ├── images.json │ │ ├── movie_detail.json │ │ ├── movie_recommendations.json │ │ ├── now_playing_movie.json │ │ ├── popular_top_rated_movie.json │ │ └── search_movie.json │ ├── json_reader.dart │ ├── test_helper.dart │ └── test_helper.mocks.dart │ └── presentation │ ├── pages │ ├── movie_detail_page_test.dart │ ├── movie_detail_page_test.mocks.dart │ ├── popular_movies_page_test.dart │ ├── popular_movies_page_test.mocks.dart │ ├── top_rated_movies_page_test.dart │ └── top_rated_movies_page_test.mocks.dart │ └── provider │ ├── movie_detail_notifier_test.dart │ ├── movie_detail_notifier_test.mocks.dart │ ├── movie_images_notifier_test.dart │ ├── movie_images_notifier_test.mocks.dart │ ├── movie_list_notifier_test.dart │ ├── movie_list_notifier_test.mocks.dart │ ├── popular_movies_notifier_test.dart │ ├── popular_movies_notifier_test.mocks.dart │ ├── top_rated_movies_notifier_test.dart │ ├── top_rated_movies_notifier_test.mocks.dart │ ├── watchlist_movie_notifier_test.dart │ └── watchlist_movie_notifier_test.mocks.dart ├── pubspec.lock ├── pubspec.yaml ├── search ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── coverage │ └── lcov.info ├── lib │ ├── domain │ │ └── usecases │ │ │ ├── search_movies.dart │ │ │ └── search_tvs.dart │ ├── presentation │ │ ├── bloc │ │ │ ├── search_bloc.dart │ │ │ ├── search_event.dart │ │ │ └── search_state.dart │ │ └── pages │ │ │ ├── movie_search_page.dart │ │ │ └── tv_search_page.dart │ └── search.dart ├── pubspec.yaml └── test │ ├── domain │ └── usecases │ │ ├── search_movie_test.dart │ │ └── search_tv_test.dart │ ├── helpers │ ├── test_helper.dart │ └── test_helper.mocks.dart │ └── presentation │ └── bloc │ ├── search_bloc_test.dart │ └── search_bloc_test.mocks.dart └── tv ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── data │ ├── datasources │ │ ├── db │ │ │ └── tv_database_helper.dart │ │ ├── tv_local_data_source.dart │ │ └── tv_remote_data_source.dart │ ├── models │ │ ├── genre_model.dart │ │ ├── media_image_model.dart │ │ ├── tv_detail_response.dart │ │ ├── tv_model.dart │ │ ├── tv_response.dart │ │ ├── tv_season_episode_model.dart │ │ ├── tv_season_episode_response.dart │ │ └── tv_table.dart │ └── repositories │ │ └── tv_repository_impl.dart ├── domain │ ├── entities │ │ ├── genre.dart │ │ ├── media_image.dart │ │ ├── tv.dart │ │ ├── tv_detail.dart │ │ └── tv_season_episode.dart │ ├── repositories │ │ └── tv_repository.dart │ └── usecases │ │ ├── get_on_the_air_tvs.dart │ │ ├── get_popular_tvs.dart │ │ ├── get_top_rated_tvs.dart │ │ ├── get_tv_detail.dart │ │ ├── get_tv_images.dart │ │ ├── get_tv_recommendations.dart │ │ ├── get_tv_season_episodes.dart │ │ ├── get_tv_watchlist_status.dart │ │ ├── get_watchlist_tvs.dart │ │ ├── remove_watchlist_tv.dart │ │ └── save_watchlist_tv.dart ├── presentation │ ├── pages │ │ ├── main_tv_page.dart │ │ ├── popular_tvs_page.dart │ │ ├── top_rated_tvs_page.dart │ │ ├── tv_detail_page.dart │ │ └── tv_watchlist_page.dart │ ├── provider │ │ ├── popular_tvs_notifier.dart │ │ ├── top_rated_tvs_notifier.dart │ │ ├── tv_detail_notifier.dart │ │ ├── tv_images_notifier.dart │ │ ├── tv_list_notifier.dart │ │ ├── tv_season_episodes_notifier.dart │ │ └── watchlist_tv_provider.dart │ └── widgets │ │ ├── horizontal_item_list.dart │ │ ├── item_card_list.dart │ │ ├── minimal_detail.dart │ │ └── sub_heading.dart └── tv.dart ├── pubspec.yaml └── test ├── data ├── datasources │ ├── tv_local_data_source_test.dart │ └── tv_remote_data_source_test.dart ├── models │ ├── tv_detail_response_test.dart │ ├── tv_model_test.dart │ ├── tv_response_model_test.dart │ ├── tv_season_episode_model_test.dart │ └── tv_season_episodes_model_test.dart └── repositories │ └── tv_repository_impl_test.dart ├── domain └── usecases │ ├── get_on_the_air_tvs_test.dart │ ├── get_popular_tvs_test.dart │ ├── get_top_rated_tvs_test.dart │ ├── get_tv_detail_test.dart │ ├── get_tv_episodes_test.dart │ ├── get_tv_images_test.dart │ ├── get_tv_recommendations_test.dart │ ├── get_tv_watchlist_status_test.dart │ ├── get_watchlist_tvs_test.dart │ ├── remove_watchlist_tv_test.dart │ └── save_watchlist_tv_test.dart ├── helpers ├── dummy_objects.dart ├── dummy_responses │ ├── images.json │ ├── search_tv.json │ ├── tv.json │ ├── tv_detail.json │ ├── tv_recommendations.json │ └── tv_season.json ├── json_reader.dart ├── test_helper.dart └── test_helper.mocks.dart └── presentation ├── pages ├── popular_tvs_page_test.dart ├── popular_tvs_page_test.mocks.dart ├── top_rated_tvs_page_test.dart ├── top_rated_tvs_page_test.mocks.dart ├── tv_detail_page_test.dart └── tv_detail_page_test.mocks.dart └── provider ├── popular_tvs_notifier_test.dart ├── popular_tvs_notifier_test.mocks.dart ├── top_rated_tvs_notifier_test.dart ├── top_rated_tvs_notifier_test.mocks.dart ├── tv_detail_notifier_test.dart ├── tv_detail_notifier_test.mocks.dart ├── tv_images_notifier_test.dart ├── tv_images_notifier_test.mocks.dart ├── tv_list_notifier_test.dart ├── tv_list_notifier_test.mocks.dart ├── tv_season_episodes_notifier_test.dart ├── tv_season_episodes_notifier_test.mocks.dart ├── watchlist_tv_notifier_test.dart └── watchlist_tv_notifier_test.mocks.dart /.github/workflows/flutter-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | # This workflow is triggered on pushes or pull requests to the repository 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | jobs: 18 | build: 19 | # This job will run on ubuntu virtual machine 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | # Setup the flutter environtment 26 | - name: Install and set Flutter version 27 | uses: subosito/flutter-action@v2 28 | with: 29 | channel: 'stable' 30 | 31 | # Get flutter dependencies 32 | - name: Restore packages 33 | run: flutter pub get 34 | 35 | # Check for any formatting issue in the code 36 | - name: Check formatting issue 37 | run: flutter format --set-exit-if-changed . 38 | 39 | # Statically analyze the Dart code for any errors. 40 | - name: Analyze 41 | run: flutter analyze . 42 | 43 | # Run widget tests on movie module. 44 | - name: Run tests on movie module 45 | working-directory: ./movie 46 | run: flutter test --coverage 47 | 48 | # Run widget tests on tv module. 49 | - name: Run tests on tv module 50 | working-directory: ./tv 51 | run: flutter test --coverage 52 | 53 | # Run widget tests on search module. 54 | - name: Run tests on search module 55 | working-directory: ./search 56 | run: flutter test --coverage 57 | 58 | # Upload coverage to codecov 59 | - name: Upload coverage to codecov 60 | uses: codecov/codecov-action@v2.1.0 61 | with: 62 | token: ${{secrets.CODECOV_TOKEN}} 63 | file: ./coverage/lcov.info 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 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: b22742018b3edf16c6cadd7b76d9db5e7f9064b5 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "flutter-movie-database-app", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "flutter-movie-database-app (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "about", 20 | "cwd": "about", 21 | "request": "launch", 22 | "type": "dart" 23 | }, 24 | { 25 | "name": "about (profile mode)", 26 | "cwd": "about", 27 | "request": "launch", 28 | "type": "dart", 29 | "flutterMode": "profile" 30 | }, 31 | { 32 | "name": "core", 33 | "cwd": "core", 34 | "request": "launch", 35 | "type": "dart" 36 | }, 37 | { 38 | "name": "core (profile mode)", 39 | "cwd": "core", 40 | "request": "launch", 41 | "type": "dart", 42 | "flutterMode": "profile" 43 | }, 44 | { 45 | "name": "search", 46 | "cwd": "search", 47 | "request": "launch", 48 | "type": "dart" 49 | }, 50 | { 51 | "name": "search (profile mode)", 52 | "cwd": "search", 53 | "request": "launch", 54 | "type": "dart", 55 | "flutterMode": "profile" 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /about/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /about/.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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /about/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /about/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /about/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /about/lib/about.dart: -------------------------------------------------------------------------------- 1 | library about; 2 | 3 | export 'about_page.dart'; 4 | -------------------------------------------------------------------------------- /about/lib/about_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/core.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AboutPage extends StatelessWidget { 5 | static const routeName = '/about'; 6 | 7 | const AboutPage({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: const Text('About'), 14 | ), 15 | body: Stack( 16 | children: [ 17 | Column( 18 | children: [ 19 | Expanded( 20 | child: Container( 21 | color: kRichBlack, 22 | child: const Center( 23 | child: Text( 24 | 'MDB', 25 | style: TextStyle( 26 | fontSize: 64.0, 27 | fontWeight: FontWeight.bold, 28 | color: Colors.redAccent, 29 | ), 30 | ), 31 | ), 32 | ), 33 | ), 34 | Expanded( 35 | child: Container( 36 | padding: const EdgeInsets.all(32.0), 37 | color: kRichBlack, 38 | child: const Text( 39 | 'Movie Database (MDB) is a movie and tv series catalog app developed by Aditya Rohman sebagai as a project submission for Flutter Developer Expert course on Dicoding Indonesia.', 40 | style: TextStyle(fontSize: 16.0), 41 | textAlign: TextAlign.justify, 42 | ), 43 | ), 44 | ), 45 | ], 46 | ), 47 | ], 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /about/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: about 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | environment: 7 | sdk: ">=2.15.1 <3.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | core: 16 | path: ../core 17 | flutter_lints: ^1.0.4 18 | flutter_test: 19 | sdk: flutter 20 | 21 | flutter: 22 | 23 | # To add assets to your package, add an assets section, like this: 24 | # assets: 25 | # - images/a_dot_burr.jpeg 26 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | avoid_print: false 26 | prefer_relative_imports: true 27 | directives_ordering: true 28 | prefer_single_quotes: true 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.dicoding.ditonton" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/dicoding/ditonton/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dicoding.ditonton 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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /art/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/art/demo.gif -------------------------------------------------------------------------------- /assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/assets/user.png -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /core/.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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /core/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /core/lib/core.dart: -------------------------------------------------------------------------------- 1 | library core; 2 | 3 | export 'styles/colors.dart'; 4 | export 'styles/text_styles.dart'; 5 | export 'utils/exception.dart'; 6 | export 'utils/failure.dart'; 7 | export 'utils/state_enum.dart'; 8 | export 'utils/urls.dart'; 9 | export 'utils/utils.dart'; 10 | -------------------------------------------------------------------------------- /core/lib/presentation/provider/home_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../core.dart'; 3 | 4 | class HomeNotifier extends ChangeNotifier { 5 | GeneralContentType _state = GeneralContentType.movie; 6 | 7 | GeneralContentType get state => _state; 8 | 9 | void setState(GeneralContentType newState) { 10 | _state = newState; 11 | notifyListeners(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/lib/styles/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const Color kRichBlack = Color(0xFF1E1E29); 4 | const Color kSpaceGrey = Color(0xFF26262F); 5 | const Color kOxfordBlue = Color(0xFF001D3D); 6 | 7 | const kColorScheme = ColorScheme( 8 | primary: Colors.redAccent, 9 | primaryContainer: Colors.redAccent, 10 | secondary: kSpaceGrey, 11 | secondaryContainer: kSpaceGrey, 12 | surface: kRichBlack, 13 | background: kRichBlack, 14 | error: Colors.redAccent, 15 | onPrimary: kRichBlack, 16 | onSecondary: Colors.white, 17 | onSurface: Colors.white, 18 | onBackground: Colors.white, 19 | onError: Colors.white, 20 | brightness: Brightness.dark, 21 | ); 22 | -------------------------------------------------------------------------------- /core/lib/styles/text_styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | final TextStyle kHeading5 = 5 | GoogleFonts.poppins(fontSize: 23, fontWeight: FontWeight.w400); 6 | final TextStyle kHeading6 = GoogleFonts.poppins( 7 | fontSize: 19, fontWeight: FontWeight.w500, letterSpacing: 0.15); 8 | final TextStyle kSubtitle = GoogleFonts.poppins( 9 | fontSize: 15, fontWeight: FontWeight.w400, letterSpacing: 0.15); 10 | final TextStyle kBodyText = GoogleFonts.poppins( 11 | fontSize: 13, fontWeight: FontWeight.w400, letterSpacing: 0.25); 12 | 13 | final kTextTheme = TextTheme( 14 | headline5: kHeading5, 15 | headline6: kHeading6, 16 | subtitle1: kSubtitle, 17 | bodyText2: kBodyText, 18 | ); 19 | -------------------------------------------------------------------------------- /core/lib/utils/exception.dart: -------------------------------------------------------------------------------- 1 | class ServerException implements Exception {} 2 | 3 | class DatabaseException implements Exception { 4 | final String message; 5 | DatabaseException(this.message); 6 | } 7 | -------------------------------------------------------------------------------- /core/lib/utils/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class Failure extends Equatable { 4 | final String message; 5 | const Failure(this.message); 6 | 7 | @override 8 | List get props => [message]; 9 | } 10 | 11 | class ServerFailure extends Failure { 12 | const ServerFailure(String message) : super(message); 13 | } 14 | 15 | class ConnectionFailure extends Failure { 16 | const ConnectionFailure(String message) : super(message); 17 | } 18 | 19 | class DatabaseFailure extends Failure { 20 | const DatabaseFailure(String message) : super(message); 21 | } 22 | -------------------------------------------------------------------------------- /core/lib/utils/routes.dart: -------------------------------------------------------------------------------- 1 | const homeRoute = '/home'; 2 | const popularMoviesRoute = '/popular-movies'; 3 | const topRatedMoviesRoute = '/top-rated-movies'; 4 | const movieDetailRoute = '/movie-detail'; 5 | 6 | const popularTvsRoute = '/popular-tvs'; 7 | const topRatedTvsRoute = '/top-rated-tvs'; 8 | const tvDetailRoute = '/tv-detail'; 9 | 10 | const movieSearchRoute = '/movie-search'; 11 | const tvSearchRoute = '/tv-search'; 12 | const watchlistRoute = '/watchlist'; 13 | const aboutRoute = '/about'; 14 | -------------------------------------------------------------------------------- /core/lib/utils/state_enum.dart: -------------------------------------------------------------------------------- 1 | enum RequestState { empty, loading, loaded, error } 2 | enum GeneralContentType { movie, tv } 3 | -------------------------------------------------------------------------------- /core/lib/utils/urls.dart: -------------------------------------------------------------------------------- 1 | class Urls { 2 | static const String baseUrl = 'https://api.themoviedb.org/3'; 3 | static const String apiKey = 'api_key=2174d146bb9c0eab47529b2e77d6b526'; 4 | 5 | /// Movies 6 | static const String nowPlayingMovies = '$baseUrl/movie/now_playing?$apiKey'; 7 | static const String popularMovies = '$baseUrl/movie/popular?$apiKey'; 8 | static const String topRatedMovies = '$baseUrl/movie/top_rated?$apiKey'; 9 | static String movieDetail(int id) => '$baseUrl/movie/$id?$apiKey'; 10 | static String movieRecommendations(int id) => 11 | '$baseUrl/movie/$id/recommendations?$apiKey'; 12 | static String searchMovies(String query) => 13 | '$baseUrl/search/movie?$apiKey&query=$query'; 14 | 15 | /// Tvs 16 | static const String onTheAirTvs = '$baseUrl/tv/on_the_air?$apiKey'; 17 | static const String popularTvs = '$baseUrl/tv/popular?$apiKey'; 18 | static const String topRatedTvs = '$baseUrl/tv/top_rated?$apiKey'; 19 | static String tvDetail(int id) => '$baseUrl/tv/$id?$apiKey'; 20 | static String tvSeasons(int id, int seasonNumber) => 21 | '$baseUrl/tv/$id/season/$seasonNumber?$apiKey'; 22 | static String tvRecommendations(int id) => 23 | '$baseUrl/tv/$id/recommendations?$apiKey'; 24 | static String searchTvs(String query) => 25 | '$baseUrl/search/tv?$apiKey&query=$query'; 26 | 27 | /// Image 28 | static const String baseImageUrl = 'https://image.tmdb.org/t/p/w500'; 29 | static String imageUrl(String path) => '$baseImageUrl$path'; 30 | static String movieImages(int id) => 31 | '$baseUrl/movie/$id/images?$apiKey&language=en-US&include_image_language=en,null'; 32 | static String tvImages(int id) => 33 | '$baseUrl/tv/$id/images?$apiKey&language=en-US&include_image_language=en,null'; 34 | } 35 | -------------------------------------------------------------------------------- /core/lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | final RouteObserver routeObserver = RouteObserver(); 4 | -------------------------------------------------------------------------------- /core/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: core 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | publish_to: none 7 | 8 | environment: 9 | sdk: ">=2.15.1 <3.0.0" 10 | flutter: ">=1.17.0" 11 | 12 | dependencies: 13 | animate_do: ^2.1.0 14 | cached_network_image: ^3.2.0 15 | carousel_slider: ^4.0.0 16 | dartz: ^0.10.1 17 | equatable: ^2.0.3 18 | flutter: 19 | sdk: flutter 20 | google_fonts: ^2.2.0 21 | intl: ^0.17.0 22 | movie: 23 | path: ../movie 24 | provider: ^6.0.2 25 | shimmer: ^2.0.0 26 | sqflite: ^2.0.1 27 | tv: 28 | path: ../tv 29 | 30 | dev_dependencies: 31 | bloc_test: ^9.0.2 32 | build_runner: ^2.1.7 33 | flutter_lints: ^1.0.4 34 | flutter_test: 35 | sdk: flutter 36 | mockito: ^5.0.17 37 | 38 | flutter: 39 | # To add assets to your package, add an assets section, like this: 40 | # assets: 41 | # - images/a_dot_burr.jpeg 42 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /integration_test/robots/movie_detail_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class MovieDetailRobot { 7 | final WidgetTester tester; 8 | MovieDetailRobot(this.tester); 9 | 10 | Future clickMovieToWatchlistButton() async { 11 | final movieToWatchlistButtonFinder = 12 | find.byKey(const Key('movieToWatchlist')); 13 | 14 | await tester.ensureVisible(movieToWatchlistButtonFinder); 15 | await tester.tap(movieToWatchlistButtonFinder); 16 | 17 | await tester.pumpAndSettle(); 18 | } 19 | 20 | Future scrollThePage({bool scrollUp = false}) async { 21 | final scrollViewFinder = find.byKey(const Key('movieDetailScrollView')); 22 | 23 | if (scrollUp) { 24 | await tester.fling(scrollViewFinder, const Offset(0, 500), 10000); 25 | await tester.pumpAndSettle(); 26 | } else { 27 | await tester.fling(scrollViewFinder, const Offset(0, -500), 10000); 28 | await tester.pumpAndSettle(); 29 | } 30 | } 31 | 32 | Future goBack() async { 33 | await tester.pageBack(); 34 | await tester.pumpAndSettle(); 35 | sleep(const Duration(seconds: 2)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /integration_test/robots/popular_movies_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class PopularMoviesRobot { 7 | final WidgetTester tester; 8 | PopularMoviesRobot(this.tester); 9 | 10 | Future scrollThePage({bool scrollUp = false}) async { 11 | final listViewFinder = find.byKey(const Key('popularMoviesListView')); 12 | 13 | if (scrollUp) { 14 | await tester.fling(listViewFinder, const Offset(0, 500), 10000); 15 | await tester.pumpAndSettle(); 16 | } else { 17 | await tester.fling(listViewFinder, const Offset(0, -500), 10000); 18 | await tester.pumpAndSettle(); 19 | } 20 | } 21 | 22 | Future goBack() async { 23 | await tester.pageBack(); 24 | await tester.pumpAndSettle(); 25 | sleep(const Duration(seconds: 2)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration_test/robots/popular_tvs_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class PopularTvsRobot { 7 | final WidgetTester tester; 8 | PopularTvsRobot(this.tester); 9 | 10 | Future scrollThePage({bool scrollUp = false}) async { 11 | final listViewFinder = find.byKey(const Key('popularTvsListView')); 12 | 13 | if (scrollUp) { 14 | await tester.fling(listViewFinder, const Offset(0, 500), 10000); 15 | await tester.pumpAndSettle(); 16 | } else { 17 | await tester.fling(listViewFinder, const Offset(0, -500), 10000); 18 | await tester.pumpAndSettle(); 19 | } 20 | } 21 | 22 | Future goBack() async { 23 | await tester.pageBack(); 24 | await tester.pumpAndSettle(); 25 | sleep(const Duration(seconds: 2)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration_test/robots/search_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class SearchRobot { 7 | final WidgetTester tester; 8 | SearchRobot(this.tester); 9 | 10 | Future openFilterDialog() async { 11 | final filterButtonFinder = find.byKey(const Key('openFilterDialog')); 12 | 13 | await tester.ensureVisible(filterButtonFinder); 14 | await tester.tap(filterButtonFinder); 15 | 16 | await tester.pumpAndSettle(); 17 | } 18 | 19 | Future selectFilterMovie() async { 20 | final filterMovieRadioButtonFinder = find.byKey(const Key('filterByMovie')); 21 | 22 | await tester.ensureVisible(filterMovieRadioButtonFinder); 23 | await tester.tap(filterMovieRadioButtonFinder); 24 | 25 | await tester.pumpAndSettle(); 26 | } 27 | 28 | Future selectFilterTv() async { 29 | final filterTvRadioButtonFinder = find.byKey(const Key('filterByTv')); 30 | 31 | await tester.ensureVisible(filterTvRadioButtonFinder); 32 | await tester.tap(filterTvRadioButtonFinder); 33 | 34 | await tester.pumpAndSettle(); 35 | } 36 | 37 | Future enterSearchQuery(String query) async { 38 | final textFieldFinder = find.byKey(const Key('enterSearchQuery')); 39 | 40 | await tester.ensureVisible(textFieldFinder); 41 | await tester.enterText(textFieldFinder, query); 42 | await tester.testTextInput.receiveAction(TextInputAction.done); 43 | 44 | await tester.pumpAndSettle(); 45 | } 46 | 47 | Future goBack() async { 48 | await tester.pageBack(); 49 | await tester.pumpAndSettle(); 50 | sleep(const Duration(seconds: 2)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /integration_test/robots/top_rated_movies_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class TopRatedMoviesRobot { 7 | final WidgetTester tester; 8 | TopRatedMoviesRobot(this.tester); 9 | 10 | Future scrollThePage({bool scrollUp = false}) async { 11 | final listViewFinder = find.byKey(const Key('topRatedMoviesListView')); 12 | 13 | if (scrollUp) { 14 | await tester.fling(listViewFinder, const Offset(0, 500), 10000); 15 | await tester.pumpAndSettle(); 16 | } else { 17 | await tester.fling(listViewFinder, const Offset(0, -500), 10000); 18 | await tester.pumpAndSettle(); 19 | } 20 | } 21 | 22 | Future goBack() async { 23 | await tester.pageBack(); 24 | await tester.pumpAndSettle(); 25 | sleep(const Duration(seconds: 2)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration_test/robots/top_rated_tvs_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class TopRatedTvsRobot { 7 | final WidgetTester tester; 8 | TopRatedTvsRobot(this.tester); 9 | 10 | Future scrollThePage({bool scrollUp = false}) async { 11 | final listViewFinder = find.byKey(const Key('topRatedTvsListView')); 12 | 13 | if (scrollUp) { 14 | await tester.fling(listViewFinder, const Offset(0, 500), 10000); 15 | await tester.pumpAndSettle(); 16 | } else { 17 | await tester.fling(listViewFinder, const Offset(0, -500), 10000); 18 | await tester.pumpAndSettle(); 19 | } 20 | } 21 | 22 | Future goBack() async { 23 | await tester.pageBack(); 24 | await tester.pumpAndSettle(); 25 | sleep(const Duration(seconds: 2)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration_test/robots/tv_detail_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class TvDetailRobot { 7 | final WidgetTester tester; 8 | TvDetailRobot(this.tester); 9 | 10 | Future clickTvToWatchlistButton() async { 11 | final tvToWatchlistButtonFinder = find.byKey(const Key('tvToWatchlist')); 12 | 13 | await tester.ensureVisible(tvToWatchlistButtonFinder); 14 | await tester.tap(tvToWatchlistButtonFinder); 15 | 16 | await tester.pumpAndSettle(); 17 | } 18 | 19 | Future scrollThePage({bool scrollUp = false}) async { 20 | final scrollViewFinder = find.byKey(const Key('tvDetailScrollView')); 21 | 22 | if (scrollUp) { 23 | await tester.fling(scrollViewFinder, const Offset(0, 500), 10000); 24 | await tester.pumpAndSettle(); 25 | } else { 26 | await tester.fling(scrollViewFinder, const Offset(0, -500), 10000); 27 | await tester.pumpAndSettle(); 28 | } 29 | } 30 | 31 | Future goBack() async { 32 | await tester.pageBack(); 33 | await tester.pumpAndSettle(); 34 | sleep(const Duration(seconds: 2)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /integration_test/robots/watchlist_robot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | class WatchlistRobot { 7 | final WidgetTester tester; 8 | WatchlistRobot(this.tester); 9 | 10 | Future clickMovieWatchlistTab() async { 11 | final movieWatchlistTabFinder = find.byKey(const Key('movieWatchlistTab')); 12 | 13 | await tester.ensureVisible(movieWatchlistTabFinder); 14 | await tester.tap(movieWatchlistTabFinder); 15 | 16 | await tester.pumpAndSettle(); 17 | 18 | final movieWatchlist = find.byKey(const Key('movieWatchlist')); 19 | 20 | expect(movieWatchlist, equals(findsOneWidget)); 21 | } 22 | 23 | Future clickTvWatchlistTab() async { 24 | final tvWatchlistTabFinder = find.byKey(const Key('tvWatchlistTab')); 25 | 26 | await tester.ensureVisible(tvWatchlistTabFinder); 27 | await tester.tap(tvWatchlistTabFinder); 28 | 29 | await tester.pumpAndSettle(); 30 | 31 | final tvWatchlist = find.byKey(const Key('tvWatchlist')); 32 | 33 | expect(tvWatchlist, equals(findsOneWidget)); 34 | } 35 | 36 | Future goBack() async { 37 | await tester.pageBack(); 38 | await tester.pumpAndSettle(); 39 | sleep(const Duration(seconds: 2)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /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 | 9.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, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - FMDB (2.7.5): 4 | - FMDB/standard (= 2.7.5) 5 | - FMDB/standard (2.7.5) 6 | - integration_test (0.0.1): 7 | - Flutter 8 | - path_provider_ios (0.0.1): 9 | - Flutter 10 | - sqflite (0.0.2): 11 | - Flutter 12 | - FMDB (>= 2.7.5) 13 | 14 | DEPENDENCIES: 15 | - Flutter (from `Flutter`) 16 | - integration_test (from `.symlinks/plugins/integration_test/ios`) 17 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 18 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 19 | 20 | SPEC REPOS: 21 | trunk: 22 | - FMDB 23 | 24 | EXTERNAL SOURCES: 25 | Flutter: 26 | :path: Flutter 27 | integration_test: 28 | :path: ".symlinks/plugins/integration_test/ios" 29 | path_provider_ios: 30 | :path: ".symlinks/plugins/path_provider_ios/ios" 31 | sqflite: 32 | :path: ".symlinks/plugins/sqflite/ios" 33 | 34 | SPEC CHECKSUMS: 35 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 36 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 37 | integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 38 | path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 39 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 40 | 41 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 42 | 43 | COCOAPODS: 1.11.2 44 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codestronautHub/flutter-movie-database-app/9fd42b87e3ccc50e334997f2715d30bfc605c76a/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/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 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ditonton 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /movie/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /movie/.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: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /movie/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /movie/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /movie/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /movie/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | avoid_print: false 26 | prefer_relative_imports: true 27 | directives_ordering: true 28 | prefer_single_quotes: true 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /movie/lib/data/datasources/movie_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/exception.dart'; 2 | 3 | import '../models/movie_table.dart'; 4 | import 'db/movie_database_helper.dart'; 5 | 6 | abstract class MovieLocalDataSource { 7 | Future insertWatchlist(MovieTable movie); 8 | Future removeWatchlist(MovieTable movie); 9 | Future getMovieById(int id); 10 | Future> getWatchlistMovies(); 11 | } 12 | 13 | class MovieLocalDataSourceImpl implements MovieLocalDataSource { 14 | final MovieDatabaseHelper databaseHelper; 15 | 16 | MovieLocalDataSourceImpl({required this.databaseHelper}); 17 | 18 | @override 19 | Future insertWatchlist(MovieTable movie) async { 20 | try { 21 | await databaseHelper.insertMovieWatchlist(movie); 22 | return 'Added to watchlist'; 23 | } catch (e) { 24 | throw DatabaseException(e.toString()); 25 | } 26 | } 27 | 28 | @override 29 | Future removeWatchlist(MovieTable movie) async { 30 | try { 31 | await databaseHelper.removeMovieWatchlist(movie); 32 | return 'Removed from watchlist'; 33 | } catch (e) { 34 | throw DatabaseException(e.toString()); 35 | } 36 | } 37 | 38 | @override 39 | Future getMovieById(int id) async { 40 | final result = await databaseHelper.getMovieById(id); 41 | if (result != null) { 42 | return MovieTable.fromMap(result); 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | @override 49 | Future> getWatchlistMovies() async { 50 | final result = await databaseHelper.getWatchlistMovies(); 51 | return result.map((data) => MovieTable.fromMap(data)).toList(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /movie/lib/data/models/genre_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/genre.dart'; 4 | 5 | class GenreModel extends Equatable { 6 | final int id; 7 | final String name; 8 | 9 | const GenreModel({ 10 | required this.id, 11 | required this.name, 12 | }); 13 | 14 | factory GenreModel.fromJson(Map json) => GenreModel( 15 | id: json['id'], 16 | name: json['name'], 17 | ); 18 | 19 | Map toJson() => { 20 | 'id': id, 21 | 'name': name, 22 | }; 23 | 24 | Genre toEntity() => Genre( 25 | id: id, 26 | name: name, 27 | ); 28 | 29 | @override 30 | List get props => [id, name]; 31 | } 32 | -------------------------------------------------------------------------------- /movie/lib/data/models/media_image_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/media_image.dart'; 4 | 5 | class MediaImageModel extends Equatable { 6 | final int id; 7 | final List backdropPaths; 8 | final List logoPaths; 9 | final List posterPaths; 10 | 11 | const MediaImageModel({ 12 | required this.id, 13 | required this.backdropPaths, 14 | required this.logoPaths, 15 | required this.posterPaths, 16 | }); 17 | 18 | factory MediaImageModel.fromJson(Map json) => 19 | MediaImageModel( 20 | id: json['id'], 21 | backdropPaths: List.from( 22 | json['backdrops'].map((x) => x['file_path']), 23 | ), 24 | logoPaths: List.from( 25 | json['logos'].map((x) => x['file_path']), 26 | ), 27 | posterPaths: List.from( 28 | json['posters'].map((x) => x['file_path']), 29 | ), 30 | ); 31 | 32 | MediaImage toEntity() => MediaImage( 33 | id: id, 34 | backdropPaths: backdropPaths, 35 | logoPaths: logoPaths, 36 | posterPaths: posterPaths, 37 | ); 38 | 39 | @override 40 | List get props => [ 41 | id, 42 | backdropPaths, 43 | logoPaths, 44 | posterPaths, 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /movie/lib/data/models/movie_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/movie.dart'; 4 | 5 | class MovieModel extends Equatable { 6 | final String? backdropPath; 7 | final List genreIds; 8 | final int id; 9 | final String overview; 10 | final String? posterPath; 11 | final String releaseDate; 12 | final String title; 13 | final double voteAverage; 14 | final int voteCount; 15 | 16 | const MovieModel({ 17 | required this.backdropPath, 18 | required this.genreIds, 19 | required this.id, 20 | required this.overview, 21 | required this.posterPath, 22 | required this.releaseDate, 23 | required this.title, 24 | required this.voteAverage, 25 | required this.voteCount, 26 | }); 27 | 28 | factory MovieModel.fromJson(Map json) => MovieModel( 29 | backdropPath: json['backdrop_path'], 30 | genreIds: List.from(json['genre_ids'].map((x) => x)), 31 | id: json['id'], 32 | overview: json['overview'], 33 | posterPath: json['poster_path'], 34 | releaseDate: json['release_date'], 35 | title: json['title'], 36 | voteAverage: json['vote_average'].toDouble(), 37 | voteCount: json['vote_count'], 38 | ); 39 | 40 | Map toJson() => { 41 | 'backdrop_path': backdropPath, 42 | 'genre_ids': List.from(genreIds.map((x) => x)), 43 | 'id': id, 44 | 'overview': overview, 45 | 'poster_path': posterPath, 46 | 'release_date': releaseDate, 47 | 'title': title, 48 | 'vote_average': voteAverage, 49 | 'vote_count': voteCount, 50 | }; 51 | 52 | Movie toEntity() => Movie( 53 | backdropPath: backdropPath, 54 | genreIds: genreIds, 55 | id: id, 56 | overview: overview, 57 | posterPath: posterPath, 58 | releaseDate: releaseDate, 59 | title: title, 60 | voteAverage: voteAverage, 61 | voteCount: voteCount, 62 | ); 63 | 64 | @override 65 | List get props => [ 66 | backdropPath, 67 | genreIds, 68 | id, 69 | overview, 70 | posterPath, 71 | releaseDate, 72 | title, 73 | voteAverage, 74 | voteCount, 75 | ]; 76 | } 77 | -------------------------------------------------------------------------------- /movie/lib/data/models/movie_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'movie_model.dart'; 4 | 5 | class MovieResponse extends Equatable { 6 | final List movieList; 7 | 8 | const MovieResponse({required this.movieList}); 9 | 10 | factory MovieResponse.fromJson(Map json) => MovieResponse( 11 | movieList: List.from((json['results'] as List) 12 | .map((x) => MovieModel.fromJson(x)) 13 | .where((element) => 14 | element.posterPath != null && element.backdropPath != null)), 15 | ); 16 | 17 | Map toJson() => { 18 | 'results': List.from(movieList.map((x) => x.toJson())), 19 | }; 20 | 21 | @override 22 | List get props => [movieList]; 23 | } 24 | -------------------------------------------------------------------------------- /movie/lib/data/models/movie_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/movie.dart'; 4 | import '../../domain/entities/movie_detail.dart'; 5 | 6 | class MovieTable extends Equatable { 7 | final String? releaseDate; 8 | final int id; 9 | final String? title; 10 | final String? posterPath; 11 | final String? overview; 12 | final double? voteAverage; 13 | 14 | const MovieTable({ 15 | required this.releaseDate, 16 | required this.id, 17 | required this.title, 18 | required this.posterPath, 19 | required this.overview, 20 | required this.voteAverage, 21 | }); 22 | 23 | factory MovieTable.fromMap(Map map) => MovieTable( 24 | releaseDate: map['releaseDate'], 25 | id: map['id'], 26 | title: map['title'], 27 | posterPath: map['posterPath'], 28 | overview: map['overview'], 29 | voteAverage: map['voteAverage']); 30 | 31 | factory MovieTable.fromEntity(MovieDetail movie) => MovieTable( 32 | releaseDate: movie.releaseDate, 33 | id: movie.id, 34 | title: movie.title, 35 | posterPath: movie.posterPath, 36 | overview: movie.overview, 37 | voteAverage: movie.voteAverage, 38 | ); 39 | 40 | Map toMap() => { 41 | 'releaseDate': releaseDate, 42 | 'id': id, 43 | 'title': title, 44 | 'posterPath': posterPath, 45 | 'overview': overview, 46 | 'voteAverage': voteAverage, 47 | }; 48 | 49 | Movie toEntity() => Movie.watchlist( 50 | releaseDate: releaseDate, 51 | id: id, 52 | overview: overview, 53 | posterPath: posterPath, 54 | title: title, 55 | voteAverage: voteAverage, 56 | ); 57 | 58 | @override 59 | List get props => [ 60 | releaseDate, 61 | id, 62 | title, 63 | posterPath, 64 | overview, 65 | voteAverage, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /movie/lib/domain/entities/genre.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Genre extends Equatable { 4 | final int id; 5 | final String name; 6 | 7 | const Genre({ 8 | required this.id, 9 | required this.name, 10 | }); 11 | 12 | @override 13 | List get props => [id, name]; 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/entities/media_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class MediaImage extends Equatable { 4 | final int id; 5 | final List backdropPaths; 6 | final List logoPaths; 7 | final List posterPaths; 8 | 9 | const MediaImage({ 10 | required this.id, 11 | required this.backdropPaths, 12 | required this.logoPaths, 13 | required this.posterPaths, 14 | }); 15 | 16 | @override 17 | List get props => [id, backdropPaths, logoPaths, posterPaths]; 18 | } 19 | -------------------------------------------------------------------------------- /movie/lib/domain/entities/movie.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | // ignore: must_be_immutable 4 | class Movie extends Equatable { 5 | String? backdropPath; 6 | List? genreIds; 7 | int id; 8 | String? overview; 9 | String? posterPath; 10 | String? releaseDate; 11 | String? title; 12 | double? voteAverage; 13 | int? voteCount; 14 | 15 | Movie({ 16 | required this.backdropPath, 17 | required this.genreIds, 18 | required this.id, 19 | required this.overview, 20 | required this.posterPath, 21 | required this.releaseDate, 22 | required this.title, 23 | required this.voteAverage, 24 | required this.voteCount, 25 | }); 26 | 27 | Movie.watchlist({ 28 | required this.releaseDate, 29 | required this.id, 30 | required this.overview, 31 | required this.posterPath, 32 | required this.title, 33 | required this.voteAverage, 34 | }); 35 | 36 | @override 37 | List get props => [ 38 | backdropPath, 39 | genreIds, 40 | id, 41 | overview, 42 | posterPath, 43 | releaseDate, 44 | title, 45 | voteAverage, 46 | voteCount, 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /movie/lib/domain/entities/movie_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'genre.dart'; 3 | 4 | class MovieDetail extends Equatable { 5 | final String? backdropPath; 6 | final List genres; 7 | final int id; 8 | final String overview; 9 | final String? posterPath; 10 | final String releaseDate; 11 | final int runtime; 12 | final String title; 13 | final double voteAverage; 14 | final int voteCount; 15 | 16 | const MovieDetail({ 17 | required this.backdropPath, 18 | required this.genres, 19 | required this.id, 20 | required this.overview, 21 | required this.posterPath, 22 | required this.releaseDate, 23 | required this.runtime, 24 | required this.title, 25 | required this.voteAverage, 26 | required this.voteCount, 27 | }); 28 | 29 | @override 30 | List get props => [ 31 | backdropPath, 32 | genres, 33 | id, 34 | overview, 35 | posterPath, 36 | releaseDate, 37 | title, 38 | voteAverage, 39 | voteCount, 40 | ]; 41 | } 42 | -------------------------------------------------------------------------------- /movie/lib/domain/repositories/movie_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/media_image.dart'; 4 | import '../entities/movie.dart'; 5 | import '../entities/movie_detail.dart'; 6 | 7 | abstract class MovieRepository { 8 | Future>> getNowPlayingMovies(); 9 | Future>> getPopularMovies(); 10 | Future>> getTopRatedMovies(); 11 | Future> getMovieDetail(int id); 12 | Future>> getMovieRecommendations(int id); 13 | Future>> searchMovies(String query); 14 | Future> getMovieImages(int id); 15 | Future> saveWatchlist(MovieDetail movie); 16 | Future> removeWatchlist(MovieDetail movie); 17 | Future isAddedToWatchlist(int id); 18 | Future>> getWatchlistMovies(); 19 | } 20 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_movie_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie_detail.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetMovieDetail { 7 | final MovieRepository repository; 8 | 9 | GetMovieDetail(this.repository); 10 | 11 | Future> execute(int id) { 12 | return repository.getMovieDetail(id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_movie_images.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/media_image.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetMovieImages { 7 | final MovieRepository repository; 8 | 9 | GetMovieImages(this.repository); 10 | 11 | Future> execute(int id) { 12 | return repository.getMovieImages(id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_movie_recommendations.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetMovieRecommendations { 7 | final MovieRepository repository; 8 | 9 | GetMovieRecommendations(this.repository); 10 | 11 | Future>> execute(id) { 12 | return repository.getMovieRecommendations(id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_movie_watchlist_status.dart: -------------------------------------------------------------------------------- 1 | import '../repositories/movie_repository.dart'; 2 | 3 | class GetMovieWatchlistStatus { 4 | final MovieRepository movieRepository; 5 | 6 | GetMovieWatchlistStatus({ 7 | required this.movieRepository, 8 | }); 9 | 10 | Future execute(int id) async { 11 | return movieRepository.isAddedToWatchlist(id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_now_playing_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetNowPlayingMovies { 7 | final MovieRepository repository; 8 | 9 | GetNowPlayingMovies(this.repository); 10 | 11 | Future>> execute() { 12 | return repository.getNowPlayingMovies(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_popular_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetPopularMovies { 7 | final MovieRepository repository; 8 | 9 | GetPopularMovies(this.repository); 10 | 11 | Future>> execute() { 12 | return repository.getPopularMovies(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_top_rated_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetTopRatedMovies { 7 | final MovieRepository repository; 8 | 9 | GetTopRatedMovies(this.repository); 10 | 11 | Future>> execute() { 12 | return repository.getTopRatedMovies(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/get_watchlist_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class GetWatchlistMovies { 7 | final MovieRepository _repository; 8 | 9 | GetWatchlistMovies(this._repository); 10 | 11 | Future>> execute() { 12 | return _repository.getWatchlistMovies(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/remove_watchlist_movie.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie_detail.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class RemoveWatchlistMovie { 7 | final MovieRepository movieRepository; 8 | 9 | RemoveWatchlistMovie({required this.movieRepository}); 10 | 11 | Future> execute(MovieDetail movie) { 12 | return movieRepository.removeWatchlist(movie); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/domain/usecases/save_watchlist_movie.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/movie_detail.dart'; 4 | import '../repositories/movie_repository.dart'; 5 | 6 | class SaveWatchlistMovie { 7 | final MovieRepository movieRepository; 8 | 9 | SaveWatchlistMovie({required this.movieRepository}); 10 | 11 | Future> execute(MovieDetail movie) { 12 | return movieRepository.saveWatchlist(movie); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /movie/lib/movie.dart: -------------------------------------------------------------------------------- 1 | library movie; 2 | 3 | export 'presentation/pages/main_movie_page.dart'; 4 | export 'presentation/pages/movie_watchlist_page.dart'; 5 | export 'presentation/provider/watchlist_movie_notifier.dart'; 6 | -------------------------------------------------------------------------------- /movie/lib/presentation/pages/movie_watchlist_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import '../provider/watchlist_movie_notifier.dart'; 5 | import '../widgets/item_card_list.dart'; 6 | 7 | class MovieWatchlist extends StatelessWidget { 8 | const MovieWatchlist({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Consumer( 13 | builder: (context, data, child) { 14 | if (data.watchlistState == RequestState.loading) { 15 | return const Center(child: CircularProgressIndicator()); 16 | } else if (data.watchlistState == RequestState.loaded) { 17 | return ListView.builder( 18 | key: const Key('movieWatchlist'), 19 | itemCount: data.watchlistMovies.length, 20 | padding: const EdgeInsets.all(16.0), 21 | itemBuilder: (context, index) { 22 | final movie = data.watchlistMovies[index]; 23 | return ItemCard( 24 | movie: movie, 25 | ); 26 | }, 27 | ); 28 | } else { 29 | return Center( 30 | key: const Key('error_message'), 31 | child: Text(data.message), 32 | ); 33 | } 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /movie/lib/presentation/provider/movie_images_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/media_image.dart'; 5 | import '../../domain/usecases/get_movie_images.dart'; 6 | 7 | class MovieImagesNotifier extends ChangeNotifier { 8 | final GetMovieImages getMovieImages; 9 | 10 | MovieImagesNotifier({required this.getMovieImages}); 11 | 12 | late MediaImage _movieImages; 13 | MediaImage get movieImages => _movieImages; 14 | 15 | RequestState _movieImagesState = RequestState.empty; 16 | RequestState get movieImagesState => _movieImagesState; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchMovieImages(int id) async { 22 | _movieImagesState = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getMovieImages.execute(id); 26 | result.fold( 27 | (failure) { 28 | _movieImagesState = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (movieImages) { 33 | _movieImagesState = RequestState.loaded; 34 | _movieImages = movieImages; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /movie/lib/presentation/provider/popular_movies_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import '../../domain/entities/movie.dart'; 5 | import '../../domain/usecases/get_popular_movies.dart'; 6 | 7 | class PopularMoviesNotifier extends ChangeNotifier { 8 | final GetPopularMovies getPopularMovies; 9 | 10 | PopularMoviesNotifier(this.getPopularMovies); 11 | 12 | RequestState _state = RequestState.empty; 13 | RequestState get state => _state; 14 | 15 | List _movies = []; 16 | List get movies => _movies; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchPopularMovies() async { 22 | _state = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getPopularMovies.execute(); 26 | 27 | result.fold( 28 | (failure) { 29 | _message = failure.message; 30 | _state = RequestState.error; 31 | notifyListeners(); 32 | }, 33 | (moviesData) { 34 | _movies = moviesData; 35 | _state = RequestState.loaded; 36 | notifyListeners(); 37 | }, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /movie/lib/presentation/provider/top_rated_movies_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import '../../domain/entities/movie.dart'; 5 | import '../../domain/usecases/get_top_rated_movies.dart'; 6 | 7 | class TopRatedMoviesNotifier extends ChangeNotifier { 8 | final GetTopRatedMovies getTopRatedMovies; 9 | 10 | TopRatedMoviesNotifier({required this.getTopRatedMovies}); 11 | 12 | RequestState _state = RequestState.empty; 13 | RequestState get state => _state; 14 | 15 | List _movies = []; 16 | List get movies => _movies; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchTopRatedMovies() async { 22 | _state = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getTopRatedMovies.execute(); 26 | 27 | result.fold( 28 | (failure) { 29 | _message = failure.message; 30 | _state = RequestState.error; 31 | notifyListeners(); 32 | }, 33 | (moviesData) { 34 | _movies = moviesData; 35 | _state = RequestState.loaded; 36 | notifyListeners(); 37 | }, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /movie/lib/presentation/provider/watchlist_movie_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import '../../domain/entities/movie.dart'; 5 | import '../../domain/usecases/get_watchlist_movies.dart'; 6 | 7 | class WatchlistMovieNotifier extends ChangeNotifier { 8 | var _watchlistMovies = []; 9 | List get watchlistMovies => _watchlistMovies; 10 | 11 | var _watchlistState = RequestState.empty; 12 | RequestState get watchlistState => _watchlistState; 13 | 14 | String _message = ''; 15 | String get message => _message; 16 | 17 | WatchlistMovieNotifier({required this.getWatchlistMovies}); 18 | 19 | final GetWatchlistMovies getWatchlistMovies; 20 | 21 | Future fetchWatchlistMovies() async { 22 | _watchlistState = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getWatchlistMovies.execute(); 26 | result.fold( 27 | (failure) { 28 | _watchlistState = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (moviesData) { 33 | _watchlistState = RequestState.loaded; 34 | _watchlistMovies = moviesData; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /movie/lib/presentation/widgets/sub_heading.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/styles/text_styles.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SubHeading extends StatelessWidget { 5 | final String? valueKey; 6 | final String text; 7 | final Function() onSeeMoreTapped; 8 | const SubHeading({ 9 | Key? key, 10 | this.valueKey, 11 | required this.text, 12 | required this.onSeeMoreTapped, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | margin: const EdgeInsets.fromLTRB( 19 | 16.0, 20 | 24.0, 21 | 16.0, 22 | 8.0, 23 | ), 24 | child: Row( 25 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 26 | children: [ 27 | Text(text, style: kHeading6), 28 | InkWell( 29 | key: Key(valueKey!), 30 | onTap: onSeeMoreTapped, 31 | child: Padding( 32 | padding: const EdgeInsets.all(8.0), 33 | child: Row( 34 | children: const [ 35 | Text('See More'), 36 | Icon(Icons.arrow_forward_ios, size: 16.0) 37 | ], 38 | ), 39 | ), 40 | ), 41 | ], 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /movie/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: movie 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | publish_to: 'none' 7 | 8 | environment: 9 | sdk: ">=2.16.0 <3.0.0" 10 | flutter: ">=1.17.0" 11 | 12 | dependencies: 13 | animate_do: ^2.1.0 14 | cached_network_image: ^3.2.0 15 | carousel_slider: ^4.0.0 16 | core: 17 | path: ../core 18 | dartz: ^0.10.1 19 | equatable: ^2.0.3 20 | flutter: 21 | sdk: flutter 22 | google_fonts: ^2.2.0 23 | intl: ^0.17.0 24 | provider: ^6.0.2 25 | shimmer: ^2.0.0 26 | sqflite: ^2.0.1 27 | 28 | dev_dependencies: 29 | bloc_test: ^9.0.2 30 | build_runner: ^2.1.7 31 | flutter_lints: ^1.0.4 32 | flutter_test: 33 | sdk: flutter 34 | mockito: ^5.0.17 35 | 36 | flutter: 37 | 38 | # To add assets to your package, add an assets section, like this: 39 | # assets: 40 | # - images/a_dot_burr.jpeg 41 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /movie/test/data/models/movie_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:movie/data/models/movie_model.dart'; 3 | import 'package:movie/domain/entities/movie.dart'; 4 | 5 | void main() { 6 | const tMovieModel = MovieModel( 7 | backdropPath: '/path.jpg', 8 | genreIds: [1, 2, 3, 4], 9 | id: 1, 10 | overview: 'Overview', 11 | posterPath: '/path.jpg', 12 | releaseDate: '2022-01-01', 13 | title: 'Title', 14 | voteAverage: 1.0, 15 | voteCount: 1, 16 | ); 17 | 18 | final tMovie = Movie( 19 | backdropPath: '/path.jpg', 20 | genreIds: const [1, 2, 3, 4], 21 | id: 1, 22 | overview: 'Overview', 23 | posterPath: '/path.jpg', 24 | releaseDate: '2022-01-01', 25 | title: 'Title', 26 | voteAverage: 1.0, 27 | voteCount: 1, 28 | ); 29 | 30 | group('to entity', () { 31 | test( 32 | 'should be a subclass of movie entity', 33 | () async { 34 | // act 35 | final result = tMovieModel.toEntity(); 36 | 37 | // assert 38 | expect(result, equals(tMovie)); 39 | }, 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /movie/test/data/models/movie_response_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:movie/data/models/movie_model.dart'; 5 | import 'package:movie/data/models/movie_response.dart'; 6 | 7 | import '../../helpers/json_reader.dart'; 8 | 9 | void main() { 10 | const tMovieModel = MovieModel( 11 | backdropPath: '/path.jpg', 12 | genreIds: [1, 2, 3, 4], 13 | id: 1, 14 | overview: 'Overview', 15 | posterPath: '/path.jpg', 16 | releaseDate: '2022-01-01', 17 | title: 'Title', 18 | voteAverage: 1.0, 19 | voteCount: 1, 20 | ); 21 | 22 | const tMovieResponseModel = MovieResponse( 23 | movieList: [tMovieModel], 24 | ); 25 | 26 | group('from json', () { 27 | test( 28 | 'should return a valid model from json', 29 | () async { 30 | // arrange 31 | final Map jsonMap = json.decode( 32 | readJson('helpers/dummy_responses/now_playing_movie.json'), 33 | ); 34 | 35 | // act 36 | final result = MovieResponse.fromJson(jsonMap); 37 | 38 | // assert 39 | expect(result, equals(tMovieResponseModel)); 40 | }, 41 | ); 42 | }); 43 | 44 | group('to json', () { 45 | test( 46 | 'should return a json map containing proper data', 47 | () async { 48 | // act 49 | final result = tMovieResponseModel.toJson(); 50 | 51 | // assert 52 | final expectedJsonMap = { 53 | 'results': [ 54 | { 55 | 'backdrop_path': '/path.jpg', 56 | 'genre_ids': [1, 2, 3, 4], 57 | 'id': 1, 58 | 'overview': 'Overview', 59 | 'poster_path': '/path.jpg', 60 | 'release_date': '2022-01-01', 61 | 'title': 'Title', 62 | 'vote_average': 1.0, 63 | 'vote_count': 1 64 | } 65 | ], 66 | }; 67 | expect(result, equals(expectedJsonMap)); 68 | }, 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_movie_detail_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/usecases/get_movie_detail.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetMovieDetail usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetMovieDetail(mockMovieRepository); 16 | }); 17 | 18 | const tId = 1; 19 | 20 | test( 21 | 'should get movie detail from the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.getMovieDetail(tId)) 25 | .thenAnswer((_) async => const Right(testMovieDetail)); 26 | 27 | // act 28 | final result = await usecase.execute(tId); 29 | 30 | // assert 31 | expect(result, equals(const Right(testMovieDetail))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_movie_images_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/usecases/get_movie_images.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetMovieImages usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetMovieImages(mockMovieRepository); 16 | }); 17 | 18 | const tId = 1; 19 | 20 | test( 21 | 'should get movie images from the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.getMovieImages(tId)) 25 | .thenAnswer((_) async => const Right(testImages)); 26 | 27 | // act 28 | final result = await usecase.execute(tId); 29 | 30 | // assert 31 | expect(result, equals(const Right(testImages))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_movie_recommendations_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:movie/domain/usecases/get_movie_recommendations.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetMovieRecommendations usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetMovieRecommendations(mockMovieRepository); 16 | }); 17 | 18 | const tId = 1; 19 | final tMovies = []; 20 | 21 | test( 22 | 'should get list of movie recommendations from the repository', 23 | () async { 24 | // arrange 25 | when(mockMovieRepository.getMovieRecommendations(tId)) 26 | .thenAnswer((_) async => Right(tMovies)); 27 | 28 | // act 29 | final result = await usecase.execute(tId); 30 | 31 | // assert 32 | expect(result, equals(Right(tMovies))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_movie_watchlist_status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:movie/domain/usecases/get_movie_watchlist_status.dart'; 4 | 5 | import '../../helpers/test_helper.mocks.dart'; 6 | 7 | void main() { 8 | late MockMovieRepository mockMovieRepository; 9 | late GetMovieWatchlistStatus usecase; 10 | 11 | setUp(() { 12 | mockMovieRepository = MockMovieRepository(); 13 | usecase = GetMovieWatchlistStatus( 14 | movieRepository: mockMovieRepository, 15 | ); 16 | }); 17 | 18 | test( 19 | 'should get movie watchlist status from repository', 20 | () async { 21 | // arrange 22 | when(mockMovieRepository.isAddedToWatchlist(1)) 23 | .thenAnswer((_) async => true); 24 | 25 | // act 26 | final result = await usecase.execute(1); 27 | 28 | // assert 29 | expect(result, equals(true)); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_now_playing_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:movie/domain/usecases/get_now_playing_movies.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetNowPlayingMovies usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetNowPlayingMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | test( 21 | 'should get list of movie from the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.getNowPlayingMovies()) 25 | .thenAnswer((_) async => Right(tMovies)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tMovies))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_popular_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:movie/domain/usecases/get_popular_movies.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRpository; 11 | late GetPopularMovies usecase; 12 | 13 | setUp(() { 14 | mockMovieRpository = MockMovieRepository(); 15 | usecase = GetPopularMovies(mockMovieRpository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | test( 21 | 'should get list of movie from the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRpository.getPopularMovies()) 25 | .thenAnswer((_) async => Right(tMovies)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tMovies))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_top_rated_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:movie/domain/usecases/get_top_rated_movies.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetTopRatedMovies usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetTopRatedMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | test( 21 | 'should get list of movie from repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.getTopRatedMovies()) 25 | .thenAnswer((_) async => Right(tMovies)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tMovies))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/get_watchlist_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/usecases/get_watchlist_movies.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late GetWatchlistMovies usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetWatchlistMovies(mockMovieRepository); 16 | }); 17 | 18 | test( 19 | 'should get list of movie from the repository', 20 | () async { 21 | // arrange 22 | when(mockMovieRepository.getWatchlistMovies()) 23 | .thenAnswer((_) async => Right(testMovieList)); 24 | 25 | // act 26 | final result = await usecase.execute(); 27 | 28 | // assert 29 | expect(result, equals(Right(testMovieList))); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/remove_watchlist_movie_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/usecases/remove_watchlist_movie.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late RemoveWatchlistMovie usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = RemoveWatchlistMovie( 16 | movieRepository: mockMovieRepository, 17 | ); 18 | }); 19 | 20 | test( 21 | 'should remove a movie from repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.removeWatchlist(testMovieDetail)) 25 | .thenAnswer((_) async => const Right('Removed from watchlist')); 26 | 27 | // act 28 | final result = await usecase.execute(testMovieDetail); 29 | 30 | // assert 31 | verify(mockMovieRepository.removeWatchlist(testMovieDetail)); 32 | expect(result, equals(const Right('Removed from watchlist'))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /movie/test/domain/usecases/save_watchlist_movie_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/usecases/save_watchlist_movie.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late SaveWatchlistMovie usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = SaveWatchlistMovie( 16 | movieRepository: mockMovieRepository, 17 | ); 18 | }); 19 | 20 | test( 21 | 'should save a movie to the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.saveWatchlist(testMovieDetail)) 25 | .thenAnswer((_) async => const Right('Added to watchlist')); 26 | 27 | // act 28 | final result = await usecase.execute(testMovieDetail); 29 | 30 | // assert 31 | verify(mockMovieRepository.saveWatchlist(testMovieDetail)); 32 | expect(result, equals(const Right('Added to watchlist'))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /movie/test/helpers/dummy_objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:movie/data/models/movie_table.dart'; 2 | import 'package:movie/domain/entities/genre.dart'; 3 | import 'package:movie/domain/entities/media_image.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:movie/domain/entities/movie_detail.dart'; 6 | 7 | final testMovie = Movie( 8 | backdropPath: '/1Rr5SrvHxMXHu5RjKpaMba8VTzi.jpg', 9 | genreIds: const [28, 12, 878], 10 | id: 634649, 11 | overview: 12 | 'Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to discover what it truly means to be Spider-Man.', 13 | posterPath: '/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg', 14 | releaseDate: '2021-12-15', 15 | title: 'Spider-Man: No Way Home', 16 | voteAverage: 8.4, 17 | voteCount: 3427, 18 | ); 19 | 20 | final testMovieList = [testMovie]; 21 | 22 | const testMovieDetail = MovieDetail( 23 | backdropPath: '/path.jpg', 24 | genres: [Genre(id: 1, name: 'Genre 1')], 25 | id: 1, 26 | overview: 'Overview', 27 | posterPath: '/path.jpg', 28 | releaseDate: '2022-01-01', 29 | runtime: 100, 30 | title: 'Title', 31 | voteAverage: 1.0, 32 | voteCount: 1, 33 | ); 34 | 35 | final testWatchlistMovie = Movie.watchlist( 36 | releaseDate: '2022-01-01', 37 | id: 1, 38 | overview: 'Overview', 39 | posterPath: '/path.jpg', 40 | title: 'Title', 41 | voteAverage: 1.0, 42 | ); 43 | 44 | const testMovieTable = MovieTable( 45 | releaseDate: '2022-01-01', 46 | id: 1, 47 | overview: 'Overview', 48 | posterPath: '/path.jpg', 49 | title: 'Title', 50 | voteAverage: 1.0, 51 | ); 52 | 53 | final testMovieMap = { 54 | 'releaseDate': '2022-01-01', 55 | 'id': 1, 56 | 'overview': 'Overview', 57 | 'posterPath': '/path.jpg', 58 | 'title': 'Title', 59 | 'voteAverage': 1.0, 60 | }; 61 | 62 | const testImages = MediaImage( 63 | id: 1, 64 | backdropPaths: ['/path.jpg'], 65 | logoPaths: ['/path.jpg'], 66 | posterPaths: ['/path.jpg'], 67 | ); 68 | -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "backdrops": [ 3 | { 4 | "aspect_ratio": 1, 5 | "height": 1000, 6 | "iso_639_1": "en", 7 | "file_path": "/path.jpg", 8 | "vote_average": 1.0, 9 | "vote_count": 1, 10 | "width": 3000 11 | } 12 | ], 13 | "id": 1, 14 | "logos": [ 15 | { 16 | "aspect_ratio": 1, 17 | "height": 100, 18 | "iso_639_1": "en", 19 | "file_path": "/path.png", 20 | "vote_average": 1.0, 21 | "vote_count": 1, 22 | "width": 300 23 | } 24 | ], 25 | "posters": [ 26 | { 27 | "aspect_ratio": 1, 28 | "height": 3000, 29 | "iso_639_1": "en", 30 | "file_path": "/path.jpg", 31 | "vote_average": 1.0, 32 | "vote_count": 1, 33 | "width": 2000 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/movie_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "backdrop_path": "/path.jpg", 3 | "genres": [ 4 | { 5 | "id": 1, 6 | "name": "Genre 1" 7 | } 8 | ], 9 | "id": 1, 10 | "overview": "Overview", 11 | "poster_path": "/path.jpg", 12 | "release_date": "2022-01-01", 13 | "runtime": 100, 14 | "title": "Title", 15 | "vote_average": 1.0, 16 | "vote_count": 1 17 | } -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/movie_recommendations.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/path.jpg", 6 | "genre_ids": [ 7 | 1, 8 | 2, 9 | 3, 10 | 4 11 | ], 12 | "id": 1, 13 | "title": "Title", 14 | "overview": "Overview", 15 | "poster_path": "/path.jpg", 16 | "release_date": "2022-01-01", 17 | "vote_average": 1.0, 18 | "vote_count": 1 19 | } 20 | ], 21 | "total_pages": 1, 22 | "total_results": 10 23 | } -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/now_playing_movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "dates": { 3 | "maximum": "2022-05-01", 4 | "minimum": "2022-01-01" 5 | }, 6 | "page": 1, 7 | "results": [ 8 | { 9 | "backdrop_path": "/path.jpg", 10 | "genre_ids": [ 11 | 1, 12 | 2, 13 | 3, 14 | 4 15 | ], 16 | "id": 1, 17 | "overview": "Overview", 18 | "poster_path": "/path.jpg", 19 | "release_date": "2022-01-01", 20 | "title": "Title", 21 | "vote_average": 1.0, 22 | "vote_count": 1 23 | } 24 | ], 25 | "total_pages": 47, 26 | "total_results": 940 27 | } -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/popular_top_rated_movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/path.jpg", 6 | "genre_ids": [ 7 | 1, 8 | 2, 9 | 3, 10 | 4 11 | ], 12 | "id": 1, 13 | "overview": "Overview", 14 | "poster_path": "/path.jpg", 15 | "release_date": "2022-01-01", 16 | "title": "Title", 17 | "vote_average": 1.0, 18 | "vote_count": 1 19 | } 20 | ], 21 | "total_pages": 47, 22 | "total_results": 940 23 | } -------------------------------------------------------------------------------- /movie/test/helpers/dummy_responses/search_movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/1Rr5SrvHxMXHu5RjKpaMba8VTzi.jpg", 6 | "genre_ids": [ 7 | 28, 8 | 12, 9 | 878 10 | ], 11 | "id": 634649, 12 | "overview": "Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to discover what it truly means to be Spider-Man.", 13 | "poster_path": "/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", 14 | "release_date": "2021-12-15", 15 | "title": "Spider-Man: No Way Home", 16 | "vote_average": 8.4, 17 | "vote_count": 3427 18 | } 19 | ], 20 | "total_pages": 4, 21 | "total_results": 62 22 | } -------------------------------------------------------------------------------- /movie/test/helpers/json_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | String readJson(String name) { 4 | var dir = Directory.current.path; 5 | if (dir.endsWith('/test')) { 6 | dir = dir.replaceAll('/test', ''); 7 | } 8 | return File('$dir/test/$name').readAsStringSync(); 9 | } 10 | -------------------------------------------------------------------------------- /movie/test/helpers/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart' as http; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:movie/data/datasources/db/movie_database_helper.dart'; 4 | import 'package:movie/data/datasources/movie_local_data_source.dart'; 5 | import 'package:movie/data/datasources/movie_remote_data_source.dart'; 6 | import 'package:movie/domain/repositories/movie_repository.dart'; 7 | 8 | @GenerateMocks([ 9 | MovieRepository, 10 | MovieRemoteDataSource, 11 | MovieLocalDataSource, 12 | MovieDatabaseHelper, 13 | ], customMocks: [ 14 | MockSpec(as: #MockHttpClient) 15 | ]) 16 | void main() {} 17 | -------------------------------------------------------------------------------- /movie/test/presentation/provider/movie_images_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in movie/test/presentation/provider/movie_images_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:movie/domain/entities/media_image.dart' as _i7; 11 | import 'package:movie/domain/repositories/movie_repository.dart' as _i2; 12 | import 'package:movie/domain/usecases/get_movie_images.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeMovieRepository_0 extends _i1.Fake implements _i2.MovieRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetMovieImages]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetMovieImages extends _i1.Mock implements _i4.GetMovieImages { 32 | MockGetMovieImages() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.MovieRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeMovieRepository_0()) as _i2.MovieRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, _i7.MediaImage>> execute(int? id) => 42 | (super.noSuchMethod(Invocation.method(#execute, [id]), 43 | returnValue: Future<_i3.Either<_i6.Failure, _i7.MediaImage>>.value( 44 | _FakeEither_1<_i6.Failure, _i7.MediaImage>())) as _i5 45 | .Future<_i3.Either<_i6.Failure, _i7.MediaImage>>); 46 | } 47 | -------------------------------------------------------------------------------- /movie/test/presentation/provider/popular_movies_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in movie/test/presentation/provider/popular_movies_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:movie/domain/entities/movie.dart' as _i7; 11 | import 'package:movie/domain/repositories/movie_repository.dart' as _i2; 12 | import 'package:movie/domain/usecases/get_popular_movies.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeMovieRepository_0 extends _i1.Fake implements _i2.MovieRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetPopularMovies]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetPopularMovies extends _i1.Mock implements _i4.GetPopularMovies { 32 | MockGetPopularMovies() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.MovieRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeMovieRepository_0()) as _i2.MovieRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 42 | (super.noSuchMethod(Invocation.method(#execute, []), 43 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 44 | _FakeEither_1<_i6.Failure, List<_i7.Movie>>())) as _i5 45 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 46 | } 47 | -------------------------------------------------------------------------------- /movie/test/presentation/provider/top_rated_movies_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in movie/test/presentation/provider/top_rated_movies_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:movie/domain/entities/movie.dart' as _i7; 11 | import 'package:movie/domain/repositories/movie_repository.dart' as _i2; 12 | import 'package:movie/domain/usecases/get_top_rated_movies.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeMovieRepository_0 extends _i1.Fake implements _i2.MovieRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetTopRatedMovies]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetTopRatedMovies extends _i1.Mock implements _i4.GetTopRatedMovies { 32 | MockGetTopRatedMovies() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.MovieRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeMovieRepository_0()) as _i2.MovieRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 42 | (super.noSuchMethod(Invocation.method(#execute, []), 43 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 44 | _FakeEither_1<_i6.Failure, List<_i7.Movie>>())) as _i5 45 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 46 | } 47 | -------------------------------------------------------------------------------- /movie/test/presentation/provider/watchlist_movie_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:core/utils/state_enum.dart'; 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:movie/domain/usecases/get_watchlist_movies.dart'; 8 | import 'package:movie/presentation/provider/watchlist_movie_notifier.dart'; 9 | 10 | import '../../helpers/dummy_objects.dart'; 11 | import 'watchlist_movie_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([GetWatchlistMovies]) 14 | void main() { 15 | late int listenerCallCount; 16 | late MockGetWatchlistMovies mockGetWatchlistMovies; 17 | late WatchlistMovieNotifier provider; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockGetWatchlistMovies = MockGetWatchlistMovies(); 22 | provider = WatchlistMovieNotifier( 23 | getWatchlistMovies: mockGetWatchlistMovies, 24 | )..addListener(() { 25 | listenerCallCount++; 26 | }); 27 | }); 28 | 29 | test( 30 | 'should change movies when data is gotten successfully', 31 | () async { 32 | // arrange 33 | when(mockGetWatchlistMovies.execute()) 34 | .thenAnswer((_) async => Right([testWatchlistMovie])); 35 | 36 | // act 37 | await provider.fetchWatchlistMovies(); 38 | 39 | // assert 40 | expect(provider.watchlistState, equals(RequestState.loaded)); 41 | expect(provider.watchlistMovies, equals([testWatchlistMovie])); 42 | expect(listenerCallCount, equals(2)); 43 | }, 44 | ); 45 | 46 | test( 47 | 'should return database failure when error', 48 | () async { 49 | // arrange 50 | when(mockGetWatchlistMovies.execute()).thenAnswer( 51 | (_) async => const Left(DatabaseFailure('Can\'t get data'))); 52 | 53 | // act 54 | await provider.fetchWatchlistMovies(); 55 | 56 | // assert 57 | expect(provider.watchlistState, equals(RequestState.error)); 58 | expect(provider.message, equals('Can\'t get data')); 59 | expect(listenerCallCount, equals(2)); 60 | }, 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /movie/test/presentation/provider/watchlist_movie_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in movie/test/presentation/provider/watchlist_movie_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i4; 6 | 7 | import 'package:core/utils/failure.dart' as _i5; 8 | import 'package:dartz/dartz.dart' as _i2; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:movie/domain/entities/movie.dart' as _i6; 11 | import 'package:movie/domain/usecases/get_watchlist_movies.dart' as _i3; 12 | 13 | // ignore_for_file: type=lint 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: avoid_setters_without_getters 16 | // ignore_for_file: comment_references 17 | // ignore_for_file: implementation_imports 18 | // ignore_for_file: invalid_use_of_visible_for_testing_member 19 | // ignore_for_file: prefer_const_constructors 20 | // ignore_for_file: unnecessary_parenthesis 21 | // ignore_for_file: camel_case_types 22 | 23 | class _FakeEither_0 extends _i1.Fake implements _i2.Either {} 24 | 25 | /// A class which mocks [GetWatchlistMovies]. 26 | /// 27 | /// See the documentation for Mockito's code generation for more information. 28 | class MockGetWatchlistMovies extends _i1.Mock 29 | implements _i3.GetWatchlistMovies { 30 | MockGetWatchlistMovies() { 31 | _i1.throwOnMissingStub(this); 32 | } 33 | 34 | @override 35 | _i4.Future<_i2.Either<_i5.Failure, List<_i6.Movie>>> execute() => 36 | (super.noSuchMethod(Invocation.method(#execute, []), 37 | returnValue: Future<_i2.Either<_i5.Failure, List<_i6.Movie>>>.value( 38 | _FakeEither_0<_i5.Failure, List<_i6.Movie>>())) as _i4 39 | .Future<_i2.Either<_i5.Failure, List<_i6.Movie>>>); 40 | } 41 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ditonton 2 | description: A new Flutter application. 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.12.0 <3.0.0" 10 | 11 | dependencies: 12 | about: 13 | path: about 14 | core: 15 | path: core 16 | animate_do: ^2.1.0 17 | flutter_bloc: ^8.0.0 18 | cached_network_image: ^3.2.0 19 | carousel_slider: ^4.0.0 20 | cupertino_icons: ^1.0.4 21 | dartz: ^0.10.1 22 | equatable: ^2.0.3 23 | flutter: 24 | sdk: flutter 25 | flutter_rating_bar: ^4.0.0 26 | get_it: ^7.2.0 27 | google_fonts: ^2.2.0 28 | http: ^0.13.4 29 | movie: 30 | path: movie 31 | intl: ^0.17.0 32 | path_provider: ^2.0.8 33 | provider: ^6.0.2 34 | search: 35 | path: search 36 | shimmer: ^2.0.0 37 | sqflite: ^2.0.1 38 | tv: 39 | path: tv 40 | 41 | 42 | dev_dependencies: 43 | bloc_test: ^9.0.2 44 | build_runner: ^2.1.7 45 | flutter_lints: ^1.0.4 46 | flutter_test: 47 | sdk: flutter 48 | integration_test: 49 | sdk: flutter 50 | mockito: ^5.0.17 51 | 52 | flutter: 53 | uses-material-design: true 54 | 55 | assets: 56 | - assets/ 57 | -------------------------------------------------------------------------------- /search/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /search/.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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /search/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /search/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /search/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /search/coverage/lcov.info: -------------------------------------------------------------------------------- 1 | SF:lib/domain/usecases/search_tvs.dart 2 | DA:9,1 3 | DA:11,1 4 | DA:12,2 5 | LF:3 6 | LH:3 7 | end_of_record 8 | SF:lib/domain/usecases/search_movies.dart 9 | DA:9,1 10 | DA:11,1 11 | DA:12,2 12 | LF:3 13 | LH:3 14 | end_of_record 15 | SF:lib/presentation/provider/tv_search_notifier.dart 16 | DA:9,1 17 | DA:12,2 18 | DA:15,2 19 | DA:18,2 20 | DA:20,1 21 | DA:21,1 22 | DA:22,1 23 | DA:24,3 24 | DA:25,1 25 | DA:26,1 26 | DA:27,1 27 | DA:28,2 28 | DA:29,1 29 | DA:31,1 30 | DA:32,1 31 | DA:33,1 32 | DA:34,1 33 | LF:17 34 | LH:17 35 | end_of_record 36 | SF:lib/presentation/provider/movie_search_notifier.dart 37 | DA:9,1 38 | DA:12,2 39 | DA:15,2 40 | DA:18,2 41 | DA:20,1 42 | DA:21,1 43 | DA:22,1 44 | DA:24,3 45 | DA:25,1 46 | DA:26,1 47 | DA:27,2 48 | DA:28,1 49 | DA:29,1 50 | DA:31,1 51 | DA:32,1 52 | DA:33,1 53 | DA:34,1 54 | LF:17 55 | LH:17 56 | end_of_record 57 | -------------------------------------------------------------------------------- /search/lib/domain/usecases/search_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:movie/domain/entities/movie.dart'; 4 | import 'package:movie/domain/repositories/movie_repository.dart'; 5 | 6 | class SearchMovies { 7 | final MovieRepository repository; 8 | 9 | SearchMovies(this.repository); 10 | 11 | Future>> execute(String query) { 12 | return repository.searchMovies(query); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /search/lib/domain/usecases/search_tvs.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:tv/domain/entities/tv.dart'; 4 | import 'package:tv/domain/repositories/tv_repository.dart'; 5 | 6 | class SearchTvs { 7 | final TvRepository repository; 8 | 9 | SearchTvs(this.repository); 10 | 11 | Future>> execute(String query) { 12 | return repository.searchTvs(query); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /search/lib/presentation/bloc/search_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:movie/domain/entities/movie.dart'; 4 | import 'package:rxdart/rxdart.dart'; 5 | import 'package:tv/domain/entities/tv.dart'; 6 | import '../../domain/usecases/search_movies.dart'; 7 | import '../../domain/usecases/search_tvs.dart'; 8 | 9 | part 'search_event.dart'; 10 | part 'search_state.dart'; 11 | 12 | class MovieSearchBloc extends Bloc { 13 | final SearchMovies _searchMovies; 14 | 15 | MovieSearchBloc(this._searchMovies) : super(SearchEmpty()) { 16 | on( 17 | (event, emit) async { 18 | final query = event.query; 19 | 20 | emit(SearchLoading()); 21 | final result = await _searchMovies.execute(query); 22 | 23 | result.fold( 24 | (failure) { 25 | emit(SearchError(failure.message)); 26 | }, 27 | (data) { 28 | emit(MovieSearchHasData(data)); 29 | }, 30 | ); 31 | }, 32 | transformer: debounce(const Duration(milliseconds: 500)), 33 | ); 34 | } 35 | } 36 | 37 | class TvSearchBloc extends Bloc { 38 | final SearchTvs _searchTvs; 39 | 40 | TvSearchBloc(this._searchTvs) : super(SearchEmpty()) { 41 | on( 42 | (event, emit) async { 43 | final query = event.query; 44 | 45 | emit(SearchLoading()); 46 | final result = await _searchTvs.execute(query); 47 | 48 | result.fold( 49 | (failure) { 50 | emit(SearchError(failure.message)); 51 | }, 52 | (data) { 53 | emit(TvSearchHasData(data)); 54 | }, 55 | ); 56 | }, 57 | transformer: debounce(const Duration(milliseconds: 500)), 58 | ); 59 | } 60 | } 61 | 62 | EventTransformer debounce(Duration duration) { 63 | return (events, mapper) => events.debounceTime(duration).flatMap(mapper); 64 | } 65 | -------------------------------------------------------------------------------- /search/lib/presentation/bloc/search_event.dart: -------------------------------------------------------------------------------- 1 | part of 'search_bloc.dart'; 2 | 3 | abstract class SearchEvent extends Equatable { 4 | const SearchEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class OnQueryChanged extends SearchEvent { 11 | final String query; 12 | 13 | const OnQueryChanged(this.query); 14 | 15 | @override 16 | List get props => [query]; 17 | } 18 | -------------------------------------------------------------------------------- /search/lib/presentation/bloc/search_state.dart: -------------------------------------------------------------------------------- 1 | part of 'search_bloc.dart'; 2 | 3 | abstract class SearchState extends Equatable { 4 | const SearchState(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class SearchEmpty extends SearchState {} 11 | 12 | class SearchLoading extends SearchState {} 13 | 14 | class SearchError extends SearchState { 15 | final String message; 16 | 17 | const SearchError(this.message); 18 | 19 | @override 20 | List get props => [message]; 21 | } 22 | 23 | class MovieSearchHasData extends SearchState { 24 | final List result; 25 | 26 | const MovieSearchHasData(this.result); 27 | 28 | @override 29 | List get props => [result]; 30 | } 31 | 32 | class TvSearchHasData extends SearchState { 33 | final List result; 34 | 35 | const TvSearchHasData(this.result); 36 | 37 | @override 38 | List get props => [result]; 39 | } 40 | -------------------------------------------------------------------------------- /search/lib/search.dart: -------------------------------------------------------------------------------- 1 | library search; 2 | 3 | export 'package:search/presentation/bloc/search_bloc.dart'; 4 | 5 | export 'domain/usecases/search_movies.dart'; 6 | export 'domain/usecases/search_tvs.dart'; 7 | export 'presentation/pages/movie_search_page.dart'; 8 | -------------------------------------------------------------------------------- /search/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: search 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | publish_to: none 7 | 8 | environment: 9 | sdk: ">=2.15.1 <3.0.0" 10 | flutter: ">=1.17.0" 11 | 12 | dependencies: 13 | core: 14 | path: ../core 15 | dartz: ^0.10.1 16 | equatable: ^2.0.3 17 | flutter: 18 | sdk: flutter 19 | flutter_bloc: ^8.0.1 20 | movie: 21 | path: ../movie 22 | rxdart: ^0.27.3 23 | tv: 24 | path: ../tv 25 | 26 | dev_dependencies: 27 | bloc_test: ^9.0.2 28 | build_runner: ^2.1.7 29 | flutter_lints: ^1.0.4 30 | flutter_test: 31 | sdk: flutter 32 | mockito: ^5.0.17 33 | 34 | flutter: 35 | # To add assets to your package, add an assets section, like this: 36 | # assets: 37 | # - images/a_dot_burr.jpeg 38 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /search/test/domain/usecases/search_movie_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:movie/domain/entities/movie.dart'; 5 | import 'package:search/domain/usecases/search_movies.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockMovieRepository mockMovieRepository; 11 | late SearchMovies usecase; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = SearchMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | const tQuery = 'Spiderman'; 20 | 21 | test( 22 | 'should get list of movie from the repository based on query', 23 | () async { 24 | // arrange 25 | when(mockMovieRepository.searchMovies(tQuery)) 26 | .thenAnswer((_) async => Right(tMovies)); 27 | 28 | // act 29 | final result = await usecase.execute(tQuery); 30 | 31 | // assert 32 | expect(result, equals(Right(tMovies))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /search/test/domain/usecases/search_tv_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:search/domain/usecases/search_tvs.dart'; 5 | import 'package:tv/domain/entities/tv.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late SearchTvs usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = SearchTvs(mockTvRepository); 16 | }); 17 | 18 | final tTvs = []; 19 | const tQuery = 'Arcane'; 20 | 21 | test( 22 | 'should get list of tv from the repository based on query', 23 | () async { 24 | // arrange 25 | when(mockTvRepository.searchTvs(tQuery)) 26 | .thenAnswer((_) async => Right(tTvs)); 27 | 28 | // act 29 | final result = await usecase.execute(tQuery); 30 | 31 | // assert 32 | expect(result, equals(Right(tTvs))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /search/test/helpers/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart' as http; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:movie/domain/repositories/movie_repository.dart'; 4 | import 'package:tv/domain/repositories/tv_repository.dart'; 5 | 6 | @GenerateMocks([ 7 | MovieRepository, 8 | TvRepository, 9 | ], customMocks: [ 10 | MockSpec(as: #MockHttpClient) 11 | ]) 12 | void main() {} 13 | -------------------------------------------------------------------------------- /tv/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /tv/.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: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /tv/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /tv/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /tv/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /tv/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /tv/lib/data/datasources/db/tv_database_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | import '../../models/tv_table.dart'; 6 | 7 | class TvDatabaseHelper { 8 | static TvDatabaseHelper? _databaseHelper; 9 | TvDatabaseHelper._instance() { 10 | _databaseHelper = this; 11 | } 12 | 13 | factory TvDatabaseHelper() => _databaseHelper ?? TvDatabaseHelper._instance(); 14 | 15 | static Database? _database; 16 | 17 | Future get database async { 18 | _database ??= await _initDb(); 19 | return _database; 20 | } 21 | 22 | static const String _tvWatchlistTable = 'tvWatchlistTable'; 23 | 24 | Future _initDb() async { 25 | final path = await getDatabasesPath(); 26 | final databasePath = '$path/tv.db'; 27 | 28 | var db = await openDatabase(databasePath, version: 1, onCreate: _onCreate); 29 | return db; 30 | } 31 | 32 | void _onCreate(Database db, int version) async { 33 | await db.execute(''' 34 | CREATE TABLE $_tvWatchlistTable ( 35 | firstAirDate TEXT, 36 | id INTEGER PRIMARY KEY, 37 | name TEXT, 38 | overview TEXT, 39 | posterPath TEXT, 40 | voteAverage DOUBLE 41 | ); 42 | '''); 43 | } 44 | 45 | Future insertTvWatchlist(TvTable tv) async { 46 | final db = await database; 47 | return await db!.insert(_tvWatchlistTable, tv.toMap()); 48 | } 49 | 50 | Future removeTvWatchlist(TvTable tv) async { 51 | final db = await database; 52 | return await db!.delete( 53 | _tvWatchlistTable, 54 | where: 'id = ?', 55 | whereArgs: [tv.id], 56 | ); 57 | } 58 | 59 | Future?> getTvById(int id) async { 60 | final db = await database; 61 | final results = await db!.query( 62 | _tvWatchlistTable, 63 | where: 'id = ?', 64 | whereArgs: [id], 65 | ); 66 | 67 | if (results.isNotEmpty) { 68 | return results.first; 69 | } else { 70 | return null; 71 | } 72 | } 73 | 74 | Future>> getWatchlistTvs() async { 75 | final db = await database; 76 | final List> results = 77 | await db!.query(_tvWatchlistTable); 78 | 79 | return results; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tv/lib/data/datasources/tv_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/exception.dart'; 2 | 3 | import '../models/tv_table.dart'; 4 | import 'db/tv_database_helper.dart'; 5 | 6 | abstract class TvLocalDataSource { 7 | Future insertWatchlist(TvTable tv); 8 | Future removeWatchlist(TvTable tv); 9 | Future getTvById(int id); 10 | Future> getWatchlistTvs(); 11 | } 12 | 13 | class TvLocalDataSourceImpl implements TvLocalDataSource { 14 | final TvDatabaseHelper databaseHelper; 15 | 16 | TvLocalDataSourceImpl({required this.databaseHelper}); 17 | 18 | @override 19 | Future insertWatchlist(TvTable tv) async { 20 | try { 21 | await databaseHelper.insertTvWatchlist(tv); 22 | return 'Added to watchlist'; 23 | } catch (e) { 24 | throw DatabaseException(e.toString()); 25 | } 26 | } 27 | 28 | @override 29 | Future removeWatchlist(TvTable tv) async { 30 | try { 31 | await databaseHelper.removeTvWatchlist(tv); 32 | return 'Removed from watchlist'; 33 | } catch (e) { 34 | throw DatabaseException(e.toString()); 35 | } 36 | } 37 | 38 | @override 39 | Future getTvById(int id) async { 40 | final result = await databaseHelper.getTvById(id); 41 | if (result != null) { 42 | return TvTable.fromMap(result); 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | @override 49 | Future> getWatchlistTvs() async { 50 | final result = await databaseHelper.getWatchlistTvs(); 51 | return result.map((data) => TvTable.fromMap(data)).toList(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tv/lib/data/models/genre_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/genre.dart'; 4 | 5 | class GenreModel extends Equatable { 6 | final int id; 7 | final String name; 8 | 9 | const GenreModel({ 10 | required this.id, 11 | required this.name, 12 | }); 13 | 14 | factory GenreModel.fromJson(Map json) => GenreModel( 15 | id: json['id'], 16 | name: json['name'], 17 | ); 18 | 19 | Map toJson() => { 20 | 'id': id, 21 | 'name': name, 22 | }; 23 | 24 | Genre toEntity() => Genre( 25 | id: id, 26 | name: name, 27 | ); 28 | 29 | @override 30 | List get props => [id, name]; 31 | } 32 | -------------------------------------------------------------------------------- /tv/lib/data/models/media_image_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/media_image.dart'; 4 | 5 | class MediaImageModel extends Equatable { 6 | final int id; 7 | final List backdropPaths; 8 | final List logoPaths; 9 | final List posterPaths; 10 | 11 | const MediaImageModel({ 12 | required this.id, 13 | required this.backdropPaths, 14 | required this.logoPaths, 15 | required this.posterPaths, 16 | }); 17 | 18 | factory MediaImageModel.fromJson(Map json) => 19 | MediaImageModel( 20 | id: json['id'], 21 | backdropPaths: List.from( 22 | json['backdrops'].map((x) => x['file_path']), 23 | ), 24 | logoPaths: List.from( 25 | json['logos'].map((x) => x['file_path']), 26 | ), 27 | posterPaths: List.from( 28 | json['posters'].map((x) => x['file_path']), 29 | ), 30 | ); 31 | 32 | MediaImage toEntity() => MediaImage( 33 | id: id, 34 | backdropPaths: backdropPaths, 35 | logoPaths: logoPaths, 36 | posterPaths: posterPaths, 37 | ); 38 | 39 | @override 40 | List get props => [ 41 | id, 42 | backdropPaths, 43 | logoPaths, 44 | posterPaths, 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /tv/lib/data/models/tv_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/tv.dart'; 4 | 5 | class TvModel extends Equatable { 6 | final String? backdropPath; 7 | final String firstAirDate; 8 | final List genreIds; 9 | final int id; 10 | final String name; 11 | final String overview; 12 | final String? posterPath; 13 | final double voteAverage; 14 | final int voteCount; 15 | 16 | const TvModel({ 17 | required this.backdropPath, 18 | required this.firstAirDate, 19 | required this.genreIds, 20 | required this.id, 21 | required this.name, 22 | required this.overview, 23 | required this.posterPath, 24 | required this.voteAverage, 25 | required this.voteCount, 26 | }); 27 | 28 | factory TvModel.fromJson(Map json) => TvModel( 29 | backdropPath: json['backdrop_path'], 30 | firstAirDate: json['first_air_date'], 31 | genreIds: List.from(json['genre_ids'].map((x) => x)), 32 | id: json['id'], 33 | name: json['name'], 34 | overview: json['overview'], 35 | posterPath: json['poster_path'], 36 | voteAverage: json['vote_average'].toDouble(), 37 | voteCount: json['vote_count'], 38 | ); 39 | 40 | Map toJson() => { 41 | 'backdrop_path': backdropPath, 42 | 'first_air_date': firstAirDate, 43 | 'genre_ids': List.from(genreIds.map((x) => x)), 44 | 'id': id, 45 | 'name': name, 46 | 'overview': overview, 47 | 'poster_path': posterPath, 48 | 'vote_average': voteAverage, 49 | 'vote_count': voteCount, 50 | }; 51 | 52 | Tv toEntity() => Tv( 53 | backdropPath: backdropPath, 54 | firstAirDate: firstAirDate, 55 | genreIds: genreIds, 56 | id: id, 57 | name: name, 58 | overview: overview, 59 | posterPath: posterPath, 60 | voteAverage: voteAverage, 61 | voteCount: voteCount, 62 | ); 63 | 64 | @override 65 | List get props => [ 66 | backdropPath, 67 | firstAirDate, 68 | genreIds, 69 | id, 70 | name, 71 | overview, 72 | posterPath, 73 | voteAverage, 74 | voteCount, 75 | ]; 76 | } 77 | -------------------------------------------------------------------------------- /tv/lib/data/models/tv_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'tv_model.dart'; 4 | 5 | class TvResponse extends Equatable { 6 | final List tvList; 7 | 8 | const TvResponse({required this.tvList}); 9 | 10 | factory TvResponse.fromJson(Map json) => TvResponse( 11 | tvList: List.from((json['results'] as List) 12 | .map((x) => TvModel.fromJson(x)) 13 | .where((element) => 14 | element.posterPath != null && element.backdropPath != null)), 15 | ); 16 | 17 | Map toJson() => { 18 | 'results': List.from(tvList.map((x) => x.toJson())), 19 | }; 20 | 21 | @override 22 | List get props => [tvList]; 23 | } 24 | -------------------------------------------------------------------------------- /tv/lib/data/models/tv_season_episode_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'tv_season_episode_model.dart'; 4 | 5 | class TvSeasonEpisodeResponse extends Equatable { 6 | final List tvEpisodes; 7 | 8 | const TvSeasonEpisodeResponse({ 9 | required this.tvEpisodes, 10 | }); 11 | 12 | factory TvSeasonEpisodeResponse.fromJson(Map json) => 13 | TvSeasonEpisodeResponse( 14 | tvEpisodes: List.from((json['episodes'] as List) 15 | .map((x) => TvSeasonEpisodeModel.fromJson(x)) 16 | .where((element) => element.stillPath != null)), 17 | ); 18 | 19 | Map toJson() => { 20 | 'episodes': List.from(tvEpisodes.map((x) => x.toJson())), 21 | }; 22 | 23 | @override 24 | List get props => [ 25 | tvEpisodes, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /tv/lib/data/models/tv_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import '../../domain/entities/tv.dart'; 4 | import '../../domain/entities/tv_detail.dart'; 5 | 6 | class TvTable extends Equatable { 7 | final String? firstAirDate; 8 | final int id; 9 | final String? name; 10 | final String? overview; 11 | final String? posterPath; 12 | final double? voteAverage; 13 | 14 | const TvTable({ 15 | required this.firstAirDate, 16 | required this.id, 17 | required this.name, 18 | required this.overview, 19 | required this.posterPath, 20 | required this.voteAverage, 21 | }); 22 | 23 | factory TvTable.fromMap(Map map) => TvTable( 24 | firstAirDate: map['firstAirDate'], 25 | id: map['id'], 26 | name: map['name'], 27 | overview: map['overview'], 28 | posterPath: map['posterPath'], 29 | voteAverage: map['voteAverage'], 30 | ); 31 | 32 | factory TvTable.fromEntity(TvDetail tv) => TvTable( 33 | firstAirDate: tv.firstAirDate, 34 | id: tv.id, 35 | name: tv.name, 36 | overview: tv.overview, 37 | posterPath: tv.posterPath, 38 | voteAverage: tv.voteAverage, 39 | ); 40 | 41 | Map toMap() => { 42 | 'firstAirDate': firstAirDate, 43 | 'id': id, 44 | 'name': name, 45 | 'overview': overview, 46 | 'posterPath': posterPath, 47 | 'voteAverage': voteAverage, 48 | }; 49 | 50 | Tv toEntity() => Tv.watchList( 51 | firstAirDate: firstAirDate, 52 | id: id, 53 | name: name, 54 | overview: overview, 55 | posterPath: posterPath, 56 | voteAverage: voteAverage, 57 | ); 58 | 59 | @override 60 | List get props => [ 61 | firstAirDate, 62 | id, 63 | name, 64 | overview, 65 | posterPath, 66 | voteAverage, 67 | ]; 68 | } 69 | -------------------------------------------------------------------------------- /tv/lib/domain/entities/genre.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Genre extends Equatable { 4 | final int id; 5 | final String name; 6 | 7 | const Genre({ 8 | required this.id, 9 | required this.name, 10 | }); 11 | 12 | @override 13 | List get props => [id, name]; 14 | } 15 | -------------------------------------------------------------------------------- /tv/lib/domain/entities/media_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class MediaImage extends Equatable { 4 | final int id; 5 | final List backdropPaths; 6 | final List logoPaths; 7 | final List posterPaths; 8 | 9 | const MediaImage({ 10 | required this.id, 11 | required this.backdropPaths, 12 | required this.logoPaths, 13 | required this.posterPaths, 14 | }); 15 | 16 | @override 17 | List get props => [id, backdropPaths, logoPaths, posterPaths]; 18 | } 19 | -------------------------------------------------------------------------------- /tv/lib/domain/entities/tv.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | // ignore: must_be_immutable 4 | class Tv extends Equatable { 5 | String? backdropPath; 6 | String? firstAirDate; 7 | List? genreIds; 8 | int id; 9 | String? name; 10 | String? overview; 11 | String? posterPath; 12 | double? voteAverage; 13 | int? voteCount; 14 | 15 | Tv({ 16 | required this.backdropPath, 17 | required this.firstAirDate, 18 | required this.genreIds, 19 | required this.id, 20 | required this.name, 21 | required this.overview, 22 | required this.posterPath, 23 | required this.voteAverage, 24 | required this.voteCount, 25 | }); 26 | 27 | Tv.watchList({ 28 | required this.firstAirDate, 29 | required this.id, 30 | required this.name, 31 | required this.overview, 32 | required this.posterPath, 33 | required this.voteAverage, 34 | }); 35 | 36 | @override 37 | List get props => [ 38 | backdropPath, 39 | firstAirDate, 40 | genreIds, 41 | id, 42 | name, 43 | overview, 44 | posterPath, 45 | voteAverage, 46 | voteCount, 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /tv/lib/domain/entities/tv_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | import 'genre.dart'; 4 | 5 | class TvDetail extends Equatable { 6 | final String? backdropPath; 7 | final List episodeRunTime; 8 | final String firstAirDate; 9 | final List genres; 10 | final int id; 11 | final String name; 12 | final int numberOfSeasons; 13 | final String overview; 14 | final String? posterPath; 15 | final double voteAverage; 16 | final int voteCount; 17 | 18 | const TvDetail({ 19 | required this.backdropPath, 20 | required this.episodeRunTime, 21 | required this.firstAirDate, 22 | required this.genres, 23 | required this.id, 24 | required this.name, 25 | required this.numberOfSeasons, 26 | required this.overview, 27 | required this.posterPath, 28 | required this.voteAverage, 29 | required this.voteCount, 30 | }); 31 | 32 | @override 33 | List get props => [ 34 | backdropPath, 35 | episodeRunTime, 36 | firstAirDate, 37 | genres, 38 | id, 39 | name, 40 | numberOfSeasons, 41 | overview, 42 | posterPath, 43 | voteAverage, 44 | voteCount, 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /tv/lib/domain/entities/tv_season_episode.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class TvSeasonEpisode extends Equatable { 4 | final String airDate; 5 | final int episodeNumber; 6 | final int id; 7 | final String name; 8 | final String overview; 9 | final int seasonNumber; 10 | final String? stillPath; 11 | final double voteAverage; 12 | final int voteCount; 13 | 14 | const TvSeasonEpisode({ 15 | required this.airDate, 16 | required this.episodeNumber, 17 | required this.id, 18 | required this.name, 19 | required this.overview, 20 | required this.seasonNumber, 21 | required this.stillPath, 22 | required this.voteAverage, 23 | required this.voteCount, 24 | }); 25 | 26 | @override 27 | List get props => [ 28 | airDate, 29 | episodeNumber, 30 | id, 31 | name, 32 | overview, 33 | seasonNumber, 34 | stillPath, 35 | voteAverage, 36 | voteCount, 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /tv/lib/domain/repositories/tv_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import '../entities/media_image.dart'; 4 | import '../entities/tv.dart'; 5 | import '../entities/tv_detail.dart'; 6 | import '../entities/tv_season_episode.dart'; 7 | 8 | abstract class TvRepository { 9 | Future>> getOnTheAirTvs(); 10 | Future>> getPopularTvs(); 11 | Future>> getTopRatedTvs(); 12 | Future> getTvDetail(int id); 13 | Future>> getTvRecommendations(int id); 14 | Future>> getTvSeasonEpisodes( 15 | int id, int seasonNumber); 16 | Future>> searchTvs(String query); 17 | Future> getTvImages(int id); 18 | Future> saveWatchlist(TvDetail tv); 19 | Future> removeWatchlist(TvDetail tv); 20 | Future isAddedToWatchlist(int id); 21 | Future>> getWatchlistTvs(); 22 | } 23 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_on_the_air_tvs.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetOnTheAirTvs { 8 | final TvRepository repository; 9 | 10 | GetOnTheAirTvs(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getOnTheAirTvs(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_popular_tvs.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetPopularTvs { 8 | final TvRepository repository; 9 | 10 | GetPopularTvs(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getPopularTvs(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_top_rated_tvs.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetTopRatedTvs { 8 | final TvRepository repository; 9 | 10 | GetTopRatedTvs(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getTopRatedTvs(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_tv_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv_detail.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetTvDetail { 8 | final TvRepository repository; 9 | 10 | GetTvDetail(this.repository); 11 | 12 | Future> execute(int id) { 13 | return repository.getTvDetail(id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_tv_images.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/media_image.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetTvImages { 8 | final TvRepository repository; 9 | 10 | GetTvImages(this.repository); 11 | 12 | Future> execute(int id) { 13 | return repository.getTvImages(id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_tv_recommendations.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetTvRecommendations { 8 | final TvRepository repository; 9 | 10 | GetTvRecommendations(this.repository); 11 | 12 | Future>> execute(id) { 13 | return repository.getTvRecommendations(id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_tv_season_episodes.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv_season_episode.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetTvSeasonEpisodes { 8 | final TvRepository repository; 9 | 10 | GetTvSeasonEpisodes(this.repository); 11 | 12 | Future>> execute(id, seasonNumber) { 13 | return repository.getTvSeasonEpisodes(id, seasonNumber); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_tv_watchlist_status.dart: -------------------------------------------------------------------------------- 1 | import '../repositories/tv_repository.dart'; 2 | 3 | class GetTvWatchlistStatus { 4 | final TvRepository tvRepository; 5 | 6 | GetTvWatchlistStatus({ 7 | required this.tvRepository, 8 | }); 9 | 10 | Future execute(int id) async { 11 | return tvRepository.isAddedToWatchlist(id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/get_watchlist_tvs.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class GetWatchlistTvs { 8 | final TvRepository repository; 9 | 10 | GetWatchlistTvs(this.repository); 11 | 12 | Future>> execute() { 13 | return repository.getWatchlistTvs(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/remove_watchlist_tv.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv_detail.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class RemoveWatchlistTv { 8 | final TvRepository tvRepository; 9 | 10 | RemoveWatchlistTv({required this.tvRepository}); 11 | 12 | Future> execute(TvDetail tv) { 13 | return tvRepository.removeWatchlist(tv); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/domain/usecases/save_watchlist_tv.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | 4 | import '../entities/tv_detail.dart'; 5 | import '../repositories/tv_repository.dart'; 6 | 7 | class SaveWatchlistTv { 8 | final TvRepository tvRepository; 9 | 10 | SaveWatchlistTv({required this.tvRepository}); 11 | 12 | Future> execute(TvDetail tv) { 13 | return tvRepository.saveWatchlist(tv); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tv/lib/presentation/pages/tv_watchlist_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import '../provider/watchlist_tv_provider.dart'; 6 | import '../widgets/item_card_list.dart'; 7 | 8 | class TvWatchlist extends StatelessWidget { 9 | const TvWatchlist({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Consumer( 14 | builder: (context, data, child) { 15 | if (data.watchlistState == RequestState.loading) { 16 | return const Center(child: CircularProgressIndicator()); 17 | } else if (data.watchlistState == RequestState.loaded) { 18 | return ListView.builder( 19 | key: const Key('tvWatchlist'), 20 | itemCount: data.watchlistTvs.length, 21 | padding: const EdgeInsets.all(16.0), 22 | itemBuilder: (context, index) { 23 | final tv = data.watchlistTvs[index]; 24 | return ItemCard( 25 | tv: tv, 26 | ); 27 | }, 28 | ); 29 | } else { 30 | return Center( 31 | key: const Key('error_message'), 32 | child: Text(data.message), 33 | ); 34 | } 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tv/lib/presentation/provider/popular_tvs_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/tv.dart'; 5 | import '../../domain/usecases/get_popular_tvs.dart'; 6 | 7 | class PopularTvsNotifier extends ChangeNotifier { 8 | final GetPopularTvs getPopularTvs; 9 | 10 | PopularTvsNotifier(this.getPopularTvs); 11 | 12 | List _tvs = []; 13 | List get tvs => _tvs; 14 | 15 | RequestState _state = RequestState.empty; 16 | RequestState get state => _state; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchPopularTvs() async { 22 | _state = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getPopularTvs.execute(); 26 | result.fold( 27 | (failure) { 28 | _state = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (tvsData) { 33 | _state = RequestState.loaded; 34 | _tvs = tvsData; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tv/lib/presentation/provider/top_rated_tvs_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/tv.dart'; 5 | import '../../domain/usecases/get_top_rated_tvs.dart'; 6 | 7 | class TopRatedTvsNotifier extends ChangeNotifier { 8 | final GetTopRatedTvs getTopRatedTvs; 9 | 10 | TopRatedTvsNotifier(this.getTopRatedTvs); 11 | 12 | List _tvs = []; 13 | List get tvs => _tvs; 14 | 15 | RequestState _state = RequestState.empty; 16 | RequestState get state => _state; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchTopRatedTvs() async { 22 | _state = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getTopRatedTvs.execute(); 26 | result.fold( 27 | (failure) { 28 | _state = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (tvsData) { 33 | _state = RequestState.loaded; 34 | _tvs = tvsData; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tv/lib/presentation/provider/tv_images_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/media_image.dart'; 5 | import '../../domain/usecases/get_tv_images.dart'; 6 | 7 | class TvImagesNotifier extends ChangeNotifier { 8 | final GetTvImages getTvImages; 9 | 10 | TvImagesNotifier({required this.getTvImages}); 11 | 12 | late MediaImage _tvImages; 13 | MediaImage get tvImages => _tvImages; 14 | 15 | RequestState _tvImagesState = RequestState.empty; 16 | RequestState get tvImagesState => _tvImagesState; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchTvImages(int id) async { 22 | _tvImagesState = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getTvImages.execute(id); 26 | result.fold( 27 | (failure) { 28 | _tvImagesState = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (tvImages) { 33 | _tvImagesState = RequestState.loaded; 34 | _tvImages = tvImages; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tv/lib/presentation/provider/tv_season_episodes_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/tv_season_episode.dart'; 5 | import '../../domain/usecases/get_tv_season_episodes.dart'; 6 | 7 | class TvSeasonEpisodesNotifier extends ChangeNotifier { 8 | final GetTvSeasonEpisodes getTvSeasonEpisodes; 9 | 10 | TvSeasonEpisodesNotifier({required this.getTvSeasonEpisodes}); 11 | 12 | late List _seasonEpisodes; 13 | List get seasonEpisodes => _seasonEpisodes; 14 | 15 | RequestState _seasonEpisodesState = RequestState.empty; 16 | RequestState get seasonEpisodesState => _seasonEpisodesState; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchTvSeasonEpisodes(int id, int seasonNumber) async { 22 | _seasonEpisodesState = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final seasonEpisodesResult = 26 | await getTvSeasonEpisodes.execute(id, seasonNumber); 27 | 28 | seasonEpisodesResult.fold( 29 | (failure) { 30 | _seasonEpisodesState = RequestState.error; 31 | _message = failure.message; 32 | notifyListeners(); 33 | }, 34 | (seasonEpisode) { 35 | _seasonEpisodesState = RequestState.loaded; 36 | _seasonEpisodes = seasonEpisode; 37 | notifyListeners(); 38 | }, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tv/lib/presentation/provider/watchlist_tv_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/state_enum.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../domain/entities/tv.dart'; 5 | import '../../domain/usecases/get_watchlist_tvs.dart'; 6 | 7 | class WatchlistTvNotifier extends ChangeNotifier { 8 | final GetWatchlistTvs getWatchlistTvs; 9 | 10 | WatchlistTvNotifier({required this.getWatchlistTvs}); 11 | 12 | List _watchlistTvs = []; 13 | List get watchlistTvs => _watchlistTvs; 14 | 15 | RequestState _watchlistState = RequestState.empty; 16 | RequestState get watchlistState => _watchlistState; 17 | 18 | String _message = ''; 19 | String get message => _message; 20 | 21 | Future fetchWatchlistTvs() async { 22 | _watchlistState = RequestState.loading; 23 | notifyListeners(); 24 | 25 | final result = await getWatchlistTvs.execute(); 26 | result.fold( 27 | (failure) { 28 | _watchlistState = RequestState.error; 29 | _message = failure.message; 30 | notifyListeners(); 31 | }, 32 | (watchlistTvs) { 33 | _watchlistState = RequestState.loaded; 34 | _watchlistTvs = watchlistTvs; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tv/lib/presentation/widgets/sub_heading.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/styles/text_styles.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SubHeading extends StatelessWidget { 5 | final String? valueKey; 6 | final String text; 7 | final Function() onSeeMoreTapped; 8 | const SubHeading({ 9 | Key? key, 10 | this.valueKey, 11 | required this.text, 12 | required this.onSeeMoreTapped, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | margin: const EdgeInsets.fromLTRB( 19 | 16.0, 20 | 24.0, 21 | 16.0, 22 | 8.0, 23 | ), 24 | child: Row( 25 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 26 | children: [ 27 | Text(text, style: kHeading6), 28 | InkWell( 29 | key: Key(valueKey!), 30 | onTap: onSeeMoreTapped, 31 | child: Padding( 32 | padding: const EdgeInsets.all(8.0), 33 | child: Row( 34 | children: const [ 35 | Text('See More'), 36 | Icon(Icons.arrow_forward_ios, size: 16.0) 37 | ], 38 | ), 39 | ), 40 | ), 41 | ], 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tv/lib/tv.dart: -------------------------------------------------------------------------------- 1 | library tv; 2 | 3 | export 'presentation/pages/main_tv_page.dart'; 4 | export 'presentation/pages/tv_watchlist_page.dart'; 5 | export 'presentation/provider/watchlist_tv_provider.dart'; 6 | -------------------------------------------------------------------------------- /tv/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tv 2 | description: A new Flutter package project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | publish_to: 'none' 7 | 8 | environment: 9 | sdk: ">=2.16.0 <3.0.0" 10 | flutter: ">=1.17.0" 11 | 12 | dependencies: 13 | animate_do: ^2.1.0 14 | cached_network_image: ^3.2.0 15 | carousel_slider: ^4.0.0 16 | core: 17 | path: ../core 18 | dartz: ^0.10.1 19 | equatable: ^2.0.3 20 | flutter: 21 | sdk: flutter 22 | google_fonts: ^2.2.0 23 | intl: ^0.17.0 24 | provider: ^6.0.2 25 | shimmer: ^2.0.0 26 | sqflite: ^2.0.1 27 | 28 | dev_dependencies: 29 | bloc_test: ^9.0.2 30 | build_runner: ^2.1.7 31 | flutter_lints: ^1.0.4 32 | flutter_test: 33 | sdk: flutter 34 | mockito: ^5.0.17 35 | 36 | flutter: 37 | 38 | # To add assets to your package, add an assets section, like this: 39 | # assets: 40 | # - images/a_dot_burr.jpeg 41 | # - images/a_dot_ham.jpeg -------------------------------------------------------------------------------- /tv/test/data/models/tv_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:tv/data/models/tv_model.dart'; 3 | import 'package:tv/domain/entities/tv.dart'; 4 | 5 | void main() { 6 | const tTvModel = TvModel( 7 | backdropPath: '/path.jpg', 8 | firstAirDate: '2022-01-01', 9 | genreIds: [1, 2, 3, 4], 10 | id: 1, 11 | name: 'Name', 12 | overview: 'Overview', 13 | posterPath: '/path.jpg', 14 | voteAverage: 1.0, 15 | voteCount: 1, 16 | ); 17 | 18 | final tTv = Tv( 19 | backdropPath: '/path.jpg', 20 | firstAirDate: '2022-01-01', 21 | genreIds: const [1, 2, 3, 4], 22 | id: 1, 23 | name: 'Name', 24 | overview: 'Overview', 25 | posterPath: '/path.jpg', 26 | voteAverage: 1.0, 27 | voteCount: 1, 28 | ); 29 | 30 | group('to entity', () { 31 | test( 32 | 'should be a subclass of tv entity', 33 | () async { 34 | // act 35 | final result = tTvModel.toEntity(); 36 | 37 | // assert 38 | expect(result, equals(tTv)); 39 | }, 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /tv/test/data/models/tv_response_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:tv/data/models/tv_model.dart'; 5 | import 'package:tv/data/models/tv_response.dart'; 6 | 7 | import '../../helpers/json_reader.dart'; 8 | 9 | void main() { 10 | const tTvModel = TvModel( 11 | backdropPath: '/path.jpg', 12 | firstAirDate: '2022-01-01', 13 | genreIds: [1, 2, 3, 4], 14 | id: 1, 15 | name: 'Name', 16 | overview: 'Overview', 17 | posterPath: '/path.jpg', 18 | voteAverage: 1.0, 19 | voteCount: 1, 20 | ); 21 | 22 | const tTvResponseModel = TvResponse( 23 | tvList: [tTvModel], 24 | ); 25 | 26 | group('from json', () { 27 | test( 28 | 'should return a valid model from json', 29 | () async { 30 | // arrange 31 | final Map jsonMap = json.decode( 32 | readJson('helpers/dummy_responses/tv.json'), 33 | ); 34 | 35 | // act 36 | final result = TvResponse.fromJson(jsonMap); 37 | 38 | // assert 39 | expect(result, equals(tTvResponseModel)); 40 | }, 41 | ); 42 | }); 43 | 44 | group('to json', () { 45 | test( 46 | 'should return a json map containing proper data', 47 | () async { 48 | // act 49 | final result = tTvResponseModel.toJson(); 50 | 51 | // assert 52 | final expectedJsonMap = { 53 | 'results': [ 54 | { 55 | 'backdrop_path': '/path.jpg', 56 | 'first_air_date': '2022-01-01', 57 | 'genre_ids': [1, 2, 3, 4], 58 | 'id': 1, 59 | 'name': 'Name', 60 | 'overview': 'Overview', 61 | 'poster_path': '/path.jpg', 62 | 'vote_average': 1.0, 63 | 'vote_count': 1 64 | } 65 | ], 66 | }; 67 | expect(result, equals(expectedJsonMap)); 68 | }, 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /tv/test/data/models/tv_season_episode_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:tv/data/models/tv_season_episode_model.dart'; 3 | import 'package:tv/domain/entities/tv_season_episode.dart'; 4 | 5 | void main() { 6 | const tTvEpisodeModel = TvSeasonEpisodeModel( 7 | airDate: '2022-01-01', 8 | episodeNumber: 1, 9 | id: 1, 10 | name: 'Name', 11 | overview: 'Overview', 12 | seasonNumber: 1, 13 | stillPath: '/path.jpg', 14 | voteAverage: 1.0, 15 | voteCount: 1, 16 | ); 17 | 18 | const tTvEpisode = TvSeasonEpisode( 19 | airDate: '2022-01-01', 20 | episodeNumber: 1, 21 | id: 1, 22 | name: 'Name', 23 | overview: 'Overview', 24 | seasonNumber: 1, 25 | stillPath: '/path.jpg', 26 | voteAverage: 1.0, 27 | voteCount: 1, 28 | ); 29 | 30 | group('to entity', () { 31 | test( 32 | 'should be a subclass of tv episode entity', 33 | () async { 34 | // act 35 | final result = tTvEpisodeModel.toEntity(); 36 | 37 | // assert 38 | expect(result, equals(tTvEpisode)); 39 | }, 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /tv/test/data/models/tv_season_episodes_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:tv/data/models/tv_season_episode_model.dart'; 5 | import 'package:tv/data/models/tv_season_episode_response.dart'; 6 | 7 | import '../../helpers/json_reader.dart'; 8 | 9 | void main() { 10 | const tTvEpisodeModel = TvSeasonEpisodeModel( 11 | airDate: '2022-01-01', 12 | episodeNumber: 1, 13 | id: 1, 14 | name: 'Name', 15 | overview: 'Overview', 16 | seasonNumber: 1, 17 | stillPath: '/path.jpg', 18 | voteAverage: 1.0, 19 | voteCount: 1, 20 | ); 21 | 22 | const tTvSeasonModel = TvSeasonEpisodeResponse( 23 | tvEpisodes: [tTvEpisodeModel], 24 | ); 25 | 26 | group('from json', () { 27 | test( 28 | 'should return a valid model from json', 29 | () async { 30 | // arrange 31 | final Map jsonMap = json.decode( 32 | readJson('helpers/dummy_responses/tv_season.json'), 33 | ); 34 | 35 | // act 36 | final result = TvSeasonEpisodeResponse.fromJson(jsonMap); 37 | 38 | // assert 39 | expect(result, equals(tTvSeasonModel)); 40 | }, 41 | ); 42 | }); 43 | 44 | group('to json', () { 45 | test( 46 | 'should return a jaon map containing proper data', 47 | () async { 48 | // act 49 | final result = tTvSeasonModel.toJson(); 50 | 51 | // assert 52 | final expectedJsonMap = { 53 | 'episodes': [ 54 | { 55 | 'air_date': '2022-01-01', 56 | 'episode_number': 1, 57 | 'id': 1, 58 | 'name': 'Name', 59 | 'overview': 'Overview', 60 | 'season_number': 1, 61 | 'still_path': '/path.jpg', 62 | 'vote_average': 1.0, 63 | 'vote_count': 1, 64 | }, 65 | ], 66 | }; 67 | expect(result, equals(expectedJsonMap)); 68 | }, 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_on_the_air_tvs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/entities/tv.dart'; 5 | import 'package:tv/domain/usecases/get_on_the_air_tvs.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetOnTheAirTvs usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetOnTheAirTvs(mockTvRepository); 16 | }); 17 | 18 | final tTvs = []; 19 | 20 | test( 21 | 'should get list of tv from the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.getOnTheAirTvs()) 25 | .thenAnswer((_) async => Right(tTvs)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tTvs))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_popular_tvs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/entities/tv.dart'; 5 | import 'package:tv/domain/usecases/get_popular_tvs.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetPopularTvs usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetPopularTvs(mockTvRepository); 16 | }); 17 | 18 | final tTvs = []; 19 | 20 | test( 21 | 'should get list of tv from the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.getPopularTvs()) 25 | .thenAnswer((_) async => Right(tTvs)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tTvs))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_top_rated_tvs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/entities/tv.dart'; 5 | import 'package:tv/domain/usecases/get_top_rated_tvs.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetTopRatedTvs usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetTopRatedTvs(mockTvRepository); 16 | }); 17 | 18 | final tTvs = []; 19 | 20 | test( 21 | 'should get list of tv from the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.getTopRatedTvs()) 25 | .thenAnswer((_) async => Right(tTvs)); 26 | 27 | // act 28 | final result = await usecase.execute(); 29 | 30 | // assert 31 | expect(result, equals(Right(tTvs))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_tv_detail_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/usecases/get_tv_detail.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetTvDetail usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetTvDetail(mockTvRepository); 16 | }); 17 | 18 | const tId = 1; 19 | 20 | test( 21 | 'should get tv detail from the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.getTvDetail(tId)) 25 | .thenAnswer((_) async => const Right(testTvDetail)); 26 | 27 | // act 28 | final result = await usecase.execute(tId); 29 | 30 | // assert 31 | expect(result, equals(const Right(testTvDetail))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_tv_episodes_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/entities/tv_season_episode.dart'; 5 | import 'package:tv/domain/usecases/get_tv_season_episodes.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetTvSeasonEpisodes usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetTvSeasonEpisodes(mockTvRepository); 16 | }); 17 | 18 | const tId = 1; 19 | const tSeasonNumber = 1; 20 | final tTvEpisodes = []; 21 | 22 | test( 23 | 'should get list of tv season episodes from the repository', 24 | () async { 25 | // arrange 26 | when(mockTvRepository.getTvSeasonEpisodes(tId, tSeasonNumber)) 27 | .thenAnswer((_) async => Right(tTvEpisodes)); 28 | 29 | // act 30 | final result = await usecase.execute(tId, tSeasonNumber); 31 | 32 | // assert 33 | expect(result, equals(Right(tTvEpisodes))); 34 | }, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_tv_images_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/usecases/get_tv_images.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetTvImages usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetTvImages(mockTvRepository); 16 | }); 17 | 18 | const tId = 1; 19 | 20 | test( 21 | 'should get tv images from the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.getTvImages(tId)) 25 | .thenAnswer((_) async => const Right(testImages)); 26 | 27 | // act 28 | final result = await usecase.execute(tId); 29 | 30 | // assert 31 | expect(result, equals(const Right(testImages))); 32 | }, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_tv_recommendations_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/entities/tv.dart'; 5 | import 'package:tv/domain/usecases/get_tv_recommendations.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetTvRecommendations usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetTvRecommendations(mockTvRepository); 16 | }); 17 | 18 | const tId = 1; 19 | final tTvs = []; 20 | 21 | test( 22 | 'should get list of tv recommendations from the repository', 23 | () async { 24 | // arrange 25 | when(mockTvRepository.getTvRecommendations(tId)) 26 | .thenAnswer((_) async => Right(tTvs)); 27 | 28 | // act 29 | final result = await usecase.execute(tId); 30 | 31 | // assert 32 | expect(result, equals(Right(tTvs))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_tv_watchlist_status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:tv/domain/usecases/get_tv_watchlist_status.dart'; 4 | 5 | import '../../helpers/test_helper.mocks.dart'; 6 | 7 | void main() { 8 | late MockTvRepository mockTvRepository; 9 | late GetTvWatchlistStatus usecase; 10 | 11 | setUp(() { 12 | mockTvRepository = MockTvRepository(); 13 | usecase = GetTvWatchlistStatus( 14 | tvRepository: mockTvRepository, 15 | ); 16 | }); 17 | 18 | test( 19 | 'should get tv watchlist status from repository', 20 | () async { 21 | // arrange 22 | when(mockTvRepository.isAddedToWatchlist(1)) 23 | .thenAnswer((_) async => true); 24 | 25 | // act 26 | final result = await usecase.execute(1); 27 | 28 | // assert 29 | expect(result, equals(true)); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/get_watchlist_tvs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/usecases/get_watchlist_tvs.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late GetWatchlistTvs usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = GetWatchlistTvs(mockTvRepository); 16 | }); 17 | 18 | test( 19 | 'should get list of tv from the repository', 20 | () async { 21 | // arrange 22 | when(mockTvRepository.getWatchlistTvs()) 23 | .thenAnswer((_) async => Right(testTvList)); 24 | 25 | // act 26 | final result = await usecase.execute(); 27 | 28 | // assert 29 | expect(result, equals(Right(testTvList))); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/remove_watchlist_tv_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/usecases/remove_watchlist_tv.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late RemoveWatchlistTv usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = RemoveWatchlistTv( 16 | tvRepository: mockTvRepository, 17 | ); 18 | }); 19 | 20 | test( 21 | 'should remove a tv from repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.removeWatchlist(testTvDetail)) 25 | .thenAnswer((_) async => const Right('Removed from watchlist')); 26 | 27 | // act 28 | final result = await usecase.execute(testTvDetail); 29 | 30 | // assert 31 | verify(mockTvRepository.removeWatchlist(testTvDetail)); 32 | expect(result, equals(const Right('Removed from watchlist'))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tv/test/domain/usecases/save_watchlist_tv_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:tv/domain/usecases/save_watchlist_tv.dart'; 5 | 6 | import '../../helpers/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MockTvRepository mockTvRepository; 11 | late SaveWatchlistTv usecase; 12 | 13 | setUp(() { 14 | mockTvRepository = MockTvRepository(); 15 | usecase = SaveWatchlistTv( 16 | tvRepository: mockTvRepository, 17 | ); 18 | }); 19 | 20 | test( 21 | 'should save a tv to the repository', 22 | () async { 23 | // arrange 24 | when(mockTvRepository.saveWatchlist(testTvDetail)) 25 | .thenAnswer((_) async => const Right('Added to watchlist')); 26 | 27 | // act 28 | final result = await usecase.execute(testTvDetail); 29 | 30 | // assert 31 | verify(mockTvRepository.saveWatchlist(testTvDetail)); 32 | expect(result, equals(const Right('Added to watchlist'))); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "backdrops": [ 3 | { 4 | "aspect_ratio": 1, 5 | "height": 1000, 6 | "iso_639_1": "en", 7 | "file_path": "/path.jpg", 8 | "vote_average": 1.0, 9 | "vote_count": 1, 10 | "width": 3000 11 | } 12 | ], 13 | "id": 1, 14 | "logos": [ 15 | { 16 | "aspect_ratio": 1, 17 | "height": 100, 18 | "iso_639_1": "en", 19 | "file_path": "/path.png", 20 | "vote_average": 1.0, 21 | "vote_count": 1, 22 | "width": 300 23 | } 24 | ], 25 | "posters": [ 26 | { 27 | "aspect_ratio": 1, 28 | "height": 3000, 29 | "iso_639_1": "en", 30 | "file_path": "/path.jpg", 31 | "vote_average": 1.0, 32 | "vote_count": 1, 33 | "width": 2000 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/search_tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/rkB4LyZHo1NHXFEDHl9vSD9r1lI.jpg", 6 | "first_air_date": "2021-11-06", 7 | "genre_ids": [ 8 | 16, 9 | 10765, 10 | 10759, 11 | 18 12 | ], 13 | "id": 94605, 14 | "name": "Arcane", 15 | "overview": "Amid the stark discord of twin cities Piltover and Zaun, two sisters fight on rival sides of a war between magic technologies and clashing convictions.", 16 | "poster_path": "/fqldf2t8ztc9aiwn3k6mlX3tvRT.jpg", 17 | "vote_average": 9.1, 18 | "vote_count": 1451 19 | } 20 | ], 21 | "total_pages": 1, 22 | "total_results": 2 23 | } -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/path.jpg", 6 | "first_air_date": "2022-01-01", 7 | "genre_ids": [ 8 | 1, 9 | 2, 10 | 3, 11 | 4 12 | ], 13 | "id": 1, 14 | "name": "Name", 15 | "overview": "Overview", 16 | "poster_path": "/path.jpg", 17 | "vote_average": 1.0, 18 | "vote_count": 1 19 | } 20 | ], 21 | "total_pages": 39, 22 | "total_results": 763 23 | } -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/tv_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "backdrop_path": "/path.jpg", 3 | "episode_run_time": [ 4 | 100 5 | ], 6 | "first_air_date": "2022-01-01", 7 | "genres": [ 8 | { 9 | "id": 1, 10 | "name": "Genre 1" 11 | } 12 | ], 13 | "id": 1, 14 | "name": "Name", 15 | "number_of_seasons": 1, 16 | "overview": "Overview", 17 | "poster_path": "/path.jpg", 18 | "vote_average": 1.0, 19 | "vote_count": 1 20 | } -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/tv_recommendations.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "backdrop_path": "/path.jpg", 6 | "genre_ids": [ 7 | 1, 8 | 2, 9 | 3, 10 | 4 11 | ], 12 | "id": 1, 13 | "name": "Name", 14 | "overview": "Overview", 15 | "poster_path": "/path.jpg", 16 | "first_air_date": "2022-01-01", 17 | "vote_average": 1.0, 18 | "vote_count": 1 19 | } 20 | ], 21 | "total_pages": 1, 22 | "total_results": 10 23 | } -------------------------------------------------------------------------------- /tv/test/helpers/dummy_responses/tv_season.json: -------------------------------------------------------------------------------- 1 | { 2 | "episodes":[ 3 | { 4 | "air_date": "2022-01-01", 5 | "episode_number": 1, 6 | "id": 1, 7 | "name": "Name", 8 | "overview": "Overview", 9 | "season_number": 1, 10 | "still_path": "/path.jpg", 11 | "vote_average": 1.0, 12 | "vote_count": 1 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /tv/test/helpers/json_reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | String readJson(String name) { 4 | var dir = Directory.current.path; 5 | if (dir.endsWith('/test')) { 6 | dir = dir.replaceAll('/test', ''); 7 | } 8 | return File('$dir/test/$name').readAsStringSync(); 9 | } 10 | -------------------------------------------------------------------------------- /tv/test/helpers/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart' as http; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:tv/data/datasources/db/tv_database_helper.dart'; 4 | import 'package:tv/data/datasources/tv_local_data_source.dart'; 5 | import 'package:tv/data/datasources/tv_remote_data_source.dart'; 6 | import 'package:tv/domain/repositories/tv_repository.dart'; 7 | 8 | @GenerateMocks([ 9 | TvRepository, 10 | TvRemoteDataSource, 11 | TvLocalDataSource, 12 | TvDatabaseHelper, 13 | ], customMocks: [ 14 | MockSpec(as: #MockHttpClient) 15 | ]) 16 | void main() {} 17 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/popular_tvs_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in tv/test/presentation/provider/popular_tvs_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:tv/domain/entities/tv.dart' as _i7; 11 | import 'package:tv/domain/repositories/tv_repository.dart' as _i2; 12 | import 'package:tv/domain/usecases/get_popular_tvs.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeTvRepository_0 extends _i1.Fake implements _i2.TvRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetPopularTvs]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetPopularTvs extends _i1.Mock implements _i4.GetPopularTvs { 32 | MockGetPopularTvs() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.TvRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeTvRepository_0()) as _i2.TvRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>> execute() => 42 | (super.noSuchMethod(Invocation.method(#execute, []), 43 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>.value( 44 | _FakeEither_1<_i6.Failure, List<_i7.Tv>>())) 45 | as _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>); 46 | } 47 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/top_rated_tvs_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in tv/test/presentation/provider/top_rated_tvs_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:tv/domain/entities/tv.dart' as _i7; 11 | import 'package:tv/domain/repositories/tv_repository.dart' as _i2; 12 | import 'package:tv/domain/usecases/get_top_rated_tvs.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeTvRepository_0 extends _i1.Fake implements _i2.TvRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetTopRatedTvs]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetTopRatedTvs extends _i1.Mock implements _i4.GetTopRatedTvs { 32 | MockGetTopRatedTvs() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.TvRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeTvRepository_0()) as _i2.TvRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>> execute() => 42 | (super.noSuchMethod(Invocation.method(#execute, []), 43 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>.value( 44 | _FakeEither_1<_i6.Failure, List<_i7.Tv>>())) 45 | as _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>); 46 | } 47 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/tv_images_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in tv/test/presentation/provider/tv_images_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:tv/domain/entities/media_image.dart' as _i7; 11 | import 'package:tv/domain/repositories/tv_repository.dart' as _i2; 12 | import 'package:tv/domain/usecases/get_tv_images.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeTvRepository_0 extends _i1.Fake implements _i2.TvRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetTvImages]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetTvImages extends _i1.Mock implements _i4.GetTvImages { 32 | MockGetTvImages() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.TvRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeTvRepository_0()) as _i2.TvRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, _i7.MediaImage>> execute(int? id) => 42 | (super.noSuchMethod(Invocation.method(#execute, [id]), 43 | returnValue: Future<_i3.Either<_i6.Failure, _i7.MediaImage>>.value( 44 | _FakeEither_1<_i6.Failure, _i7.MediaImage>())) as _i5 45 | .Future<_i3.Either<_i6.Failure, _i7.MediaImage>>); 46 | } 47 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/tv_season_episodes_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in tv/test/presentation/provider/tv_season_episodes_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:tv/domain/entities/tv_season_episode.dart' as _i7; 11 | import 'package:tv/domain/repositories/tv_repository.dart' as _i2; 12 | import 'package:tv/domain/usecases/get_tv_season_episodes.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeTvRepository_0 extends _i1.Fake implements _i2.TvRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetTvSeasonEpisodes]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetTvSeasonEpisodes extends _i1.Mock 32 | implements _i4.GetTvSeasonEpisodes { 33 | MockGetTvSeasonEpisodes() { 34 | _i1.throwOnMissingStub(this); 35 | } 36 | 37 | @override 38 | _i2.TvRepository get repository => 39 | (super.noSuchMethod(Invocation.getter(#repository), 40 | returnValue: _FakeTvRepository_0()) as _i2.TvRepository); 41 | @override 42 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.TvSeasonEpisode>>> execute( 43 | dynamic id, dynamic seasonNumber) => 44 | (super.noSuchMethod(Invocation.method(#execute, [id, seasonNumber]), 45 | returnValue: Future< 46 | _i3.Either<_i6.Failure, List<_i7.TvSeasonEpisode>>>.value( 47 | _FakeEither_1<_i6.Failure, List<_i7.TvSeasonEpisode>>())) 48 | as _i5.Future<_i3.Either<_i6.Failure, List<_i7.TvSeasonEpisode>>>); 49 | } 50 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/watchlist_tv_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:core/utils/failure.dart'; 2 | import 'package:core/utils/state_enum.dart'; 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | import 'package:tv/domain/usecases/get_watchlist_tvs.dart'; 8 | import 'package:tv/presentation/provider/watchlist_tv_provider.dart'; 9 | 10 | import '../../helpers/dummy_objects.dart'; 11 | import 'watchlist_tv_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([GetWatchlistTvs]) 14 | void main() { 15 | late int listenerCallCount; 16 | late MockGetWatchlistTvs mockGetWatchlistTvs; 17 | late WatchlistTvNotifier provider; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockGetWatchlistTvs = MockGetWatchlistTvs(); 22 | provider = WatchlistTvNotifier( 23 | getWatchlistTvs: mockGetWatchlistTvs, 24 | )..addListener(() { 25 | listenerCallCount++; 26 | }); 27 | }); 28 | 29 | test( 30 | 'should change tvs when data is gotten successfully', 31 | () async { 32 | // arrange 33 | when(mockGetWatchlistTvs.execute()) 34 | .thenAnswer((_) async => Right([testWatchlistTv])); 35 | 36 | // act 37 | await provider.fetchWatchlistTvs(); 38 | 39 | // assert 40 | expect(provider.watchlistState, equals(RequestState.loaded)); 41 | expect(provider.watchlistTvs, equals([testWatchlistTv])); 42 | expect(listenerCallCount, equals(2)); 43 | }, 44 | ); 45 | 46 | test( 47 | 'should return database failure when error occurred', 48 | () async { 49 | // arrange 50 | when(mockGetWatchlistTvs.execute()).thenAnswer( 51 | (_) async => const Left(DatabaseFailure('Can\'t get data'))); 52 | 53 | // act 54 | await provider.fetchWatchlistTvs(); 55 | 56 | // assert 57 | expect(provider.watchlistState, equals(RequestState.error)); 58 | expect(provider.message, equals('Can\'t get data')); 59 | expect(listenerCallCount, equals(2)); 60 | }, 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /tv/test/presentation/provider/watchlist_tv_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.1.0 from annotations 2 | // in tv/test/presentation/provider/watchlist_tv_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:core/utils/failure.dart' as _i6; 8 | import 'package:dartz/dartz.dart' as _i3; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:tv/domain/entities/tv.dart' as _i7; 11 | import 'package:tv/domain/repositories/tv_repository.dart' as _i2; 12 | import 'package:tv/domain/usecases/get_watchlist_tvs.dart' as _i4; 13 | 14 | // ignore_for_file: type=lint 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: avoid_setters_without_getters 17 | // ignore_for_file: comment_references 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | 24 | class _FakeTvRepository_0 extends _i1.Fake implements _i2.TvRepository {} 25 | 26 | class _FakeEither_1 extends _i1.Fake implements _i3.Either {} 27 | 28 | /// A class which mocks [GetWatchlistTvs]. 29 | /// 30 | /// See the documentation for Mockito's code generation for more information. 31 | class MockGetWatchlistTvs extends _i1.Mock implements _i4.GetWatchlistTvs { 32 | MockGetWatchlistTvs() { 33 | _i1.throwOnMissingStub(this); 34 | } 35 | 36 | @override 37 | _i2.TvRepository get repository => 38 | (super.noSuchMethod(Invocation.getter(#repository), 39 | returnValue: _FakeTvRepository_0()) as _i2.TvRepository); 40 | @override 41 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>> execute() => 42 | (super.noSuchMethod(Invocation.method(#execute, []), 43 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>.value( 44 | _FakeEither_1<_i6.Failure, List<_i7.Tv>>())) 45 | as _i5.Future<_i3.Either<_i6.Failure, List<_i7.Tv>>>); 46 | } 47 | --------------------------------------------------------------------------------