├── .gitignore ├── .metadata ├── README.md ├── 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 ├── assets └── circle-g.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── common │ ├── constants.dart │ ├── exception.dart │ ├── failure.dart │ ├── state_enum.dart │ └── utils.dart ├── data │ ├── datasources │ │ ├── db │ │ │ └── database_helper.dart │ │ ├── movie_local_data_source.dart │ │ └── movie_remote_data_source.dart │ ├── models │ │ ├── genre_model.dart │ │ ├── movie_detail_model.dart │ │ ├── movie_model.dart │ │ ├── movie_response.dart │ │ └── movie_table.dart │ └── repositories │ │ └── movie_repository_impl.dart ├── domain │ ├── entities │ │ ├── genre.dart │ │ ├── movie.dart │ │ └── movie_detail.dart │ ├── repositories │ │ └── movie_repository.dart │ └── usecases │ │ ├── get_movie_detail.dart │ │ ├── get_movie_recommendations.dart │ │ ├── get_now_playing_movies.dart │ │ ├── get_popular_movies.dart │ │ ├── get_top_rated_movies.dart │ │ ├── get_watchlist_movies.dart │ │ ├── get_watchlist_status.dart │ │ ├── remove_watchlist.dart │ │ ├── save_watchlist.dart │ │ └── search_movies.dart ├── injection.dart ├── main.dart └── presentation │ ├── pages │ ├── about_page.dart │ ├── home_movie_page.dart │ ├── movie_detail_page.dart │ ├── popular_movies_page.dart │ ├── search_page.dart │ ├── top_rated_movies_page.dart │ └── watchlist_movies_page.dart │ ├── provider │ ├── movie_detail_notifier.dart │ ├── movie_list_notifier.dart │ ├── movie_search_notifier.dart │ ├── popular_movies_notifier.dart │ ├── top_rated_movies_notifier.dart │ └── watchlist_movie_notifier.dart │ └── widgets │ └── movie_card_list.dart ├── pubspec.lock ├── pubspec.yaml ├── test.sh └── test ├── data ├── datasources │ ├── movie_local_data_source_test.dart │ └── movie_remote_data_source_test.dart ├── models │ ├── movie_model_test.dart │ └── movie_response_model_test.dart └── repositories │ └── movie_repository_impl_test.dart ├── domain └── usecases │ ├── get_movie_detail_test.dart │ ├── get_movie_recommendations_test.dart │ ├── get_now_playing_movies_test.dart │ ├── get_popular_movies_test.dart │ ├── get_top_rated_movies_test.dart │ ├── get_watchlist_movies_test.dart │ ├── get_watchlist_status_test.dart │ ├── remove_watchlist_test.dart │ ├── save_watchlist_test.dart │ └── search_movies_test.dart ├── dummy_data ├── dummy_objects.dart ├── movie_detail.json ├── movie_recommendations.json ├── now_playing.json ├── popular.json ├── search_spiderman_movie.json └── top_rated.json ├── helpers ├── test_helper.dart └── test_helper.mocks.dart ├── json_reader.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_list_notifier_test.dart ├── movie_list_notifier_test.mocks.dart ├── movie_search_notifier_test.dart ├── movie_search_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 /.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: "b0850beeb25f6d5b10426284f506557f66181b36" 8 | channel: "[user-branch]" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b0850beeb25f6d5b10426284f506557f66181b36 17 | base_revision: b0850beeb25f6d5b10426284f506557f66181b36 18 | - platform: android 19 | create_revision: b0850beeb25f6d5b10426284f506557f66181b36 20 | base_revision: b0850beeb25f6d5b10426284f506557f66181b36 21 | - platform: ios 22 | create_revision: b0850beeb25f6d5b10426284f506557f66181b36 23 | base_revision: b0850beeb25f6d5b10426284f506557f66181b36 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # a199-flutter-expert-project 2 | 3 | Repository ini merupakan starter project submission kelas Flutter Expert Dicoding Indonesia. 4 | 5 | --- 6 | 7 | ## Tips Submission Awal 8 | 9 | Pastikan untuk memeriksa kembali seluruh hasil testing pada submissionmu sebelum dikirimkan. Karena kriteria pada submission ini akan diperiksa setelah seluruh berkas testing berhasil dijalankan. 10 | 11 | 12 | ## Tips Submission Akhir 13 | 14 | Jika kamu menerapkan modular pada project, Anda dapat memanfaatkan berkas `test.sh` pada repository ini. Berkas tersebut dapat mempermudah proses testing melalui *terminal* atau *command prompt*. Sebelumnya menjalankan berkas tersebut, ikuti beberapa langkah berikut: 15 | 1. Install terlebih dahulu aplikasi sesuai dengan Operating System (OS) yang Anda gunakan. 16 | - Bagi pengguna **Linux**, jalankan perintah berikut pada terminal. 17 | ``` 18 | sudo apt-get update -qq -y 19 | sudo apt-get install lcov -y 20 | ``` 21 | 22 | - Bagi pengguna **Mac**, jalankan perintah berikut pada terminal. 23 | ``` 24 | brew install lcov 25 | ``` 26 | - Bagi pengguna **Windows**, ikuti langkah berikut. 27 | - Install [Chocolatey](https://chocolatey.org/install) pada komputermu. 28 | - Setelah berhasil, install [lcov](https://community.chocolatey.org/packages/lcov) dengan menjalankan perintah berikut. 29 | ``` 30 | choco install lcov 31 | ``` 32 | - Kemudian cek **Environtment Variabel** pada kolom **System variabels** terdapat variabel GENTHTML dan LCOV_HOME. Jika tidak tersedia, Anda bisa menambahkan variabel baru dengan nilai seperti berikut. 33 | | Variable | Value| 34 | | ----------- | ----------- | 35 | | GENTHTML | C:\ProgramData\chocolatey\lib\lcov\tools\bin\genhtml | 36 | | LCOV_HOME | C:\ProgramData\chocolatey\lib\lcov\tools | 37 | 38 | 2. Untuk mempermudah proses verifikasi testing, jalankan perintah berikut. 39 | ``` 40 | git init 41 | ``` 42 | 3. Kemudian jalankan berkas `test.sh` dengan perintah berikut pada *terminal* atau *powershell*. 43 | ``` 44 | test.sh 45 | ``` 46 | atau 47 | ``` 48 | ./test.sh 49 | ``` 50 | Proses ini akan men-*generate* berkas `lcov.info` dan folder `coverage` terkait dengan laporan coverage. 51 | 4. Tunggu proses testing selesai hingga muncul web terkait laporan coverage. 52 | 53 | -------------------------------------------------------------------------------- /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 # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file("local.properties") 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader("UTF-8") { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | def flutterVersionCode = localProperties.getProperty("flutter.versionCode") 17 | if (flutterVersionCode == null) { 18 | flutterVersionCode = "1" 19 | } 20 | 21 | def flutterVersionName = localProperties.getProperty("flutter.versionName") 22 | if (flutterVersionName == null) { 23 | flutterVersionName = "1.0" 24 | } 25 | 26 | android { 27 | namespace = "com.dicoding.ditonton" 28 | compileSdk = flutter.compileSdkVersion 29 | ndkVersion = flutter.ndkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId = "com.dicoding.ditonton" 39 | // You can update the following values to match your application needs. 40 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 41 | minSdk = flutter.minSdkVersion 42 | targetSdk = flutter.targetSdkVersion 43 | versionCode = flutterVersionCode.toInteger() 44 | versionName = flutterVersionName 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig = signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source = "../.." 58 | } 59 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.3.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/circle-g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/assets/circle-g.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - sqflite (0.0.3): 7 | - Flutter 8 | - FlutterMacOS 9 | 10 | DEPENDENCIES: 11 | - Flutter (from `Flutter`) 12 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 13 | - sqflite (from `.symlinks/plugins/sqflite/darwin`) 14 | 15 | EXTERNAL SOURCES: 16 | Flutter: 17 | :path: Flutter 18 | path_provider_foundation: 19 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 20 | sqflite: 21 | :path: ".symlinks/plugins/sqflite/darwin" 22 | 23 | SPEC CHECKSUMS: 24 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 25 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 26 | sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec 27 | 28 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 29 | 30 | COCOAPODS: 1.15.2 31 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/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/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dicodingacademy/a199-flutter-expert-project/5c59ea3c2c7e02aba8f309c39afb1c03753b8ac4/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Ditonton 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ditonton 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | const String BASE_IMAGE_URL = 'https://image.tmdb.org/t/p/w500'; 5 | 6 | // colors 7 | const Color kRichBlack = Color(0xFF000814); 8 | const Color kOxfordBlue = Color(0xFF001D3D); 9 | const Color kPrussianBlue = Color(0xFF003566); 10 | const Color kMikadoYellow = Color(0xFFffc300); 11 | const Color kDavysGrey = Color(0xFF4B5358); 12 | const Color kGrey = Color(0xFF303030); 13 | 14 | // text style 15 | final TextStyle kHeading5 = 16 | GoogleFonts.poppins(fontSize: 23, fontWeight: FontWeight.w400); 17 | final TextStyle kHeading6 = GoogleFonts.poppins( 18 | fontSize: 19, fontWeight: FontWeight.w500, letterSpacing: 0.15); 19 | final TextStyle kSubtitle = GoogleFonts.poppins( 20 | fontSize: 15, fontWeight: FontWeight.w400, letterSpacing: 0.15); 21 | final TextStyle kBodyText = GoogleFonts.poppins( 22 | fontSize: 13, fontWeight: FontWeight.w400, letterSpacing: 0.25); 23 | 24 | // text theme 25 | final kTextTheme = TextTheme( 26 | headlineMedium: kHeading5, 27 | headlineSmall: kHeading6, 28 | labelMedium: kSubtitle, 29 | bodyMedium: kBodyText, 30 | ); 31 | 32 | final kDrawerTheme = DrawerThemeData( 33 | backgroundColor: Colors.grey.shade700, 34 | ); 35 | 36 | const kColorScheme = ColorScheme( 37 | primary: kMikadoYellow, 38 | secondary: kPrussianBlue, 39 | secondaryContainer: kPrussianBlue, 40 | surface: kRichBlack, 41 | error: Colors.red, 42 | onPrimary: kRichBlack, 43 | onSecondary: Colors.white, 44 | onSurface: Colors.white, 45 | onError: Colors.white, 46 | brightness: Brightness.dark, 47 | ); 48 | -------------------------------------------------------------------------------- /lib/common/exception.dart: -------------------------------------------------------------------------------- 1 | class ServerException implements Exception {} 2 | 3 | class DatabaseException implements Exception { 4 | final String message; 5 | 6 | DatabaseException(this.message); 7 | } 8 | -------------------------------------------------------------------------------- /lib/common/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | abstract class Failure extends Equatable { 4 | final String message; 5 | 6 | Failure(this.message); 7 | 8 | @override 9 | List get props => [message]; 10 | } 11 | 12 | class ServerFailure extends Failure { 13 | ServerFailure(String message) : super(message); 14 | } 15 | 16 | class ConnectionFailure extends Failure { 17 | ConnectionFailure(String message) : super(message); 18 | } 19 | 20 | class DatabaseFailure extends Failure { 21 | DatabaseFailure(String message) : super(message); 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/state_enum.dart: -------------------------------------------------------------------------------- 1 | enum RequestState { Empty, Loading, Loaded, Error } 2 | -------------------------------------------------------------------------------- /lib/common/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | final RouteObserver routeObserver = RouteObserver(); 4 | -------------------------------------------------------------------------------- /lib/data/datasources/db/database_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:ditonton/data/models/movie_table.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | 6 | class DatabaseHelper { 7 | static DatabaseHelper? _databaseHelper; 8 | DatabaseHelper._instance() { 9 | _databaseHelper = this; 10 | } 11 | 12 | factory DatabaseHelper() => _databaseHelper ?? DatabaseHelper._instance(); 13 | 14 | static Database? _database; 15 | 16 | Future get database async { 17 | if (_database == null) { 18 | _database = await _initDb(); 19 | } 20 | return _database; 21 | } 22 | 23 | static const String _tblWatchlist = 'watchlist'; 24 | 25 | Future _initDb() async { 26 | final path = await getDatabasesPath(); 27 | final databasePath = '$path/ditonton.db'; 28 | 29 | var db = await openDatabase(databasePath, version: 1, onCreate: _onCreate); 30 | return db; 31 | } 32 | 33 | void _onCreate(Database db, int version) async { 34 | await db.execute(''' 35 | CREATE TABLE $_tblWatchlist ( 36 | id INTEGER PRIMARY KEY, 37 | title TEXT, 38 | overview TEXT, 39 | posterPath TEXT 40 | ); 41 | '''); 42 | } 43 | 44 | Future insertWatchlist(MovieTable movie) async { 45 | final db = await database; 46 | return await db!.insert(_tblWatchlist, movie.toJson()); 47 | } 48 | 49 | Future removeWatchlist(MovieTable movie) async { 50 | final db = await database; 51 | return await db!.delete( 52 | _tblWatchlist, 53 | where: 'id = ?', 54 | whereArgs: [movie.id], 55 | ); 56 | } 57 | 58 | Future?> getMovieById(int id) async { 59 | final db = await database; 60 | final results = await db!.query( 61 | _tblWatchlist, 62 | where: 'id = ?', 63 | whereArgs: [id], 64 | ); 65 | 66 | if (results.isNotEmpty) { 67 | return results.first; 68 | } else { 69 | return null; 70 | } 71 | } 72 | 73 | Future>> getWatchlistMovies() async { 74 | final db = await database; 75 | final List> results = await db!.query(_tblWatchlist); 76 | 77 | return results; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/data/datasources/movie_local_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/exception.dart'; 2 | import 'package:ditonton/data/datasources/db/database_helper.dart'; 3 | import 'package:ditonton/data/models/movie_table.dart'; 4 | 5 | abstract class MovieLocalDataSource { 6 | Future insertWatchlist(MovieTable movie); 7 | Future removeWatchlist(MovieTable movie); 8 | Future getMovieById(int id); 9 | Future> getWatchlistMovies(); 10 | } 11 | 12 | class MovieLocalDataSourceImpl implements MovieLocalDataSource { 13 | final DatabaseHelper databaseHelper; 14 | 15 | MovieLocalDataSourceImpl({required this.databaseHelper}); 16 | 17 | @override 18 | Future insertWatchlist(MovieTable movie) async { 19 | try { 20 | await databaseHelper.insertWatchlist(movie); 21 | return 'Added to Watchlist'; 22 | } catch (e) { 23 | throw DatabaseException(e.toString()); 24 | } 25 | } 26 | 27 | @override 28 | Future removeWatchlist(MovieTable movie) async { 29 | try { 30 | await databaseHelper.removeWatchlist(movie); 31 | return 'Removed from Watchlist'; 32 | } catch (e) { 33 | throw DatabaseException(e.toString()); 34 | } 35 | } 36 | 37 | @override 38 | Future getMovieById(int id) async { 39 | final result = await databaseHelper.getMovieById(id); 40 | if (result != null) { 41 | return MovieTable.fromMap(result); 42 | } else { 43 | return null; 44 | } 45 | } 46 | 47 | @override 48 | Future> getWatchlistMovies() async { 49 | final result = await databaseHelper.getWatchlistMovies(); 50 | return result.map((data) => MovieTable.fromMap(data)).toList(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/data/datasources/movie_remote_data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ditonton/data/models/movie_detail_model.dart'; 4 | import 'package:ditonton/data/models/movie_model.dart'; 5 | import 'package:ditonton/data/models/movie_response.dart'; 6 | import 'package:ditonton/common/exception.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | abstract class MovieRemoteDataSource { 10 | Future> getNowPlayingMovies(); 11 | Future> getPopularMovies(); 12 | Future> getTopRatedMovies(); 13 | Future getMovieDetail(int id); 14 | Future> getMovieRecommendations(int id); 15 | Future> searchMovies(String query); 16 | } 17 | 18 | class MovieRemoteDataSourceImpl implements MovieRemoteDataSource { 19 | static const API_KEY = 'api_key=2174d146bb9c0eab47529b2e77d6b526'; 20 | static const BASE_URL = 'https://api.themoviedb.org/3'; 21 | 22 | final http.Client client; 23 | 24 | MovieRemoteDataSourceImpl({required this.client}); 25 | 26 | @override 27 | Future> getNowPlayingMovies() async { 28 | final response = 29 | await client.get(Uri.parse('$BASE_URL/movie/now_playing?$API_KEY')); 30 | 31 | if (response.statusCode == 200) { 32 | return MovieResponse.fromJson(json.decode(response.body)).movieList; 33 | } else { 34 | throw ServerException(); 35 | } 36 | } 37 | 38 | @override 39 | Future getMovieDetail(int id) async { 40 | final response = 41 | await client.get(Uri.parse('$BASE_URL/movie/$id?$API_KEY')); 42 | 43 | if (response.statusCode == 200) { 44 | return MovieDetailResponse.fromJson(json.decode(response.body)); 45 | } else { 46 | throw ServerException(); 47 | } 48 | } 49 | 50 | @override 51 | Future> getMovieRecommendations(int id) async { 52 | final response = await client 53 | .get(Uri.parse('$BASE_URL/movie/$id/recommendations?$API_KEY')); 54 | 55 | if (response.statusCode == 200) { 56 | return MovieResponse.fromJson(json.decode(response.body)).movieList; 57 | } else { 58 | throw ServerException(); 59 | } 60 | } 61 | 62 | @override 63 | Future> getPopularMovies() async { 64 | final response = 65 | await client.get(Uri.parse('$BASE_URL/movie/popular?$API_KEY')); 66 | 67 | if (response.statusCode == 200) { 68 | return MovieResponse.fromJson(json.decode(response.body)).movieList; 69 | } else { 70 | throw ServerException(); 71 | } 72 | } 73 | 74 | @override 75 | Future> getTopRatedMovies() async { 76 | final response = 77 | await client.get(Uri.parse('$BASE_URL/movie/top_rated?$API_KEY')); 78 | 79 | if (response.statusCode == 200) { 80 | return MovieResponse.fromJson(json.decode(response.body)).movieList; 81 | } else { 82 | throw ServerException(); 83 | } 84 | } 85 | 86 | @override 87 | Future> searchMovies(String query) async { 88 | final response = await client 89 | .get(Uri.parse('$BASE_URL/search/movie?$API_KEY&query=$query')); 90 | 91 | if (response.statusCode == 200) { 92 | return MovieResponse.fromJson(json.decode(response.body)).movieList; 93 | } else { 94 | throw ServerException(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/data/models/genre_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/genre.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class GenreModel extends Equatable { 5 | GenreModel({ 6 | required this.id, 7 | required this.name, 8 | }); 9 | 10 | final int id; 11 | final String name; 12 | 13 | factory GenreModel.fromJson(Map json) => GenreModel( 14 | id: json["id"], 15 | name: json["name"], 16 | ); 17 | 18 | Map toJson() => { 19 | "id": id, 20 | "name": name, 21 | }; 22 | 23 | Genre toEntity() { 24 | return Genre(id: this.id, name: this.name); 25 | } 26 | 27 | @override 28 | List get props => [id, name]; 29 | } 30 | -------------------------------------------------------------------------------- /lib/data/models/movie_detail_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/models/genre_model.dart'; 2 | import 'package:ditonton/domain/entities/movie_detail.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | 5 | class MovieDetailResponse extends Equatable { 6 | MovieDetailResponse({ 7 | required this.adult, 8 | required this.backdropPath, 9 | required this.budget, 10 | required this.genres, 11 | required this.homepage, 12 | required this.id, 13 | required this.imdbId, 14 | required this.originalLanguage, 15 | required this.originalTitle, 16 | required this.overview, 17 | required this.popularity, 18 | required this.posterPath, 19 | required this.releaseDate, 20 | required this.revenue, 21 | required this.runtime, 22 | required this.status, 23 | required this.tagline, 24 | required this.title, 25 | required this.video, 26 | required this.voteAverage, 27 | required this.voteCount, 28 | }); 29 | 30 | final bool adult; 31 | final String? backdropPath; 32 | final int budget; 33 | final List genres; 34 | final String homepage; 35 | final int id; 36 | final String? imdbId; 37 | final String originalLanguage; 38 | final String originalTitle; 39 | final String overview; 40 | final double popularity; 41 | final String posterPath; 42 | final String releaseDate; 43 | final int revenue; 44 | final int runtime; 45 | final String status; 46 | final String tagline; 47 | final String title; 48 | final bool video; 49 | final double voteAverage; 50 | final int voteCount; 51 | 52 | factory MovieDetailResponse.fromJson(Map json) => 53 | MovieDetailResponse( 54 | adult: json["adult"], 55 | backdropPath: json["backdrop_path"], 56 | budget: json["budget"], 57 | genres: List.from( 58 | json["genres"].map((x) => GenreModel.fromJson(x))), 59 | homepage: json["homepage"], 60 | id: json["id"], 61 | imdbId: json["imdb_id"], 62 | originalLanguage: json["original_language"], 63 | originalTitle: json["original_title"], 64 | overview: json["overview"], 65 | popularity: json["popularity"].toDouble(), 66 | posterPath: json["poster_path"], 67 | releaseDate: json["release_date"], 68 | revenue: json["revenue"], 69 | runtime: json["runtime"], 70 | status: json["status"], 71 | tagline: json["tagline"], 72 | title: json["title"], 73 | video: json["video"], 74 | voteAverage: json["vote_average"].toDouble(), 75 | voteCount: json["vote_count"], 76 | ); 77 | 78 | Map toJson() => { 79 | "adult": adult, 80 | "backdrop_path": backdropPath, 81 | "budget": budget, 82 | "genres": List.from(genres.map((x) => x.toJson())), 83 | "homepage": homepage, 84 | "id": id, 85 | "imdb_id": imdbId, 86 | "original_language": originalLanguage, 87 | "original_title": originalTitle, 88 | "overview": overview, 89 | "popularity": popularity, 90 | "poster_path": posterPath, 91 | "release_date": releaseDate, 92 | "revenue": revenue, 93 | "runtime": runtime, 94 | "status": status, 95 | "tagline": tagline, 96 | "title": title, 97 | "video": video, 98 | "vote_average": voteAverage, 99 | "vote_count": voteCount, 100 | }; 101 | 102 | MovieDetail toEntity() { 103 | return MovieDetail( 104 | adult: this.adult, 105 | backdropPath: this.backdropPath, 106 | genres: this.genres.map((genre) => genre.toEntity()).toList(), 107 | id: this.id, 108 | originalTitle: this.originalTitle, 109 | overview: this.overview, 110 | posterPath: this.posterPath, 111 | releaseDate: this.releaseDate, 112 | runtime: this.runtime, 113 | title: this.title, 114 | voteAverage: this.voteAverage, 115 | voteCount: this.voteCount, 116 | ); 117 | } 118 | 119 | @override 120 | // TODO: implement props 121 | List get props => [ 122 | adult, 123 | backdropPath, 124 | budget, 125 | genres, 126 | homepage, 127 | id, 128 | imdbId, 129 | originalLanguage, 130 | originalTitle, 131 | overview, 132 | popularity, 133 | posterPath, 134 | releaseDate, 135 | revenue, 136 | runtime, 137 | status, 138 | tagline, 139 | title, 140 | video, 141 | voteAverage, 142 | voteCount, 143 | ]; 144 | } 145 | -------------------------------------------------------------------------------- /lib/data/models/movie_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/movie.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class MovieModel extends Equatable { 5 | MovieModel({ 6 | required this.adult, 7 | required this.backdropPath, 8 | required this.genreIds, 9 | required this.id, 10 | required this.originalTitle, 11 | required this.overview, 12 | required this.popularity, 13 | required this.posterPath, 14 | required this.releaseDate, 15 | required this.title, 16 | required this.video, 17 | required this.voteAverage, 18 | required this.voteCount, 19 | }); 20 | 21 | final bool adult; 22 | final String? backdropPath; 23 | final List genreIds; 24 | final int id; 25 | final String originalTitle; 26 | final String overview; 27 | final double popularity; 28 | final String? posterPath; 29 | final String? releaseDate; 30 | final String title; 31 | final bool video; 32 | final double voteAverage; 33 | final int voteCount; 34 | 35 | factory MovieModel.fromJson(Map json) => MovieModel( 36 | adult: json["adult"], 37 | backdropPath: json["backdrop_path"], 38 | genreIds: List.from(json["genre_ids"].map((x) => x)), 39 | id: json["id"], 40 | originalTitle: json["original_title"], 41 | overview: json["overview"], 42 | popularity: json["popularity"].toDouble(), 43 | posterPath: json["poster_path"], 44 | releaseDate: json["release_date"], 45 | title: json["title"], 46 | video: json["video"], 47 | voteAverage: json["vote_average"].toDouble(), 48 | voteCount: json["vote_count"], 49 | ); 50 | 51 | Map toJson() => { 52 | "adult": adult, 53 | "backdrop_path": backdropPath, 54 | "genre_ids": List.from(genreIds.map((x) => x)), 55 | "id": id, 56 | "original_title": originalTitle, 57 | "overview": overview, 58 | "popularity": popularity, 59 | "poster_path": posterPath, 60 | "release_date": releaseDate, 61 | "title": title, 62 | "video": video, 63 | "vote_average": voteAverage, 64 | "vote_count": voteCount, 65 | }; 66 | 67 | Movie toEntity() { 68 | return Movie( 69 | adult: this.adult, 70 | backdropPath: this.backdropPath, 71 | genreIds: this.genreIds, 72 | id: this.id, 73 | originalTitle: this.originalTitle, 74 | overview: this.overview, 75 | popularity: this.popularity, 76 | posterPath: this.posterPath, 77 | releaseDate: this.releaseDate, 78 | title: this.title, 79 | video: this.video, 80 | voteAverage: this.voteAverage, 81 | voteCount: this.voteCount, 82 | ); 83 | } 84 | 85 | @override 86 | List get props => [ 87 | adult, 88 | backdropPath, 89 | genreIds, 90 | id, 91 | originalTitle, 92 | overview, 93 | popularity, 94 | posterPath, 95 | releaseDate, 96 | title, 97 | video, 98 | voteAverage, 99 | voteCount, 100 | ]; 101 | } 102 | -------------------------------------------------------------------------------- /lib/data/models/movie_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/models/movie_model.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class MovieResponse extends Equatable { 5 | final List movieList; 6 | 7 | MovieResponse({required this.movieList}); 8 | 9 | factory MovieResponse.fromJson(Map json) => MovieResponse( 10 | movieList: List.from((json["results"] as List) 11 | .map((x) => MovieModel.fromJson(x)) 12 | .where((element) => element.posterPath != null)), 13 | ); 14 | 15 | Map toJson() => { 16 | "results": List.from(movieList.map((x) => x.toJson())), 17 | }; 18 | 19 | @override 20 | List get props => [movieList]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/data/models/movie_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/movie.dart'; 2 | import 'package:ditonton/domain/entities/movie_detail.dart'; 3 | import 'package:equatable/equatable.dart'; 4 | 5 | class MovieTable extends Equatable { 6 | final int id; 7 | final String? title; 8 | final String? posterPath; 9 | final String? overview; 10 | 11 | MovieTable({ 12 | required this.id, 13 | required this.title, 14 | required this.posterPath, 15 | required this.overview, 16 | }); 17 | 18 | factory MovieTable.fromEntity(MovieDetail movie) => MovieTable( 19 | id: movie.id, 20 | title: movie.title, 21 | posterPath: movie.posterPath, 22 | overview: movie.overview, 23 | ); 24 | 25 | factory MovieTable.fromMap(Map map) => MovieTable( 26 | id: map['id'], 27 | title: map['title'], 28 | posterPath: map['posterPath'], 29 | overview: map['overview'], 30 | ); 31 | 32 | Map toJson() => { 33 | 'id': id, 34 | 'title': title, 35 | 'posterPath': posterPath, 36 | 'overview': overview, 37 | }; 38 | 39 | Movie toEntity() => Movie.watchlist( 40 | id: id, 41 | overview: overview, 42 | posterPath: posterPath, 43 | title: title, 44 | ); 45 | 46 | @override 47 | // TODO: implement props 48 | List get props => [id, title, posterPath, overview]; 49 | } 50 | -------------------------------------------------------------------------------- /lib/data/repositories/movie_repository_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:ditonton/data/datasources/movie_local_data_source.dart'; 5 | import 'package:ditonton/data/datasources/movie_remote_data_source.dart'; 6 | import 'package:ditonton/data/models/movie_table.dart'; 7 | import 'package:ditonton/domain/entities/movie.dart'; 8 | import 'package:ditonton/domain/entities/movie_detail.dart'; 9 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 10 | import 'package:ditonton/common/exception.dart'; 11 | import 'package:ditonton/common/failure.dart'; 12 | 13 | class MovieRepositoryImpl implements MovieRepository { 14 | final MovieRemoteDataSource remoteDataSource; 15 | final MovieLocalDataSource localDataSource; 16 | 17 | MovieRepositoryImpl({ 18 | required this.remoteDataSource, 19 | required this.localDataSource, 20 | }); 21 | 22 | @override 23 | Future>> getNowPlayingMovies() async { 24 | try { 25 | final result = await remoteDataSource.getNowPlayingMovies(); 26 | return Right(result.map((model) => model.toEntity()).toList()); 27 | } on ServerException { 28 | return Left(ServerFailure('')); 29 | } on SocketException { 30 | return Left(ConnectionFailure('Failed to connect to the network')); 31 | } 32 | } 33 | 34 | @override 35 | Future> getMovieDetail(int id) async { 36 | try { 37 | final result = await remoteDataSource.getMovieDetail(id); 38 | return Right(result.toEntity()); 39 | } on ServerException { 40 | return Left(ServerFailure('')); 41 | } on SocketException { 42 | return Left(ConnectionFailure('Failed to connect to the network')); 43 | } 44 | } 45 | 46 | @override 47 | Future>> getMovieRecommendations(int id) async { 48 | try { 49 | final result = await remoteDataSource.getMovieRecommendations(id); 50 | return Right(result.map((model) => model.toEntity()).toList()); 51 | } on ServerException { 52 | return Left(ServerFailure('')); 53 | } on SocketException { 54 | return Left(ConnectionFailure('Failed to connect to the network')); 55 | } 56 | } 57 | 58 | @override 59 | Future>> getPopularMovies() async { 60 | try { 61 | final result = await remoteDataSource.getPopularMovies(); 62 | return Right(result.map((model) => model.toEntity()).toList()); 63 | } on ServerException { 64 | return Left(ServerFailure('')); 65 | } on SocketException { 66 | return Left(ConnectionFailure('Failed to connect to the network')); 67 | } 68 | } 69 | 70 | @override 71 | Future>> getTopRatedMovies() async { 72 | try { 73 | final result = await remoteDataSource.getTopRatedMovies(); 74 | return Right(result.map((model) => model.toEntity()).toList()); 75 | } on ServerException { 76 | return Left(ServerFailure('')); 77 | } on SocketException { 78 | return Left(ConnectionFailure('Failed to connect to the network')); 79 | } 80 | } 81 | 82 | @override 83 | Future>> searchMovies(String query) async { 84 | try { 85 | final result = await remoteDataSource.searchMovies(query); 86 | return Right(result.map((model) => model.toEntity()).toList()); 87 | } on ServerException { 88 | return Left(ServerFailure('')); 89 | } on SocketException { 90 | return Left(ConnectionFailure('Failed to connect to the network')); 91 | } 92 | } 93 | 94 | @override 95 | Future> saveWatchlist(MovieDetail movie) async { 96 | try { 97 | final result = 98 | await localDataSource.insertWatchlist(MovieTable.fromEntity(movie)); 99 | return Right(result); 100 | } on DatabaseException catch (e) { 101 | return Left(DatabaseFailure(e.message)); 102 | } catch (e) { 103 | throw e; 104 | } 105 | } 106 | 107 | @override 108 | Future> removeWatchlist(MovieDetail movie) async { 109 | try { 110 | final result = 111 | await localDataSource.removeWatchlist(MovieTable.fromEntity(movie)); 112 | return Right(result); 113 | } on DatabaseException catch (e) { 114 | return Left(DatabaseFailure(e.message)); 115 | } 116 | } 117 | 118 | @override 119 | Future isAddedToWatchlist(int id) async { 120 | final result = await localDataSource.getMovieById(id); 121 | return result != null; 122 | } 123 | 124 | @override 125 | Future>> getWatchlistMovies() async { 126 | final result = await localDataSource.getWatchlistMovies(); 127 | return Right(result.map((data) => data.toEntity()).toList()); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/domain/entities/genre.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Genre extends Equatable { 4 | Genre({ 5 | required this.id, 6 | required this.name, 7 | }); 8 | 9 | final int id; 10 | final String name; 11 | 12 | @override 13 | List get props => [id, name]; 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/entities/movie.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Movie extends Equatable { 4 | Movie({ 5 | required this.adult, 6 | required this.backdropPath, 7 | required this.genreIds, 8 | required this.id, 9 | required this.originalTitle, 10 | required this.overview, 11 | required this.popularity, 12 | required this.posterPath, 13 | required this.releaseDate, 14 | required this.title, 15 | required this.video, 16 | required this.voteAverage, 17 | required this.voteCount, 18 | }); 19 | 20 | Movie.watchlist({ 21 | required this.id, 22 | required this.overview, 23 | required this.posterPath, 24 | required this.title, 25 | }); 26 | 27 | bool? adult; 28 | String? backdropPath; 29 | List? genreIds; 30 | int id; 31 | String? originalTitle; 32 | String? overview; 33 | double? popularity; 34 | String? posterPath; 35 | String? releaseDate; 36 | String? title; 37 | bool? video; 38 | double? voteAverage; 39 | int? voteCount; 40 | 41 | @override 42 | List get props => [ 43 | adult, 44 | backdropPath, 45 | genreIds, 46 | id, 47 | originalTitle, 48 | overview, 49 | popularity, 50 | posterPath, 51 | releaseDate, 52 | title, 53 | video, 54 | voteAverage, 55 | voteCount, 56 | ]; 57 | } 58 | -------------------------------------------------------------------------------- /lib/domain/entities/movie_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/genre.dart'; 2 | import 'package:equatable/equatable.dart'; 3 | 4 | class MovieDetail extends Equatable { 5 | MovieDetail({ 6 | required this.adult, 7 | required this.backdropPath, 8 | required this.genres, 9 | required this.id, 10 | required this.originalTitle, 11 | required this.overview, 12 | required this.posterPath, 13 | required this.releaseDate, 14 | required this.runtime, 15 | required this.title, 16 | required this.voteAverage, 17 | required this.voteCount, 18 | }); 19 | 20 | final bool adult; 21 | final String? backdropPath; 22 | final List genres; 23 | final int id; 24 | final String originalTitle; 25 | final String overview; 26 | final String posterPath; 27 | final String releaseDate; 28 | final int runtime; 29 | final String title; 30 | final double voteAverage; 31 | final int voteCount; 32 | 33 | @override 34 | List get props => [ 35 | adult, 36 | backdropPath, 37 | genres, 38 | id, 39 | originalTitle, 40 | overview, 41 | posterPath, 42 | releaseDate, 43 | title, 44 | voteAverage, 45 | voteCount, 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /lib/domain/repositories/movie_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/entities/movie_detail.dart'; 4 | import 'package:ditonton/common/failure.dart'; 5 | 6 | abstract class MovieRepository { 7 | Future>> getNowPlayingMovies(); 8 | Future>> getPopularMovies(); 9 | Future>> getTopRatedMovies(); 10 | Future> getMovieDetail(int id); 11 | Future>> getMovieRecommendations(int id); 12 | Future>> searchMovies(String query); 13 | Future> saveWatchlist(MovieDetail movie); 14 | Future> removeWatchlist(MovieDetail movie); 15 | Future isAddedToWatchlist(int id); 16 | Future>> getWatchlistMovies(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_movie_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie_detail.dart'; 3 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 4 | import 'package:ditonton/common/failure.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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_movie_recommendations.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 4 | import 'package:ditonton/common/failure.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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_now_playing_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 4 | import 'package:ditonton/common/failure.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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_popular_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/domain/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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_top_rated_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/domain/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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_watchlist_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 4 | import 'package:ditonton/common/failure.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 | -------------------------------------------------------------------------------- /lib/domain/usecases/get_watchlist_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 2 | 3 | class GetWatchListStatus { 4 | final MovieRepository repository; 5 | 6 | GetWatchListStatus(this.repository); 7 | 8 | Future execute(int id) async { 9 | return repository.isAddedToWatchlist(id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/domain/usecases/remove_watchlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/domain/entities/movie_detail.dart'; 4 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 5 | 6 | class RemoveWatchlist { 7 | final MovieRepository repository; 8 | 9 | RemoveWatchlist(this.repository); 10 | 11 | Future> execute(MovieDetail movie) { 12 | return repository.removeWatchlist(movie); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/save_watchlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/domain/entities/movie_detail.dart'; 4 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 5 | 6 | class SaveWatchlist { 7 | final MovieRepository repository; 8 | 9 | SaveWatchlist(this.repository); 10 | 11 | Future> execute(MovieDetail movie) { 12 | return repository.saveWatchlist(movie); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/usecases/search_movies.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/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 | -------------------------------------------------------------------------------- /lib/injection.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/datasources/db/database_helper.dart'; 2 | import 'package:ditonton/data/datasources/movie_local_data_source.dart'; 3 | import 'package:ditonton/data/datasources/movie_remote_data_source.dart'; 4 | import 'package:ditonton/data/repositories/movie_repository_impl.dart'; 5 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 6 | import 'package:ditonton/domain/usecases/get_movie_detail.dart'; 7 | import 'package:ditonton/domain/usecases/get_movie_recommendations.dart'; 8 | import 'package:ditonton/domain/usecases/get_now_playing_movies.dart'; 9 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 10 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 11 | import 'package:ditonton/domain/usecases/get_watchlist_movies.dart'; 12 | import 'package:ditonton/domain/usecases/get_watchlist_status.dart'; 13 | import 'package:ditonton/domain/usecases/remove_watchlist.dart'; 14 | import 'package:ditonton/domain/usecases/save_watchlist.dart'; 15 | import 'package:ditonton/domain/usecases/search_movies.dart'; 16 | import 'package:ditonton/presentation/provider/movie_detail_notifier.dart'; 17 | import 'package:ditonton/presentation/provider/movie_list_notifier.dart'; 18 | import 'package:ditonton/presentation/provider/movie_search_notifier.dart'; 19 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart'; 20 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart'; 21 | import 'package:ditonton/presentation/provider/watchlist_movie_notifier.dart'; 22 | import 'package:http/http.dart' as http; 23 | import 'package:get_it/get_it.dart'; 24 | 25 | final locator = GetIt.instance; 26 | 27 | void init() { 28 | // provider 29 | locator.registerFactory( 30 | () => MovieListNotifier( 31 | getNowPlayingMovies: locator(), 32 | getPopularMovies: locator(), 33 | getTopRatedMovies: locator(), 34 | ), 35 | ); 36 | locator.registerFactory( 37 | () => MovieDetailNotifier( 38 | getMovieDetail: locator(), 39 | getMovieRecommendations: locator(), 40 | getWatchListStatus: locator(), 41 | saveWatchlist: locator(), 42 | removeWatchlist: locator(), 43 | ), 44 | ); 45 | locator.registerFactory( 46 | () => MovieSearchNotifier( 47 | searchMovies: locator(), 48 | ), 49 | ); 50 | locator.registerFactory( 51 | () => PopularMoviesNotifier( 52 | locator(), 53 | ), 54 | ); 55 | locator.registerFactory( 56 | () => TopRatedMoviesNotifier( 57 | getTopRatedMovies: locator(), 58 | ), 59 | ); 60 | locator.registerFactory( 61 | () => WatchlistMovieNotifier( 62 | getWatchlistMovies: locator(), 63 | ), 64 | ); 65 | 66 | // use case 67 | locator.registerLazySingleton(() => GetNowPlayingMovies(locator())); 68 | locator.registerLazySingleton(() => GetPopularMovies(locator())); 69 | locator.registerLazySingleton(() => GetTopRatedMovies(locator())); 70 | locator.registerLazySingleton(() => GetMovieDetail(locator())); 71 | locator.registerLazySingleton(() => GetMovieRecommendations(locator())); 72 | locator.registerLazySingleton(() => SearchMovies(locator())); 73 | locator.registerLazySingleton(() => GetWatchListStatus(locator())); 74 | locator.registerLazySingleton(() => SaveWatchlist(locator())); 75 | locator.registerLazySingleton(() => RemoveWatchlist(locator())); 76 | locator.registerLazySingleton(() => GetWatchlistMovies(locator())); 77 | 78 | // repository 79 | locator.registerLazySingleton( 80 | () => MovieRepositoryImpl( 81 | remoteDataSource: locator(), 82 | localDataSource: locator(), 83 | ), 84 | ); 85 | 86 | // data sources 87 | locator.registerLazySingleton( 88 | () => MovieRemoteDataSourceImpl(client: locator())); 89 | locator.registerLazySingleton( 90 | () => MovieLocalDataSourceImpl(databaseHelper: locator())); 91 | 92 | // helper 93 | locator.registerLazySingleton(() => DatabaseHelper()); 94 | 95 | // external 96 | locator.registerLazySingleton(() => http.Client()); 97 | } 98 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/constants.dart'; 2 | import 'package:ditonton/common/utils.dart'; 3 | import 'package:ditonton/presentation/pages/about_page.dart'; 4 | import 'package:ditonton/presentation/pages/movie_detail_page.dart'; 5 | import 'package:ditonton/presentation/pages/home_movie_page.dart'; 6 | import 'package:ditonton/presentation/pages/popular_movies_page.dart'; 7 | import 'package:ditonton/presentation/pages/search_page.dart'; 8 | import 'package:ditonton/presentation/pages/top_rated_movies_page.dart'; 9 | import 'package:ditonton/presentation/pages/watchlist_movies_page.dart'; 10 | import 'package:ditonton/presentation/provider/movie_detail_notifier.dart'; 11 | import 'package:ditonton/presentation/provider/movie_list_notifier.dart'; 12 | import 'package:ditonton/presentation/provider/movie_search_notifier.dart'; 13 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart'; 14 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart'; 15 | import 'package:ditonton/presentation/provider/watchlist_movie_notifier.dart'; 16 | import 'package:flutter/cupertino.dart'; 17 | import 'package:flutter/material.dart'; 18 | import 'package:provider/provider.dart'; 19 | import 'package:ditonton/injection.dart' as di; 20 | 21 | void main() { 22 | di.init(); 23 | runApp(MyApp()); 24 | } 25 | 26 | class MyApp extends StatelessWidget { 27 | @override 28 | Widget build(BuildContext context) { 29 | return MultiProvider( 30 | providers: [ 31 | ChangeNotifierProvider( 32 | create: (_) => di.locator(), 33 | ), 34 | ChangeNotifierProvider( 35 | create: (_) => di.locator(), 36 | ), 37 | ChangeNotifierProvider( 38 | create: (_) => di.locator(), 39 | ), 40 | ChangeNotifierProvider( 41 | create: (_) => di.locator(), 42 | ), 43 | ChangeNotifierProvider( 44 | create: (_) => di.locator(), 45 | ), 46 | ChangeNotifierProvider( 47 | create: (_) => di.locator(), 48 | ), 49 | ], 50 | child: MaterialApp( 51 | title: 'Flutter Demo', 52 | theme: ThemeData.dark().copyWith( 53 | colorScheme: kColorScheme, 54 | primaryColor: kRichBlack, 55 | scaffoldBackgroundColor: kRichBlack, 56 | textTheme: kTextTheme, 57 | drawerTheme: kDrawerTheme, 58 | ), 59 | home: HomeMoviePage(), 60 | navigatorObservers: [routeObserver], 61 | onGenerateRoute: (RouteSettings settings) { 62 | switch (settings.name) { 63 | case '/home': 64 | return MaterialPageRoute(builder: (_) => HomeMoviePage()); 65 | case PopularMoviesPage.ROUTE_NAME: 66 | return CupertinoPageRoute(builder: (_) => PopularMoviesPage()); 67 | case TopRatedMoviesPage.ROUTE_NAME: 68 | return CupertinoPageRoute(builder: (_) => TopRatedMoviesPage()); 69 | case MovieDetailPage.ROUTE_NAME: 70 | final id = settings.arguments as int; 71 | return MaterialPageRoute( 72 | builder: (_) => MovieDetailPage(id: id), 73 | settings: settings, 74 | ); 75 | case SearchPage.ROUTE_NAME: 76 | return CupertinoPageRoute(builder: (_) => SearchPage()); 77 | case WatchlistMoviesPage.ROUTE_NAME: 78 | return MaterialPageRoute(builder: (_) => WatchlistMoviesPage()); 79 | case AboutPage.ROUTE_NAME: 80 | return MaterialPageRoute(builder: (_) => AboutPage()); 81 | default: 82 | return MaterialPageRoute(builder: (_) { 83 | return Scaffold( 84 | body: Center( 85 | child: Text('Page not found :('), 86 | ), 87 | ); 88 | }); 89 | } 90 | }, 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/presentation/pages/about_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/constants.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class AboutPage extends StatelessWidget { 5 | static const ROUTE_NAME = '/about'; 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | body: Stack( 11 | children: [ 12 | Column( 13 | children: [ 14 | Expanded( 15 | child: Container( 16 | color: kPrussianBlue, 17 | child: Center( 18 | child: Image.asset( 19 | 'assets/circle-g.png', 20 | width: 128, 21 | ), 22 | ), 23 | ), 24 | ), 25 | Expanded( 26 | child: Container( 27 | padding: const EdgeInsets.all(32.0), 28 | color: kMikadoYellow, 29 | child: Text( 30 | 'Ditonton merupakan sebuah aplikasi katalog film yang dikembangkan oleh Dicoding Indonesia sebagai contoh proyek aplikasi untuk kelas Menjadi Flutter Developer Expert.', 31 | style: TextStyle(color: Colors.black87, fontSize: 16), 32 | textAlign: TextAlign.justify, 33 | ), 34 | ), 35 | ), 36 | ], 37 | ), 38 | SafeArea( 39 | child: IconButton( 40 | onPressed: () => Navigator.pop(context), 41 | icon: Icon(Icons.arrow_back), 42 | ), 43 | ) 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/presentation/pages/home_movie_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:ditonton/common/constants.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/presentation/pages/about_page.dart'; 5 | import 'package:ditonton/presentation/pages/movie_detail_page.dart'; 6 | import 'package:ditonton/presentation/pages/popular_movies_page.dart'; 7 | import 'package:ditonton/presentation/pages/search_page.dart'; 8 | import 'package:ditonton/presentation/pages/top_rated_movies_page.dart'; 9 | import 'package:ditonton/presentation/pages/watchlist_movies_page.dart'; 10 | import 'package:ditonton/presentation/provider/movie_list_notifier.dart'; 11 | import 'package:ditonton/common/state_enum.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:provider/provider.dart'; 14 | 15 | class HomeMoviePage extends StatefulWidget { 16 | @override 17 | _HomeMoviePageState createState() => _HomeMoviePageState(); 18 | } 19 | 20 | class _HomeMoviePageState extends State { 21 | @override 22 | void initState() { 23 | super.initState(); 24 | Future.microtask( 25 | () => Provider.of(context, listen: false) 26 | ..fetchNowPlayingMovies() 27 | ..fetchPopularMovies() 28 | ..fetchTopRatedMovies()); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | drawer: Drawer( 35 | child: Column( 36 | children: [ 37 | UserAccountsDrawerHeader( 38 | currentAccountPicture: CircleAvatar( 39 | backgroundImage: AssetImage('assets/circle-g.png'), 40 | backgroundColor: Colors.grey.shade900, 41 | ), 42 | accountName: Text('Ditonton'), 43 | accountEmail: Text('ditonton@dicoding.com'), 44 | decoration: BoxDecoration( 45 | color: Colors.grey.shade900, 46 | ), 47 | ), 48 | ListTile( 49 | leading: Icon(Icons.movie), 50 | title: Text('Movies'), 51 | onTap: () { 52 | Navigator.pop(context); 53 | }, 54 | ), 55 | ListTile( 56 | leading: Icon(Icons.save_alt), 57 | title: Text('Watchlist'), 58 | onTap: () { 59 | Navigator.pushNamed(context, WatchlistMoviesPage.ROUTE_NAME); 60 | }, 61 | ), 62 | ListTile( 63 | onTap: () { 64 | Navigator.pushNamed(context, AboutPage.ROUTE_NAME); 65 | }, 66 | leading: Icon(Icons.info_outline), 67 | title: Text('About'), 68 | ), 69 | ], 70 | ), 71 | ), 72 | appBar: AppBar( 73 | title: Text('Ditonton'), 74 | actions: [ 75 | IconButton( 76 | onPressed: () { 77 | Navigator.pushNamed(context, SearchPage.ROUTE_NAME); 78 | }, 79 | icon: Icon(Icons.search), 80 | ) 81 | ], 82 | ), 83 | body: Padding( 84 | padding: const EdgeInsets.all(8.0), 85 | child: SingleChildScrollView( 86 | child: Column( 87 | crossAxisAlignment: CrossAxisAlignment.start, 88 | children: [ 89 | Text( 90 | 'Now Playing', 91 | style: kHeading6, 92 | ), 93 | Consumer(builder: (context, data, child) { 94 | final state = data.nowPlayingState; 95 | if (state == RequestState.Loading) { 96 | return Center( 97 | child: CircularProgressIndicator(), 98 | ); 99 | } else if (state == RequestState.Loaded) { 100 | return MovieList(data.nowPlayingMovies); 101 | } else { 102 | return Text('Failed'); 103 | } 104 | }), 105 | _buildSubHeading( 106 | title: 'Popular', 107 | onTap: () => 108 | Navigator.pushNamed(context, PopularMoviesPage.ROUTE_NAME), 109 | ), 110 | Consumer(builder: (context, data, child) { 111 | final state = data.popularMoviesState; 112 | if (state == RequestState.Loading) { 113 | return Center( 114 | child: CircularProgressIndicator(), 115 | ); 116 | } else if (state == RequestState.Loaded) { 117 | return MovieList(data.popularMovies); 118 | } else { 119 | return Text('Failed'); 120 | } 121 | }), 122 | _buildSubHeading( 123 | title: 'Top Rated', 124 | onTap: () => 125 | Navigator.pushNamed(context, TopRatedMoviesPage.ROUTE_NAME), 126 | ), 127 | Consumer(builder: (context, data, child) { 128 | final state = data.topRatedMoviesState; 129 | if (state == RequestState.Loading) { 130 | return Center( 131 | child: CircularProgressIndicator(), 132 | ); 133 | } else if (state == RequestState.Loaded) { 134 | return MovieList(data.topRatedMovies); 135 | } else { 136 | return Text('Failed'); 137 | } 138 | }), 139 | ], 140 | ), 141 | ), 142 | ), 143 | ); 144 | } 145 | 146 | Row _buildSubHeading({required String title, required Function() onTap}) { 147 | return Row( 148 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 149 | children: [ 150 | Text( 151 | title, 152 | style: kHeading6, 153 | ), 154 | InkWell( 155 | onTap: onTap, 156 | child: Padding( 157 | padding: const EdgeInsets.all(8.0), 158 | child: Row( 159 | children: [Text('See More'), Icon(Icons.arrow_forward_ios)], 160 | ), 161 | ), 162 | ), 163 | ], 164 | ); 165 | } 166 | } 167 | 168 | class MovieList extends StatelessWidget { 169 | final List movies; 170 | 171 | MovieList(this.movies); 172 | 173 | @override 174 | Widget build(BuildContext context) { 175 | return Container( 176 | height: 200, 177 | child: ListView.builder( 178 | scrollDirection: Axis.horizontal, 179 | itemBuilder: (context, index) { 180 | final movie = movies[index]; 181 | return Container( 182 | padding: const EdgeInsets.all(8), 183 | child: InkWell( 184 | onTap: () { 185 | Navigator.pushNamed( 186 | context, 187 | MovieDetailPage.ROUTE_NAME, 188 | arguments: movie.id, 189 | ); 190 | }, 191 | child: ClipRRect( 192 | borderRadius: BorderRadius.all(Radius.circular(16)), 193 | child: CachedNetworkImage( 194 | imageUrl: '$BASE_IMAGE_URL${movie.posterPath}', 195 | placeholder: (context, url) => Center( 196 | child: CircularProgressIndicator(), 197 | ), 198 | errorWidget: (context, url, error) => Icon(Icons.error), 199 | ), 200 | ), 201 | ), 202 | ); 203 | }, 204 | itemCount: movies.length, 205 | ), 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/presentation/pages/popular_movies_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart'; 3 | import 'package:ditonton/presentation/widgets/movie_card_list.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class PopularMoviesPage extends StatefulWidget { 8 | static const ROUTE_NAME = '/popular-movie'; 9 | 10 | @override 11 | _PopularMoviesPageState createState() => _PopularMoviesPageState(); 12 | } 13 | 14 | class _PopularMoviesPageState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | Future.microtask(() => 19 | Provider.of(context, listen: false) 20 | .fetchPopularMovies()); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text('Popular Movies'), 28 | ), 29 | body: Padding( 30 | padding: const EdgeInsets.all(8.0), 31 | child: Consumer( 32 | builder: (context, data, child) { 33 | if (data.state == RequestState.Loading) { 34 | return Center( 35 | child: CircularProgressIndicator(), 36 | ); 37 | } else if (data.state == RequestState.Loaded) { 38 | return ListView.builder( 39 | itemBuilder: (context, index) { 40 | final movie = data.movies[index]; 41 | return MovieCard(movie); 42 | }, 43 | itemCount: data.movies.length, 44 | ); 45 | } else { 46 | return Center( 47 | key: Key('error_message'), 48 | child: Text(data.message), 49 | ); 50 | } 51 | }, 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/presentation/pages/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/constants.dart'; 2 | import 'package:ditonton/common/state_enum.dart'; 3 | import 'package:ditonton/presentation/provider/movie_search_notifier.dart'; 4 | import 'package:ditonton/presentation/widgets/movie_card_list.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SearchPage extends StatelessWidget { 9 | static const ROUTE_NAME = '/search'; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text('Search'), 16 | ), 17 | body: Padding( 18 | padding: const EdgeInsets.all(16.0), 19 | child: Column( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | TextField( 23 | onSubmitted: (query) { 24 | Provider.of(context, listen: false) 25 | .fetchMovieSearch(query); 26 | }, 27 | decoration: InputDecoration( 28 | hintText: 'Search title', 29 | prefixIcon: Icon(Icons.search), 30 | border: OutlineInputBorder(), 31 | ), 32 | textInputAction: TextInputAction.search, 33 | ), 34 | SizedBox(height: 16), 35 | Text( 36 | 'Search Result', 37 | style: kHeading6, 38 | ), 39 | Consumer( 40 | builder: (context, data, child) { 41 | if (data.state == RequestState.Loading) { 42 | return Center( 43 | child: CircularProgressIndicator(), 44 | ); 45 | } else if (data.state == RequestState.Loaded) { 46 | final result = data.searchResult; 47 | return Expanded( 48 | child: ListView.builder( 49 | padding: const EdgeInsets.all(8), 50 | itemBuilder: (context, index) { 51 | final movie = data.searchResult[index]; 52 | return MovieCard(movie); 53 | }, 54 | itemCount: result.length, 55 | ), 56 | ); 57 | } else { 58 | return Expanded( 59 | child: Container(), 60 | ); 61 | } 62 | }, 63 | ), 64 | ], 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/presentation/pages/top_rated_movies_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart'; 3 | import 'package:ditonton/presentation/widgets/movie_card_list.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class TopRatedMoviesPage extends StatefulWidget { 8 | static const ROUTE_NAME = '/top-rated-movie'; 9 | 10 | @override 11 | _TopRatedMoviesPageState createState() => _TopRatedMoviesPageState(); 12 | } 13 | 14 | class _TopRatedMoviesPageState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | Future.microtask(() => 19 | Provider.of(context, listen: false) 20 | .fetchTopRatedMovies()); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text('Top Rated Movies'), 28 | ), 29 | body: Padding( 30 | padding: const EdgeInsets.all(8.0), 31 | child: Consumer( 32 | builder: (context, data, child) { 33 | if (data.state == RequestState.Loading) { 34 | return Center( 35 | child: CircularProgressIndicator(), 36 | ); 37 | } else if (data.state == RequestState.Loaded) { 38 | return ListView.builder( 39 | itemBuilder: (context, index) { 40 | final movie = data.movies[index]; 41 | return MovieCard(movie); 42 | }, 43 | itemCount: data.movies.length, 44 | ); 45 | } else { 46 | return Center( 47 | key: Key('error_message'), 48 | child: Text(data.message), 49 | ); 50 | } 51 | }, 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/presentation/pages/watchlist_movies_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/common/utils.dart'; 3 | import 'package:ditonton/presentation/provider/watchlist_movie_notifier.dart'; 4 | import 'package:ditonton/presentation/widgets/movie_card_list.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class WatchlistMoviesPage extends StatefulWidget { 9 | static const ROUTE_NAME = '/watchlist-movie'; 10 | 11 | @override 12 | _WatchlistMoviesPageState createState() => _WatchlistMoviesPageState(); 13 | } 14 | 15 | class _WatchlistMoviesPageState extends State 16 | with RouteAware { 17 | @override 18 | void initState() { 19 | super.initState(); 20 | Future.microtask(() => 21 | Provider.of(context, listen: false) 22 | .fetchWatchlistMovies()); 23 | } 24 | 25 | @override 26 | void didChangeDependencies() { 27 | super.didChangeDependencies(); 28 | routeObserver.subscribe(this, ModalRoute.of(context)!); 29 | } 30 | 31 | void didPopNext() { 32 | Provider.of(context, listen: false) 33 | .fetchWatchlistMovies(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | appBar: AppBar( 40 | title: Text('Watchlist'), 41 | ), 42 | body: Padding( 43 | padding: const EdgeInsets.all(8.0), 44 | child: Consumer( 45 | builder: (context, data, child) { 46 | if (data.watchlistState == RequestState.Loading) { 47 | return Center( 48 | child: CircularProgressIndicator(), 49 | ); 50 | } else if (data.watchlistState == RequestState.Loaded) { 51 | return ListView.builder( 52 | itemBuilder: (context, index) { 53 | final movie = data.watchlistMovies[index]; 54 | return MovieCard(movie); 55 | }, 56 | itemCount: data.watchlistMovies.length, 57 | ); 58 | } else { 59 | return Center( 60 | key: Key('error_message'), 61 | child: Text(data.message), 62 | ); 63 | } 64 | }, 65 | ), 66 | ), 67 | ); 68 | } 69 | 70 | @override 71 | void dispose() { 72 | routeObserver.unsubscribe(this); 73 | super.dispose(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/presentation/provider/movie_detail_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/movie.dart'; 2 | import 'package:ditonton/domain/entities/movie_detail.dart'; 3 | import 'package:ditonton/domain/usecases/get_movie_detail.dart'; 4 | import 'package:ditonton/domain/usecases/get_movie_recommendations.dart'; 5 | import 'package:ditonton/common/state_enum.dart'; 6 | import 'package:ditonton/domain/usecases/get_watchlist_status.dart'; 7 | import 'package:ditonton/domain/usecases/remove_watchlist.dart'; 8 | import 'package:ditonton/domain/usecases/save_watchlist.dart'; 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class MovieDetailNotifier extends ChangeNotifier { 13 | static const watchlistAddSuccessMessage = 'Added to Watchlist'; 14 | static const watchlistRemoveSuccessMessage = 'Removed from Watchlist'; 15 | 16 | final GetMovieDetail getMovieDetail; 17 | final GetMovieRecommendations getMovieRecommendations; 18 | final GetWatchListStatus getWatchListStatus; 19 | final SaveWatchlist saveWatchlist; 20 | final RemoveWatchlist removeWatchlist; 21 | 22 | MovieDetailNotifier({ 23 | required this.getMovieDetail, 24 | required this.getMovieRecommendations, 25 | required this.getWatchListStatus, 26 | required this.saveWatchlist, 27 | required this.removeWatchlist, 28 | }); 29 | 30 | late MovieDetail _movie; 31 | MovieDetail get movie => _movie; 32 | 33 | RequestState _movieState = RequestState.Empty; 34 | RequestState get movieState => _movieState; 35 | 36 | List _movieRecommendations = []; 37 | List get movieRecommendations => _movieRecommendations; 38 | 39 | RequestState _recommendationState = RequestState.Empty; 40 | RequestState get recommendationState => _recommendationState; 41 | 42 | String _message = ''; 43 | String get message => _message; 44 | 45 | bool _isAddedtoWatchlist = false; 46 | bool get isAddedToWatchlist => _isAddedtoWatchlist; 47 | 48 | Future fetchMovieDetail(int id) async { 49 | _movieState = RequestState.Loading; 50 | notifyListeners(); 51 | final detailResult = await getMovieDetail.execute(id); 52 | final recommendationResult = await getMovieRecommendations.execute(id); 53 | detailResult.fold( 54 | (failure) { 55 | _movieState = RequestState.Error; 56 | _message = failure.message; 57 | notifyListeners(); 58 | }, 59 | (movie) { 60 | _recommendationState = RequestState.Loading; 61 | _movie = movie; 62 | notifyListeners(); 63 | recommendationResult.fold( 64 | (failure) { 65 | _recommendationState = RequestState.Error; 66 | _message = failure.message; 67 | }, 68 | (movies) { 69 | _recommendationState = RequestState.Loaded; 70 | _movieRecommendations = movies; 71 | }, 72 | ); 73 | _movieState = RequestState.Loaded; 74 | notifyListeners(); 75 | }, 76 | ); 77 | } 78 | 79 | String _watchlistMessage = ''; 80 | String get watchlistMessage => _watchlistMessage; 81 | 82 | Future addWatchlist(MovieDetail movie) async { 83 | final result = await saveWatchlist.execute(movie); 84 | 85 | await result.fold( 86 | (failure) async { 87 | _watchlistMessage = failure.message; 88 | }, 89 | (successMessage) async { 90 | _watchlistMessage = successMessage; 91 | }, 92 | ); 93 | 94 | await loadWatchlistStatus(movie.id); 95 | } 96 | 97 | Future removeFromWatchlist(MovieDetail movie) async { 98 | final result = await removeWatchlist.execute(movie); 99 | 100 | await result.fold( 101 | (failure) async { 102 | _watchlistMessage = failure.message; 103 | }, 104 | (successMessage) async { 105 | _watchlistMessage = successMessage; 106 | }, 107 | ); 108 | 109 | await loadWatchlistStatus(movie.id); 110 | } 111 | 112 | Future loadWatchlistStatus(int id) async { 113 | final result = await getWatchListStatus.execute(id); 114 | _isAddedtoWatchlist = result; 115 | notifyListeners(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/presentation/provider/movie_list_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/entities/movie.dart'; 2 | import 'package:ditonton/domain/usecases/get_now_playing_movies.dart'; 3 | import 'package:ditonton/common/state_enum.dart'; 4 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 5 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class MovieListNotifier extends ChangeNotifier { 9 | var _nowPlayingMovies = []; 10 | List get nowPlayingMovies => _nowPlayingMovies; 11 | 12 | RequestState _nowPlayingState = RequestState.Empty; 13 | RequestState get nowPlayingState => _nowPlayingState; 14 | 15 | var _popularMovies = []; 16 | List get popularMovies => _popularMovies; 17 | 18 | RequestState _popularMoviesState = RequestState.Empty; 19 | RequestState get popularMoviesState => _popularMoviesState; 20 | 21 | var _topRatedMovies = []; 22 | List get topRatedMovies => _topRatedMovies; 23 | 24 | RequestState _topRatedMoviesState = RequestState.Empty; 25 | RequestState get topRatedMoviesState => _topRatedMoviesState; 26 | 27 | String _message = ''; 28 | String get message => _message; 29 | 30 | MovieListNotifier({ 31 | required this.getNowPlayingMovies, 32 | required this.getPopularMovies, 33 | required this.getTopRatedMovies, 34 | }); 35 | 36 | final GetNowPlayingMovies getNowPlayingMovies; 37 | final GetPopularMovies getPopularMovies; 38 | final GetTopRatedMovies getTopRatedMovies; 39 | 40 | Future fetchNowPlayingMovies() async { 41 | _nowPlayingState = RequestState.Loading; 42 | notifyListeners(); 43 | 44 | final result = await getNowPlayingMovies.execute(); 45 | result.fold( 46 | (failure) { 47 | _nowPlayingState = RequestState.Error; 48 | _message = failure.message; 49 | notifyListeners(); 50 | }, 51 | (moviesData) { 52 | _nowPlayingState = RequestState.Loaded; 53 | _nowPlayingMovies = moviesData; 54 | notifyListeners(); 55 | }, 56 | ); 57 | } 58 | 59 | Future fetchPopularMovies() async { 60 | _popularMoviesState = RequestState.Loading; 61 | notifyListeners(); 62 | 63 | final result = await getPopularMovies.execute(); 64 | result.fold( 65 | (failure) { 66 | _popularMoviesState = RequestState.Error; 67 | _message = failure.message; 68 | notifyListeners(); 69 | }, 70 | (moviesData) { 71 | _popularMoviesState = RequestState.Loaded; 72 | _popularMovies = moviesData; 73 | notifyListeners(); 74 | }, 75 | ); 76 | } 77 | 78 | Future fetchTopRatedMovies() async { 79 | _topRatedMoviesState = RequestState.Loading; 80 | notifyListeners(); 81 | 82 | final result = await getTopRatedMovies.execute(); 83 | result.fold( 84 | (failure) { 85 | _topRatedMoviesState = RequestState.Error; 86 | _message = failure.message; 87 | notifyListeners(); 88 | }, 89 | (moviesData) { 90 | _topRatedMoviesState = RequestState.Loaded; 91 | _topRatedMovies = moviesData; 92 | notifyListeners(); 93 | }, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/presentation/provider/movie_search_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/search_movies.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | class MovieSearchNotifier extends ChangeNotifier { 7 | final SearchMovies searchMovies; 8 | 9 | MovieSearchNotifier({required this.searchMovies}); 10 | 11 | RequestState _state = RequestState.Empty; 12 | RequestState get state => _state; 13 | 14 | List _searchResult = []; 15 | List get searchResult => _searchResult; 16 | 17 | String _message = ''; 18 | String get message => _message; 19 | 20 | Future fetchMovieSearch(String query) async { 21 | _state = RequestState.Loading; 22 | notifyListeners(); 23 | 24 | final result = await searchMovies.execute(query); 25 | result.fold( 26 | (failure) { 27 | _message = failure.message; 28 | _state = RequestState.Error; 29 | notifyListeners(); 30 | }, 31 | (data) { 32 | _searchResult = data; 33 | _state = RequestState.Loaded; 34 | notifyListeners(); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/presentation/provider/popular_movies_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | class PopularMoviesNotifier extends ChangeNotifier { 7 | final GetPopularMovies getPopularMovies; 8 | 9 | PopularMoviesNotifier(this.getPopularMovies); 10 | 11 | RequestState _state = RequestState.Empty; 12 | RequestState get state => _state; 13 | 14 | List _movies = []; 15 | List get movies => _movies; 16 | 17 | String _message = ''; 18 | String get message => _message; 19 | 20 | Future fetchPopularMovies() async { 21 | _state = RequestState.Loading; 22 | notifyListeners(); 23 | 24 | final result = await getPopularMovies.execute(); 25 | 26 | result.fold( 27 | (failure) { 28 | _message = failure.message; 29 | _state = RequestState.Error; 30 | notifyListeners(); 31 | }, 32 | (moviesData) { 33 | _movies = moviesData; 34 | _state = RequestState.Loaded; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/presentation/provider/top_rated_movies_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | class TopRatedMoviesNotifier extends ChangeNotifier { 7 | final GetTopRatedMovies getTopRatedMovies; 8 | 9 | TopRatedMoviesNotifier({required this.getTopRatedMovies}); 10 | 11 | RequestState _state = RequestState.Empty; 12 | RequestState get state => _state; 13 | 14 | List _movies = []; 15 | List get movies => _movies; 16 | 17 | String _message = ''; 18 | String get message => _message; 19 | 20 | Future fetchTopRatedMovies() async { 21 | _state = RequestState.Loading; 22 | notifyListeners(); 23 | 24 | final result = await getTopRatedMovies.execute(); 25 | 26 | result.fold( 27 | (failure) { 28 | _message = failure.message; 29 | _state = RequestState.Error; 30 | notifyListeners(); 31 | }, 32 | (moviesData) { 33 | _movies = moviesData; 34 | _state = RequestState.Loaded; 35 | notifyListeners(); 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/presentation/provider/watchlist_movie_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_watchlist_movies.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | class WatchlistMovieNotifier extends ChangeNotifier { 7 | var _watchlistMovies = []; 8 | List get watchlistMovies => _watchlistMovies; 9 | 10 | var _watchlistState = RequestState.Empty; 11 | RequestState get watchlistState => _watchlistState; 12 | 13 | String _message = ''; 14 | String get message => _message; 15 | 16 | WatchlistMovieNotifier({required this.getWatchlistMovies}); 17 | 18 | final GetWatchlistMovies getWatchlistMovies; 19 | 20 | Future fetchWatchlistMovies() async { 21 | _watchlistState = RequestState.Loading; 22 | notifyListeners(); 23 | 24 | final result = await getWatchlistMovies.execute(); 25 | result.fold( 26 | (failure) { 27 | _watchlistState = RequestState.Error; 28 | _message = failure.message; 29 | notifyListeners(); 30 | }, 31 | (moviesData) { 32 | _watchlistState = RequestState.Loaded; 33 | _watchlistMovies = moviesData; 34 | notifyListeners(); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/presentation/widgets/movie_card_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:ditonton/common/constants.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/presentation/pages/movie_detail_page.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class MovieCard extends StatelessWidget { 8 | final Movie movie; 9 | 10 | MovieCard(this.movie); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | margin: const EdgeInsets.symmetric(vertical: 4), 16 | child: InkWell( 17 | onTap: () { 18 | Navigator.pushNamed( 19 | context, 20 | MovieDetailPage.ROUTE_NAME, 21 | arguments: movie.id, 22 | ); 23 | }, 24 | child: Stack( 25 | alignment: Alignment.bottomLeft, 26 | children: [ 27 | Card( 28 | child: Container( 29 | margin: const EdgeInsets.only( 30 | left: 16 + 80 + 16, 31 | bottom: 8, 32 | right: 8, 33 | ), 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Text( 38 | movie.title ?? '-', 39 | maxLines: 1, 40 | overflow: TextOverflow.ellipsis, 41 | style: kHeading6, 42 | ), 43 | SizedBox(height: 16), 44 | Text( 45 | movie.overview ?? '-', 46 | maxLines: 2, 47 | overflow: TextOverflow.ellipsis, 48 | ), 49 | ], 50 | ), 51 | ), 52 | ), 53 | Container( 54 | margin: const EdgeInsets.only( 55 | left: 16, 56 | bottom: 16, 57 | ), 58 | child: ClipRRect( 59 | child: CachedNetworkImage( 60 | imageUrl: '$BASE_IMAGE_URL${movie.posterPath}', 61 | width: 80, 62 | placeholder: (context, url) => Center( 63 | child: CircularProgressIndicator(), 64 | ), 65 | errorWidget: (context, url, error) => Icon(Icons.error), 66 | ), 67 | borderRadius: BorderRadius.all(Radius.circular(8)), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ditonton 2 | description: A new Flutter application. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | http: ^1.2.2 28 | provider: ^6.1.2 29 | dartz: ^0.10.1 30 | equatable: ^2.0.5 31 | google_fonts: ^6.2.1 32 | flutter_rating_bar: ^4.0.1 33 | cached_network_image: ^3.4.1 34 | get_it: ^7.7.0 35 | sqflite: ^2.3.3+1 36 | path_provider: ^2.1.4 37 | 38 | dev_dependencies: 39 | mockito: ^5.0.8 40 | build_runner: ^2.0.4 41 | flutter_test: 42 | sdk: flutter 43 | 44 | # For information on the generic Dart part of this file, see the 45 | # following page: https://dart.dev/tools/pub/pubspec 46 | 47 | # The following section is specific to Flutter. 48 | flutter: 49 | 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | assets: 57 | - assets/ 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.dev/assets-and-images/#resolution-aware. 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # https://medium.com/@nocnoc/combined-code-coverage-for-flutter-and-dart-237b9563ecf8 4 | 5 | # remember some failed commands and report on exit 6 | error=false 7 | 8 | show_help() { 9 | printf "usage: $0 [--help] 10 | Tool for running all unit and widget tests with code coverage and automatically generated if lcov is installed. 11 | 12 | (run from root of repo) 13 | where: 14 | --help 15 | print this message 16 | " 17 | exit 1 18 | } 19 | 20 | # run unit and widget tests 21 | runTests() { 22 | cd $1 23 | if [ -f "pubspec.yaml" ] && [ -d "test" ]; then 24 | echo "running tests in $1" 25 | flutter pub get 26 | 27 | escapedPath="$(echo $1 | sed 's/\//\\\//g')" 28 | 29 | # run tests with coverage 30 | if grep flutter pubspec.yaml >/dev/null; then 31 | echo "run flutter tests" 32 | if [ -f "test/all_tests.dart" ]; then 33 | flutter test --coverage test/all_tests.dart || error=true 34 | else 35 | flutter test --coverage || error=true 36 | fi 37 | 38 | if [ -d "coverage" ]; then 39 | # combine line coverage info from package tests to a common file 40 | sed "s/^SF:lib/SF:$escapedPath\/lib/g" coverage/lcov.info >>$2/coverage/test.info 41 | rm -f coverage/lcov.info 42 | fi 43 | else 44 | echo "not a flutter package, skipping" 45 | fi 46 | fi 47 | cd - >/dev/null 48 | } 49 | 50 | runReport() { 51 | if [ -f "coverage/test.info" ] && ! [ "$TRAVIS" ]; then 52 | genhtml coverage/test.info -o coverage --no-function-coverage --prefix $(pwd) 53 | 54 | if [ "$(uname)" == "Darwin" ]; then 55 | open coverage/index.html 56 | else 57 | start coverage/index.html 58 | fi 59 | fi 60 | } 61 | 62 | if ! [ -f "pubspec.yaml" ] && [ -d .git ]; then 63 | printf "\nError: not in root of repo\n" 64 | show_help 65 | fi 66 | 67 | case $1 in 68 | --help) 69 | show_help 70 | ;; 71 | *) 72 | currentDir=$(pwd) 73 | # if no parameter passed 74 | if [ -z $1 ]; then 75 | if [ -d "coverage" ]; then 76 | rm -r coverage 77 | fi 78 | dirs=($(find . -maxdepth 2 -type d)) 79 | for dir in "${dirs[@]}"; do 80 | runTests $dir $currentDir 81 | done 82 | else 83 | if [[ -d "$1" ]]; then 84 | runTests $1 $currentDir 85 | else 86 | printf "\nError: not a directory: $1" 87 | show_help 88 | fi 89 | fi 90 | runReport 91 | ;; 92 | esac 93 | 94 | # Fail the build if there was an error 95 | if [ "$error" = true ]; then 96 | exit -1 97 | fi 98 | -------------------------------------------------------------------------------- /test/data/datasources/movie_local_data_source_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/exception.dart'; 2 | import 'package:ditonton/data/datasources/movie_local_data_source.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../../dummy_data/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late MovieLocalDataSourceImpl dataSource; 11 | late MockDatabaseHelper mockDatabaseHelper; 12 | 13 | setUp(() { 14 | mockDatabaseHelper = MockDatabaseHelper(); 15 | dataSource = MovieLocalDataSourceImpl(databaseHelper: mockDatabaseHelper); 16 | }); 17 | 18 | group('save watchlist', () { 19 | test('should return success message when insert to database is success', 20 | () async { 21 | // arrange 22 | when(mockDatabaseHelper.insertWatchlist(testMovieTable)) 23 | .thenAnswer((_) async => 1); 24 | // act 25 | final result = await dataSource.insertWatchlist(testMovieTable); 26 | // assert 27 | expect(result, 'Added to Watchlist'); 28 | }); 29 | 30 | test('should throw DatabaseException when insert to database is failed', 31 | () async { 32 | // arrange 33 | when(mockDatabaseHelper.insertWatchlist(testMovieTable)) 34 | .thenThrow(Exception()); 35 | // act 36 | final call = dataSource.insertWatchlist(testMovieTable); 37 | // assert 38 | expect(() => call, throwsA(isA())); 39 | }); 40 | }); 41 | 42 | group('remove watchlist', () { 43 | test('should return success message when remove from database is success', 44 | () async { 45 | // arrange 46 | when(mockDatabaseHelper.removeWatchlist(testMovieTable)) 47 | .thenAnswer((_) async => 1); 48 | // act 49 | final result = await dataSource.removeWatchlist(testMovieTable); 50 | // assert 51 | expect(result, 'Removed from Watchlist'); 52 | }); 53 | 54 | test('should throw DatabaseException when remove from database is failed', 55 | () async { 56 | // arrange 57 | when(mockDatabaseHelper.removeWatchlist(testMovieTable)) 58 | .thenThrow(Exception()); 59 | // act 60 | final call = dataSource.removeWatchlist(testMovieTable); 61 | // assert 62 | expect(() => call, throwsA(isA())); 63 | }); 64 | }); 65 | 66 | group('Get Movie Detail By Id', () { 67 | final tId = 1; 68 | 69 | test('should return Movie Detail Table when data is found', () async { 70 | // arrange 71 | when(mockDatabaseHelper.getMovieById(tId)) 72 | .thenAnswer((_) async => testMovieMap); 73 | // act 74 | final result = await dataSource.getMovieById(tId); 75 | // assert 76 | expect(result, testMovieTable); 77 | }); 78 | 79 | test('should return null when data is not found', () async { 80 | // arrange 81 | when(mockDatabaseHelper.getMovieById(tId)).thenAnswer((_) async => null); 82 | // act 83 | final result = await dataSource.getMovieById(tId); 84 | // assert 85 | expect(result, null); 86 | }); 87 | }); 88 | 89 | group('get watchlist movies', () { 90 | test('should return list of MovieTable from database', () async { 91 | // arrange 92 | when(mockDatabaseHelper.getWatchlistMovies()) 93 | .thenAnswer((_) async => [testMovieMap]); 94 | // act 95 | final result = await dataSource.getWatchlistMovies(); 96 | // assert 97 | expect(result, [testMovieTable]); 98 | }); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /test/data/models/movie_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/models/movie_model.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | final tMovieModel = MovieModel( 7 | adult: false, 8 | backdropPath: 'backdropPath', 9 | genreIds: [1, 2, 3], 10 | id: 1, 11 | originalTitle: 'originalTitle', 12 | overview: 'overview', 13 | popularity: 1, 14 | posterPath: 'posterPath', 15 | releaseDate: 'releaseDate', 16 | title: 'title', 17 | video: false, 18 | voteAverage: 1, 19 | voteCount: 1, 20 | ); 21 | 22 | final tMovie = Movie( 23 | adult: false, 24 | backdropPath: 'backdropPath', 25 | genreIds: [1, 2, 3], 26 | id: 1, 27 | originalTitle: 'originalTitle', 28 | overview: 'overview', 29 | popularity: 1, 30 | posterPath: 'posterPath', 31 | releaseDate: 'releaseDate', 32 | title: 'title', 33 | video: false, 34 | voteAverage: 1, 35 | voteCount: 1, 36 | ); 37 | 38 | test('should be a subclass of Movie entity', () async { 39 | final result = tMovieModel.toEntity(); 40 | expect(result, tMovie); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/data/models/movie_response_model_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:ditonton/data/models/movie_model.dart'; 4 | import 'package:ditonton/data/models/movie_response.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import '../../json_reader.dart'; 8 | 9 | void main() { 10 | final tMovieModel = MovieModel( 11 | adult: false, 12 | backdropPath: "/path.jpg", 13 | genreIds: [1, 2, 3, 4], 14 | id: 1, 15 | originalTitle: "Original Title", 16 | overview: "Overview", 17 | popularity: 1.0, 18 | posterPath: "/path.jpg", 19 | releaseDate: "2020-05-05", 20 | title: "Title", 21 | video: false, 22 | voteAverage: 1.0, 23 | voteCount: 1, 24 | ); 25 | final tMovieResponseModel = 26 | MovieResponse(movieList: [tMovieModel]); 27 | group('fromJson', () { 28 | test('should return a valid model from JSON', () async { 29 | // arrange 30 | final Map jsonMap = 31 | json.decode(readJson('dummy_data/now_playing.json')); 32 | // act 33 | final result = MovieResponse.fromJson(jsonMap); 34 | // assert 35 | expect(result, tMovieResponseModel); 36 | }); 37 | }); 38 | 39 | group('toJson', () { 40 | test('should return a JSON map containing proper data', () async { 41 | // arrange 42 | 43 | // act 44 | final result = tMovieResponseModel.toJson(); 45 | // assert 46 | final expectedJsonMap = { 47 | "results": [ 48 | { 49 | "adult": false, 50 | "backdrop_path": "/path.jpg", 51 | "genre_ids": [1, 2, 3, 4], 52 | "id": 1, 53 | "original_title": "Original Title", 54 | "overview": "Overview", 55 | "popularity": 1.0, 56 | "poster_path": "/path.jpg", 57 | "release_date": "2020-05-05", 58 | "title": "Title", 59 | "video": false, 60 | "vote_average": 1.0, 61 | "vote_count": 1 62 | } 63 | ], 64 | }; 65 | expect(result, expectedJsonMap); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/domain/usecases/get_movie_detail_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/usecases/get_movie_detail.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../../dummy_data/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetMovieDetail usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetMovieDetail(mockMovieRepository); 16 | }); 17 | 18 | final tId = 1; 19 | 20 | test('should get movie detail from the repository', () async { 21 | // arrange 22 | when(mockMovieRepository.getMovieDetail(tId)) 23 | .thenAnswer((_) async => Right(testMovieDetail)); 24 | // act 25 | final result = await usecase.execute(tId); 26 | // assert 27 | expect(result, Right(testMovieDetail)); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/domain/usecases/get_movie_recommendations_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_movie_recommendations.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetMovieRecommendations usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetMovieRecommendations(mockMovieRepository); 16 | }); 17 | 18 | final tId = 1; 19 | final tMovies = []; 20 | 21 | test('should get list of movie recommendations from the repository', 22 | () async { 23 | // arrange 24 | when(mockMovieRepository.getMovieRecommendations(tId)) 25 | .thenAnswer((_) async => Right(tMovies)); 26 | // act 27 | final result = await usecase.execute(tId); 28 | // assert 29 | expect(result, Right(tMovies)); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/domain/usecases/get_now_playing_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_now_playing_movies.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetNowPlayingMovies usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetNowPlayingMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | test('should get list of movies from the repository', () async { 21 | // arrange 22 | when(mockMovieRepository.getNowPlayingMovies()) 23 | .thenAnswer((_) async => Right(tMovies)); 24 | // act 25 | final result = await usecase.execute(); 26 | // assert 27 | expect(result, Right(tMovies)); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/domain/usecases/get_popular_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetPopularMovies usecase; 11 | late MockMovieRepository mockMovieRpository; 12 | 13 | setUp(() { 14 | mockMovieRpository = MockMovieRepository(); 15 | usecase = GetPopularMovies(mockMovieRpository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | group('GetPopularMovies Tests', () { 21 | group('execute', () { 22 | test( 23 | 'should get list of movies from the repository when execute function is called', 24 | () async { 25 | // arrange 26 | when(mockMovieRpository.getPopularMovies()) 27 | .thenAnswer((_) async => Right(tMovies)); 28 | // act 29 | final result = await usecase.execute(); 30 | // assert 31 | expect(result, Right(tMovies)); 32 | }); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/domain/usecases/get_top_rated_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetTopRatedMovies usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetTopRatedMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | 20 | test('should get list of movies from repository', () async { 21 | // arrange 22 | when(mockMovieRepository.getTopRatedMovies()) 23 | .thenAnswer((_) async => Right(tMovies)); 24 | // act 25 | final result = await usecase.execute(); 26 | // assert 27 | expect(result, Right(tMovies)); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/domain/usecases/get_watchlist_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/usecases/get_watchlist_movies.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../../dummy_data/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late GetWatchlistMovies usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = GetWatchlistMovies(mockMovieRepository); 16 | }); 17 | 18 | test('should get list of movies from the repository', () async { 19 | // arrange 20 | when(mockMovieRepository.getWatchlistMovies()) 21 | .thenAnswer((_) async => Right(testMovieList)); 22 | // act 23 | final result = await usecase.execute(); 24 | // assert 25 | expect(result, Right(testMovieList)); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/domain/usecases/get_watchlist_status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/domain/usecases/get_watchlist_status.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | 5 | import '../../helpers/test_helper.mocks.dart'; 6 | 7 | void main() { 8 | late GetWatchListStatus usecase; 9 | late MockMovieRepository mockMovieRepository; 10 | 11 | setUp(() { 12 | mockMovieRepository = MockMovieRepository(); 13 | usecase = GetWatchListStatus(mockMovieRepository); 14 | }); 15 | 16 | test('should get watchlist status from repository', () async { 17 | // arrange 18 | when(mockMovieRepository.isAddedToWatchlist(1)) 19 | .thenAnswer((_) async => true); 20 | // act 21 | final result = await usecase.execute(1); 22 | // assert 23 | expect(result, true); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/domain/usecases/remove_watchlist_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/usecases/remove_watchlist.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../../dummy_data/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late RemoveWatchlist usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = RemoveWatchlist(mockMovieRepository); 16 | }); 17 | 18 | test('should remove watchlist movie from repository', () async { 19 | // arrange 20 | when(mockMovieRepository.removeWatchlist(testMovieDetail)) 21 | .thenAnswer((_) async => Right('Removed from watchlist')); 22 | // act 23 | final result = await usecase.execute(testMovieDetail); 24 | // assert 25 | verify(mockMovieRepository.removeWatchlist(testMovieDetail)); 26 | expect(result, Right('Removed from watchlist')); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/domain/usecases/save_watchlist_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/usecases/save_watchlist.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../../dummy_data/dummy_objects.dart'; 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late SaveWatchlist usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = SaveWatchlist(mockMovieRepository); 16 | }); 17 | 18 | test('should save movie to the repository', () async { 19 | // arrange 20 | when(mockMovieRepository.saveWatchlist(testMovieDetail)) 21 | .thenAnswer((_) async => Right('Added to Watchlist')); 22 | // act 23 | final result = await usecase.execute(testMovieDetail); 24 | // assert 25 | verify(mockMovieRepository.saveWatchlist(testMovieDetail)); 26 | expect(result, Right('Added to Watchlist')); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/domain/usecases/search_movies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/search_movies.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | import '../../helpers/test_helper.mocks.dart'; 8 | 9 | void main() { 10 | late SearchMovies usecase; 11 | late MockMovieRepository mockMovieRepository; 12 | 13 | setUp(() { 14 | mockMovieRepository = MockMovieRepository(); 15 | usecase = SearchMovies(mockMovieRepository); 16 | }); 17 | 18 | final tMovies = []; 19 | final tQuery = 'Spiderman'; 20 | 21 | test('should get list of movies from the repository', () async { 22 | // arrange 23 | when(mockMovieRepository.searchMovies(tQuery)) 24 | .thenAnswer((_) async => Right(tMovies)); 25 | // act 26 | final result = await usecase.execute(tQuery); 27 | // assert 28 | expect(result, Right(tMovies)); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/dummy_data/dummy_objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/models/movie_table.dart'; 2 | import 'package:ditonton/domain/entities/genre.dart'; 3 | import 'package:ditonton/domain/entities/movie.dart'; 4 | import 'package:ditonton/domain/entities/movie_detail.dart'; 5 | 6 | final testMovie = Movie( 7 | adult: false, 8 | backdropPath: '/muth4OYamXf41G2evdrLEg8d3om.jpg', 9 | genreIds: [14, 28], 10 | id: 557, 11 | originalTitle: 'Spider-Man', 12 | overview: 13 | 'After being bitten by a genetically altered spider, nerdy high school student Peter Parker is endowed with amazing powers to become the Amazing superhero known as Spider-Man.', 14 | popularity: 60.441, 15 | posterPath: '/rweIrveL43TaxUN0akQEaAXL6x0.jpg', 16 | releaseDate: '2002-05-01', 17 | title: 'Spider-Man', 18 | video: false, 19 | voteAverage: 7.2, 20 | voteCount: 13507, 21 | ); 22 | 23 | final testMovieList = [testMovie]; 24 | 25 | final testMovieDetail = MovieDetail( 26 | adult: false, 27 | backdropPath: 'backdropPath', 28 | genres: [Genre(id: 1, name: 'Action')], 29 | id: 1, 30 | originalTitle: 'originalTitle', 31 | overview: 'overview', 32 | posterPath: 'posterPath', 33 | releaseDate: 'releaseDate', 34 | runtime: 120, 35 | title: 'title', 36 | voteAverage: 1, 37 | voteCount: 1, 38 | ); 39 | 40 | final testWatchlistMovie = Movie.watchlist( 41 | id: 1, 42 | title: 'title', 43 | posterPath: 'posterPath', 44 | overview: 'overview', 45 | ); 46 | 47 | final testMovieTable = MovieTable( 48 | id: 1, 49 | title: 'title', 50 | posterPath: 'posterPath', 51 | overview: 'overview', 52 | ); 53 | 54 | final testMovieMap = { 55 | 'id': 1, 56 | 'overview': 'overview', 57 | 'posterPath': 'posterPath', 58 | 'title': 'title', 59 | }; 60 | -------------------------------------------------------------------------------- /test/dummy_data/movie_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "adult": false, 3 | "backdrop_path": "/path.jpg", 4 | "belongs_to_collection": { 5 | "id": 11, 6 | "name": "Collection", 7 | "poster_path": null, 8 | "backdrop_path": null 9 | }, 10 | "budget": 100, 11 | "genres": [ 12 | { 13 | "id": 1, 14 | "name": "Action" 15 | } 16 | ], 17 | "homepage": "https://google.com", 18 | "id": 1, 19 | "imdb_id": "imdb1", 20 | "original_language": "en", 21 | "original_title": "Original Title", 22 | "overview": "Overview", 23 | "popularity": 1.0, 24 | "poster_path": "/path.jpg", 25 | "production_companies": [ 26 | { 27 | "id": 1, 28 | "logo_path": null, 29 | "name": "Company", 30 | "origin_country": "US" 31 | } 32 | ], 33 | "production_countries": [ 34 | { 35 | "iso_3166_1": "US", 36 | "name": "United States of America" 37 | } 38 | ], 39 | "release_date": "2020-05-05", 40 | "revenue": 12000, 41 | "runtime": 120, 42 | "spoken_languages": [ 43 | { 44 | "english_name": "English", 45 | "iso_639_1": "en", 46 | "name": "English" 47 | }, 48 | { 49 | "english_name": "German", 50 | "iso_639_1": "de", 51 | "name": "Deutsch" 52 | }, 53 | { 54 | "english_name": "Spanish", 55 | "iso_639_1": "es", 56 | "name": "Español" 57 | } 58 | ], 59 | "status": "Status", 60 | "tagline": "Tagline", 61 | "title": "Title", 62 | "video": false, 63 | "vote_average": 1.0, 64 | "vote_count": 1 65 | } -------------------------------------------------------------------------------- /test/dummy_data/movie_recommendations.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "adult": false, 6 | "backdrop_path": "/path.jpg", 7 | "genre_ids": [ 8 | 1, 9 | 2, 10 | 3 11 | ], 12 | "id": 1, 13 | "media_type": "movie", 14 | "title": "Title", 15 | "original_language": "en", 16 | "original_title": "Original Title", 17 | "overview": "Overview", 18 | "popularity": 1.0, 19 | "poster_path": "/path.jpg", 20 | "release_date": "2020-05-05", 21 | "video": false, 22 | "vote_average": 1.0, 23 | "vote_count": 1 24 | } 25 | ], 26 | "total_pages": 1, 27 | "total_results": 10 28 | } -------------------------------------------------------------------------------- /test/dummy_data/now_playing.json: -------------------------------------------------------------------------------- 1 | { 2 | "dates": { 3 | "maximum": "2021-05-23", 4 | "minimum": "2021-04-05" 5 | }, 6 | "page": 1, 7 | "results": [ 8 | { 9 | "adult": false, 10 | "backdrop_path": "/path.jpg", 11 | "genre_ids": [ 12 | 1, 13 | 2, 14 | 3, 15 | 4 16 | ], 17 | "id": 1, 18 | "original_language": "en", 19 | "original_title": "Original Title", 20 | "overview": "Overview", 21 | "popularity": 1.0, 22 | "poster_path": "/path.jpg", 23 | "release_date": "2020-05-05", 24 | "title": "Title", 25 | "video": false, 26 | "vote_average": 1.0, 27 | "vote_count": 1 28 | } 29 | ], 30 | "total_pages": 47, 31 | "total_results": 940 32 | } -------------------------------------------------------------------------------- /test/dummy_data/popular.json: -------------------------------------------------------------------------------- 1 | { 2 | "dates": { 3 | "maximum": "2021-05-23", 4 | "minimum": "2021-04-05" 5 | }, 6 | "page": 1, 7 | "results": [ 8 | { 9 | "adult": false, 10 | "backdrop_path": "/path.jpg", 11 | "genre_ids": [ 12 | 1, 13 | 2, 14 | 3, 15 | 4 16 | ], 17 | "id": 1, 18 | "original_language": "en", 19 | "original_title": "Original Title", 20 | "overview": "Overview", 21 | "popularity": 1.0, 22 | "poster_path": "/path.jpg", 23 | "release_date": "2020-05-05", 24 | "title": "Title", 25 | "video": false, 26 | "vote_average": 1.0, 27 | "vote_count": 1 28 | } 29 | ], 30 | "total_pages": 47, 31 | "total_results": 940 32 | } -------------------------------------------------------------------------------- /test/dummy_data/search_spiderman_movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": 1, 3 | "results": [ 4 | { 5 | "adult": false, 6 | "backdrop_path": "/muth4OYamXf41G2evdrLEg8d3om.jpg", 7 | "genre_ids": [ 8 | 14, 9 | 28 10 | ], 11 | "id": 557, 12 | "original_language": "en", 13 | "original_title": "Spider-Man", 14 | "overview": "After being bitten by a genetically altered spider, nerdy high school student Peter Parker is endowed with amazing powers to become the Amazing superhero known as Spider-Man.", 15 | "popularity": 60.441, 16 | "poster_path": "/rweIrveL43TaxUN0akQEaAXL6x0.jpg", 17 | "release_date": "2002-05-01", 18 | "title": "Spider-Man", 19 | "video": false, 20 | "vote_average": 7.2, 21 | "vote_count": 13507 22 | } 23 | ], 24 | "total_pages": 4, 25 | "total_results": 62 26 | } -------------------------------------------------------------------------------- /test/dummy_data/top_rated.json: -------------------------------------------------------------------------------- 1 | { 2 | "dates": { 3 | "maximum": "2021-05-23", 4 | "minimum": "2021-04-05" 5 | }, 6 | "page": 1, 7 | "results": [ 8 | { 9 | "adult": false, 10 | "backdrop_path": "/path.jpg", 11 | "genre_ids": [ 12 | 1, 13 | 2, 14 | 3, 15 | 4 16 | ], 17 | "id": 1, 18 | "original_language": "en", 19 | "original_title": "Original Title", 20 | "overview": "Overview", 21 | "popularity": 1.0, 22 | "poster_path": "/path.jpg", 23 | "release_date": "2020-05-05", 24 | "title": "Title", 25 | "video": false, 26 | "vote_average": 1.0, 27 | "vote_count": 1 28 | } 29 | ], 30 | "total_pages": 47, 31 | "total_results": 940 32 | } -------------------------------------------------------------------------------- /test/helpers/test_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/data/datasources/db/database_helper.dart'; 2 | import 'package:ditonton/data/datasources/movie_local_data_source.dart'; 3 | import 'package:ditonton/data/datasources/movie_remote_data_source.dart'; 4 | import 'package:ditonton/domain/repositories/movie_repository.dart'; 5 | import 'package:mockito/annotations.dart'; 6 | import 'package:http/http.dart' as http; 7 | 8 | @GenerateMocks([ 9 | MovieRepository, 10 | MovieRemoteDataSource, 11 | MovieLocalDataSource, 12 | DatabaseHelper, 13 | ], customMocks: [ 14 | MockSpec(as: #MockHttpClient) 15 | ]) 16 | void main() {} 17 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/presentation/pages/movie_detail_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/presentation/pages/movie_detail_page.dart'; 4 | import 'package:ditonton/presentation/provider/movie_detail_notifier.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import '../../dummy_data/dummy_objects.dart'; 12 | import 'movie_detail_page_test.mocks.dart'; 13 | 14 | @GenerateMocks([MovieDetailNotifier]) 15 | void main() { 16 | late MockMovieDetailNotifier mockNotifier; 17 | 18 | setUp(() { 19 | mockNotifier = MockMovieDetailNotifier(); 20 | }); 21 | 22 | Widget _makeTestableWidget(Widget body) { 23 | return ChangeNotifierProvider.value( 24 | value: mockNotifier, 25 | child: MaterialApp( 26 | home: body, 27 | ), 28 | ); 29 | } 30 | 31 | testWidgets( 32 | 'Watchlist button should display add icon when movie not added to watchlist', 33 | (WidgetTester tester) async { 34 | when(mockNotifier.movieState).thenReturn(RequestState.Loaded); 35 | when(mockNotifier.movie).thenReturn(testMovieDetail); 36 | when(mockNotifier.recommendationState).thenReturn(RequestState.Loaded); 37 | when(mockNotifier.movieRecommendations).thenReturn([]); 38 | when(mockNotifier.isAddedToWatchlist).thenReturn(false); 39 | 40 | final watchlistButtonIcon = find.byIcon(Icons.add); 41 | 42 | await tester.pumpWidget(_makeTestableWidget(MovieDetailPage(id: 1))); 43 | 44 | expect(watchlistButtonIcon, findsOneWidget); 45 | }); 46 | 47 | testWidgets( 48 | 'Watchlist button should dispay check icon when movie is added to wathclist', 49 | (WidgetTester tester) async { 50 | when(mockNotifier.movieState).thenReturn(RequestState.Loaded); 51 | when(mockNotifier.movie).thenReturn(testMovieDetail); 52 | when(mockNotifier.recommendationState).thenReturn(RequestState.Loaded); 53 | when(mockNotifier.movieRecommendations).thenReturn([]); 54 | when(mockNotifier.isAddedToWatchlist).thenReturn(true); 55 | 56 | final watchlistButtonIcon = find.byIcon(Icons.check); 57 | 58 | await tester.pumpWidget(_makeTestableWidget(MovieDetailPage(id: 1))); 59 | 60 | expect(watchlistButtonIcon, findsOneWidget); 61 | }); 62 | 63 | testWidgets( 64 | 'Watchlist button should display Snackbar when added to watchlist', 65 | (WidgetTester tester) async { 66 | when(mockNotifier.movieState).thenReturn(RequestState.Loaded); 67 | when(mockNotifier.movie).thenReturn(testMovieDetail); 68 | when(mockNotifier.recommendationState).thenReturn(RequestState.Loaded); 69 | when(mockNotifier.movieRecommendations).thenReturn([]); 70 | when(mockNotifier.isAddedToWatchlist).thenReturn(false); 71 | when(mockNotifier.watchlistMessage).thenReturn('Added to Watchlist'); 72 | 73 | final watchlistButton = find.byType(ElevatedButton); 74 | 75 | await tester.pumpWidget(_makeTestableWidget(MovieDetailPage(id: 1))); 76 | 77 | expect(find.byIcon(Icons.add), findsOneWidget); 78 | 79 | await tester.tap(watchlistButton); 80 | await tester.pump(); 81 | 82 | expect(find.byType(SnackBar), findsOneWidget); 83 | expect(find.text('Added to Watchlist'), findsOneWidget); 84 | }); 85 | 86 | testWidgets( 87 | 'Watchlist button should display AlertDialog when add to watchlist failed', 88 | (WidgetTester tester) async { 89 | when(mockNotifier.movieState).thenReturn(RequestState.Loaded); 90 | when(mockNotifier.movie).thenReturn(testMovieDetail); 91 | when(mockNotifier.recommendationState).thenReturn(RequestState.Loaded); 92 | when(mockNotifier.movieRecommendations).thenReturn([]); 93 | when(mockNotifier.isAddedToWatchlist).thenReturn(false); 94 | when(mockNotifier.watchlistMessage).thenReturn('Failed'); 95 | 96 | final watchlistButton = find.byType(ElevatedButton); 97 | 98 | await tester.pumpWidget(_makeTestableWidget(MovieDetailPage(id: 1))); 99 | 100 | expect(find.byIcon(Icons.add), findsOneWidget); 101 | 102 | await tester.tap(watchlistButton); 103 | await tester.pump(); 104 | 105 | expect(find.byType(AlertDialog), findsOneWidget); 106 | expect(find.text('Failed'), findsOneWidget); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /test/presentation/pages/movie_detail_page_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/pages/movie_detail_page_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i11; 6 | import 'dart:ui' as _i12; 7 | 8 | import 'package:ditonton/common/state_enum.dart' as _i9; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i10; 10 | import 'package:ditonton/domain/entities/movie_detail.dart' as _i7; 11 | import 'package:ditonton/domain/usecases/get_movie_detail.dart' as _i2; 12 | import 'package:ditonton/domain/usecases/get_movie_recommendations.dart' as _i3; 13 | import 'package:ditonton/domain/usecases/get_watchlist_status.dart' as _i4; 14 | import 'package:ditonton/domain/usecases/remove_watchlist.dart' as _i6; 15 | import 'package:ditonton/domain/usecases/save_watchlist.dart' as _i5; 16 | import 'package:ditonton/presentation/provider/movie_detail_notifier.dart' 17 | as _i8; 18 | import 'package:mockito/mockito.dart' as _i1; 19 | 20 | // ignore_for_file: avoid_redundant_argument_values 21 | // ignore_for_file: comment_references 22 | // ignore_for_file: invalid_use_of_visible_for_testing_member 23 | // ignore_for_file: prefer_const_constructors 24 | // ignore_for_file: unnecessary_parenthesis 25 | 26 | class _FakeGetMovieDetail extends _i1.Fake implements _i2.GetMovieDetail {} 27 | 28 | class _FakeGetMovieRecommendations extends _i1.Fake 29 | implements _i3.GetMovieRecommendations {} 30 | 31 | class _FakeGetWatchListStatus extends _i1.Fake 32 | implements _i4.GetWatchListStatus {} 33 | 34 | class _FakeSaveWatchlist extends _i1.Fake implements _i5.SaveWatchlist {} 35 | 36 | class _FakeRemoveWatchlist extends _i1.Fake implements _i6.RemoveWatchlist {} 37 | 38 | class _FakeMovieDetail extends _i1.Fake implements _i7.MovieDetail {} 39 | 40 | /// A class which mocks [MovieDetailNotifier]. 41 | /// 42 | /// See the documentation for Mockito's code generation for more information. 43 | class MockMovieDetailNotifier extends _i1.Mock 44 | implements _i8.MovieDetailNotifier { 45 | MockMovieDetailNotifier() { 46 | _i1.throwOnMissingStub(this); 47 | } 48 | 49 | @override 50 | _i2.GetMovieDetail get getMovieDetail => 51 | (super.noSuchMethod(Invocation.getter(#getMovieDetail), 52 | returnValue: _FakeGetMovieDetail()) as _i2.GetMovieDetail); 53 | @override 54 | _i3.GetMovieRecommendations get getMovieRecommendations => 55 | (super.noSuchMethod(Invocation.getter(#getMovieRecommendations), 56 | returnValue: _FakeGetMovieRecommendations()) 57 | as _i3.GetMovieRecommendations); 58 | @override 59 | _i4.GetWatchListStatus get getWatchListStatus => 60 | (super.noSuchMethod(Invocation.getter(#getWatchListStatus), 61 | returnValue: _FakeGetWatchListStatus()) as _i4.GetWatchListStatus); 62 | @override 63 | _i5.SaveWatchlist get saveWatchlist => 64 | (super.noSuchMethod(Invocation.getter(#saveWatchlist), 65 | returnValue: _FakeSaveWatchlist()) as _i5.SaveWatchlist); 66 | @override 67 | _i6.RemoveWatchlist get removeWatchlist => 68 | (super.noSuchMethod(Invocation.getter(#removeWatchlist), 69 | returnValue: _FakeRemoveWatchlist()) as _i6.RemoveWatchlist); 70 | @override 71 | _i7.MovieDetail get movie => (super.noSuchMethod(Invocation.getter(#movie), 72 | returnValue: _FakeMovieDetail()) as _i7.MovieDetail); 73 | @override 74 | _i9.RequestState get movieState => 75 | (super.noSuchMethod(Invocation.getter(#movieState), 76 | returnValue: _i9.RequestState.Empty) as _i9.RequestState); 77 | @override 78 | List<_i10.Movie> get movieRecommendations => 79 | (super.noSuchMethod(Invocation.getter(#movieRecommendations), 80 | returnValue: <_i10.Movie>[]) as List<_i10.Movie>); 81 | @override 82 | _i9.RequestState get recommendationState => 83 | (super.noSuchMethod(Invocation.getter(#recommendationState), 84 | returnValue: _i9.RequestState.Empty) as _i9.RequestState); 85 | @override 86 | String get message => 87 | (super.noSuchMethod(Invocation.getter(#message), returnValue: '') 88 | as String); 89 | @override 90 | bool get isAddedToWatchlist => 91 | (super.noSuchMethod(Invocation.getter(#isAddedToWatchlist), 92 | returnValue: false) as bool); 93 | @override 94 | String get watchlistMessage => 95 | (super.noSuchMethod(Invocation.getter(#watchlistMessage), returnValue: '') 96 | as String); 97 | @override 98 | bool get hasListeners => 99 | (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) 100 | as bool); 101 | @override 102 | _i11.Future fetchMovieDetail(int? id) => 103 | (super.noSuchMethod(Invocation.method(#fetchMovieDetail, [id]), 104 | returnValue: Future.value(), 105 | returnValueForMissingStub: Future.value()) as _i11.Future); 106 | @override 107 | _i11.Future addWatchlist(_i7.MovieDetail? movie) => 108 | (super.noSuchMethod(Invocation.method(#addWatchlist, [movie]), 109 | returnValue: Future.value(), 110 | returnValueForMissingStub: Future.value()) as _i11.Future); 111 | @override 112 | _i11.Future removeFromWatchlist(_i7.MovieDetail? movie) => 113 | (super.noSuchMethod(Invocation.method(#removeFromWatchlist, [movie]), 114 | returnValue: Future.value(), 115 | returnValueForMissingStub: Future.value()) as _i11.Future); 116 | @override 117 | _i11.Future loadWatchlistStatus(int? id) => 118 | (super.noSuchMethod(Invocation.method(#loadWatchlistStatus, [id]), 119 | returnValue: Future.value(), 120 | returnValueForMissingStub: Future.value()) as _i11.Future); 121 | @override 122 | void addListener(_i12.VoidCallback? listener) => 123 | super.noSuchMethod(Invocation.method(#addListener, [listener]), 124 | returnValueForMissingStub: null); 125 | @override 126 | void removeListener(_i12.VoidCallback? listener) => 127 | super.noSuchMethod(Invocation.method(#removeListener, [listener]), 128 | returnValueForMissingStub: null); 129 | @override 130 | void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), 131 | returnValueForMissingStub: null); 132 | @override 133 | void notifyListeners() => 134 | super.noSuchMethod(Invocation.method(#notifyListeners, []), 135 | returnValueForMissingStub: null); 136 | } 137 | -------------------------------------------------------------------------------- /test/presentation/pages/popular_movies_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/presentation/pages/popular_movies_page.dart'; 4 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import 'popular_movies_page_test.mocks.dart'; 12 | 13 | @GenerateMocks([PopularMoviesNotifier]) 14 | void main() { 15 | late MockPopularMoviesNotifier mockNotifier; 16 | 17 | setUp(() { 18 | mockNotifier = MockPopularMoviesNotifier(); 19 | }); 20 | 21 | Widget _makeTestableWidget(Widget body) { 22 | return ChangeNotifierProvider.value( 23 | value: mockNotifier, 24 | child: MaterialApp( 25 | home: body, 26 | ), 27 | ); 28 | } 29 | 30 | testWidgets('Page should display center progress bar when loading', 31 | (WidgetTester tester) async { 32 | when(mockNotifier.state).thenReturn(RequestState.Loading); 33 | 34 | final progressBarFinder = find.byType(CircularProgressIndicator); 35 | final centerFinder = find.byType(Center); 36 | 37 | await tester.pumpWidget(_makeTestableWidget(PopularMoviesPage())); 38 | 39 | expect(centerFinder, findsOneWidget); 40 | expect(progressBarFinder, findsOneWidget); 41 | }); 42 | 43 | testWidgets('Page should display ListView when data is loaded', 44 | (WidgetTester tester) async { 45 | when(mockNotifier.state).thenReturn(RequestState.Loaded); 46 | when(mockNotifier.movies).thenReturn([]); 47 | 48 | final listViewFinder = find.byType(ListView); 49 | 50 | await tester.pumpWidget(_makeTestableWidget(PopularMoviesPage())); 51 | 52 | expect(listViewFinder, findsOneWidget); 53 | }); 54 | 55 | testWidgets('Page should display text with message when Error', 56 | (WidgetTester tester) async { 57 | when(mockNotifier.state).thenReturn(RequestState.Error); 58 | when(mockNotifier.message).thenReturn('Error message'); 59 | 60 | final textFinder = find.byKey(Key('error_message')); 61 | 62 | await tester.pumpWidget(_makeTestableWidget(PopularMoviesPage())); 63 | 64 | expect(textFinder, findsOneWidget); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/presentation/pages/popular_movies_page_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/pages/popular_movies_page_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i6; 6 | import 'dart:ui' as _i7; 7 | 8 | import 'package:ditonton/common/state_enum.dart' as _i4; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i5; 10 | import 'package:ditonton/domain/usecases/get_popular_movies.dart' as _i2; 11 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart' 12 | as _i3; 13 | import 'package:mockito/mockito.dart' as _i1; 14 | 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: comment_references 17 | // ignore_for_file: invalid_use_of_visible_for_testing_member 18 | // ignore_for_file: prefer_const_constructors 19 | // ignore_for_file: unnecessary_parenthesis 20 | 21 | class _FakeGetPopularMovies extends _i1.Fake implements _i2.GetPopularMovies {} 22 | 23 | /// A class which mocks [PopularMoviesNotifier]. 24 | /// 25 | /// See the documentation for Mockito's code generation for more information. 26 | class MockPopularMoviesNotifier extends _i1.Mock 27 | implements _i3.PopularMoviesNotifier { 28 | MockPopularMoviesNotifier() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i2.GetPopularMovies get getPopularMovies => 34 | (super.noSuchMethod(Invocation.getter(#getPopularMovies), 35 | returnValue: _FakeGetPopularMovies()) as _i2.GetPopularMovies); 36 | @override 37 | _i4.RequestState get state => (super.noSuchMethod(Invocation.getter(#state), 38 | returnValue: _i4.RequestState.Empty) as _i4.RequestState); 39 | @override 40 | List<_i5.Movie> get movies => (super.noSuchMethod(Invocation.getter(#movies), 41 | returnValue: <_i5.Movie>[]) as List<_i5.Movie>); 42 | @override 43 | String get message => 44 | (super.noSuchMethod(Invocation.getter(#message), returnValue: '') 45 | as String); 46 | @override 47 | bool get hasListeners => 48 | (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) 49 | as bool); 50 | @override 51 | _i6.Future fetchPopularMovies() => 52 | (super.noSuchMethod(Invocation.method(#fetchPopularMovies, []), 53 | returnValue: Future.value(), 54 | returnValueForMissingStub: Future.value()) as _i6.Future); 55 | @override 56 | void addListener(_i7.VoidCallback? listener) => 57 | super.noSuchMethod(Invocation.method(#addListener, [listener]), 58 | returnValueForMissingStub: null); 59 | @override 60 | void removeListener(_i7.VoidCallback? listener) => 61 | super.noSuchMethod(Invocation.method(#removeListener, [listener]), 62 | returnValueForMissingStub: null); 63 | @override 64 | void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), 65 | returnValueForMissingStub: null); 66 | @override 67 | void notifyListeners() => 68 | super.noSuchMethod(Invocation.method(#notifyListeners, []), 69 | returnValueForMissingStub: null); 70 | } 71 | -------------------------------------------------------------------------------- /test/presentation/pages/top_rated_movies_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ditonton/common/state_enum.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/presentation/pages/top_rated_movies_page.dart'; 4 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import 'top_rated_movies_page_test.mocks.dart'; 12 | 13 | @GenerateMocks([TopRatedMoviesNotifier]) 14 | void main() { 15 | late MockTopRatedMoviesNotifier mockNotifier; 16 | 17 | setUp(() { 18 | mockNotifier = MockTopRatedMoviesNotifier(); 19 | }); 20 | 21 | Widget _makeTestableWidget(Widget body) { 22 | return ChangeNotifierProvider.value( 23 | value: mockNotifier, 24 | child: MaterialApp( 25 | home: body, 26 | ), 27 | ); 28 | } 29 | 30 | testWidgets('Page should display progress bar when loading', 31 | (WidgetTester tester) async { 32 | when(mockNotifier.state).thenReturn(RequestState.Loading); 33 | 34 | final progressFinder = find.byType(CircularProgressIndicator); 35 | final centerFinder = find.byType(Center); 36 | 37 | await tester.pumpWidget(_makeTestableWidget(TopRatedMoviesPage())); 38 | 39 | expect(centerFinder, findsOneWidget); 40 | expect(progressFinder, findsOneWidget); 41 | }); 42 | 43 | testWidgets('Page should display when data is loaded', 44 | (WidgetTester tester) async { 45 | when(mockNotifier.state).thenReturn(RequestState.Loaded); 46 | when(mockNotifier.movies).thenReturn([]); 47 | 48 | final listViewFinder = find.byType(ListView); 49 | 50 | await tester.pumpWidget(_makeTestableWidget(TopRatedMoviesPage())); 51 | 52 | expect(listViewFinder, findsOneWidget); 53 | }); 54 | 55 | testWidgets('Page should display text with message when Error', 56 | (WidgetTester tester) async { 57 | when(mockNotifier.state).thenReturn(RequestState.Error); 58 | when(mockNotifier.message).thenReturn('Error message'); 59 | 60 | final textFinder = find.byKey(Key('error_message')); 61 | 62 | await tester.pumpWidget(_makeTestableWidget(TopRatedMoviesPage())); 63 | 64 | expect(textFinder, findsOneWidget); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/presentation/pages/top_rated_movies_page_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/pages/top_rated_movies_page_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i6; 6 | import 'dart:ui' as _i7; 7 | 8 | import 'package:ditonton/common/state_enum.dart' as _i4; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i5; 10 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart' as _i2; 11 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart' 12 | as _i3; 13 | import 'package:mockito/mockito.dart' as _i1; 14 | 15 | // ignore_for_file: avoid_redundant_argument_values 16 | // ignore_for_file: comment_references 17 | // ignore_for_file: invalid_use_of_visible_for_testing_member 18 | // ignore_for_file: prefer_const_constructors 19 | // ignore_for_file: unnecessary_parenthesis 20 | 21 | class _FakeGetTopRatedMovies extends _i1.Fake implements _i2.GetTopRatedMovies { 22 | } 23 | 24 | /// A class which mocks [TopRatedMoviesNotifier]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockTopRatedMoviesNotifier extends _i1.Mock 28 | implements _i3.TopRatedMoviesNotifier { 29 | MockTopRatedMoviesNotifier() { 30 | _i1.throwOnMissingStub(this); 31 | } 32 | 33 | @override 34 | _i2.GetTopRatedMovies get getTopRatedMovies => 35 | (super.noSuchMethod(Invocation.getter(#getTopRatedMovies), 36 | returnValue: _FakeGetTopRatedMovies()) as _i2.GetTopRatedMovies); 37 | @override 38 | _i4.RequestState get state => (super.noSuchMethod(Invocation.getter(#state), 39 | returnValue: _i4.RequestState.Empty) as _i4.RequestState); 40 | @override 41 | List<_i5.Movie> get movies => (super.noSuchMethod(Invocation.getter(#movies), 42 | returnValue: <_i5.Movie>[]) as List<_i5.Movie>); 43 | @override 44 | String get message => 45 | (super.noSuchMethod(Invocation.getter(#message), returnValue: '') 46 | as String); 47 | @override 48 | bool get hasListeners => 49 | (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) 50 | as bool); 51 | @override 52 | _i6.Future fetchTopRatedMovies() => 53 | (super.noSuchMethod(Invocation.method(#fetchTopRatedMovies, []), 54 | returnValue: Future.value(), 55 | returnValueForMissingStub: Future.value()) as _i6.Future); 56 | @override 57 | void addListener(_i7.VoidCallback? listener) => 58 | super.noSuchMethod(Invocation.method(#addListener, [listener]), 59 | returnValueForMissingStub: null); 60 | @override 61 | void removeListener(_i7.VoidCallback? listener) => 62 | super.noSuchMethod(Invocation.method(#removeListener, [listener]), 63 | returnValueForMissingStub: null); 64 | @override 65 | void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), 66 | returnValueForMissingStub: null); 67 | @override 68 | void notifyListeners() => 69 | super.noSuchMethod(Invocation.method(#notifyListeners, []), 70 | returnValueForMissingStub: null); 71 | } 72 | -------------------------------------------------------------------------------- /test/presentation/provider/movie_detail_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/provider/movie_detail_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:dartz/dartz.dart' as _i3; 8 | import 'package:ditonton/common/failure.dart' as _i6; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i9; 10 | import 'package:ditonton/domain/entities/movie_detail.dart' as _i7; 11 | import 'package:ditonton/domain/repositories/movie_repository.dart' as _i2; 12 | import 'package:ditonton/domain/usecases/get_movie_detail.dart' as _i4; 13 | import 'package:ditonton/domain/usecases/get_movie_recommendations.dart' as _i8; 14 | import 'package:ditonton/domain/usecases/get_watchlist_status.dart' as _i10; 15 | import 'package:ditonton/domain/usecases/remove_watchlist.dart' as _i12; 16 | import 'package:ditonton/domain/usecases/save_watchlist.dart' as _i11; 17 | import 'package:mockito/mockito.dart' as _i1; 18 | 19 | // ignore_for_file: avoid_redundant_argument_values 20 | // ignore_for_file: comment_references 21 | // ignore_for_file: invalid_use_of_visible_for_testing_member 22 | // ignore_for_file: prefer_const_constructors 23 | // ignore_for_file: unnecessary_parenthesis 24 | 25 | class _FakeMovieRepository extends _i1.Fake implements _i2.MovieRepository {} 26 | 27 | class _FakeEither extends _i1.Fake implements _i3.Either {} 28 | 29 | /// A class which mocks [GetMovieDetail]. 30 | /// 31 | /// See the documentation for Mockito's code generation for more information. 32 | class MockGetMovieDetail extends _i1.Mock implements _i4.GetMovieDetail { 33 | MockGetMovieDetail() { 34 | _i1.throwOnMissingStub(this); 35 | } 36 | 37 | @override 38 | _i2.MovieRepository get repository => 39 | (super.noSuchMethod(Invocation.getter(#repository), 40 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 41 | @override 42 | _i5.Future<_i3.Either<_i6.Failure, _i7.MovieDetail>> execute(int? id) => 43 | (super.noSuchMethod(Invocation.method(#execute, [id]), 44 | returnValue: Future<_i3.Either<_i6.Failure, _i7.MovieDetail>>.value( 45 | _FakeEither<_i6.Failure, _i7.MovieDetail>())) as _i5 46 | .Future<_i3.Either<_i6.Failure, _i7.MovieDetail>>); 47 | } 48 | 49 | /// A class which mocks [GetMovieRecommendations]. 50 | /// 51 | /// See the documentation for Mockito's code generation for more information. 52 | class MockGetMovieRecommendations extends _i1.Mock 53 | implements _i8.GetMovieRecommendations { 54 | MockGetMovieRecommendations() { 55 | _i1.throwOnMissingStub(this); 56 | } 57 | 58 | @override 59 | _i2.MovieRepository get repository => 60 | (super.noSuchMethod(Invocation.getter(#repository), 61 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 62 | @override 63 | _i5.Future<_i3.Either<_i6.Failure, List<_i9.Movie>>> execute(dynamic id) => 64 | (super.noSuchMethod(Invocation.method(#execute, [id]), 65 | returnValue: Future<_i3.Either<_i6.Failure, List<_i9.Movie>>>.value( 66 | _FakeEither<_i6.Failure, List<_i9.Movie>>())) as _i5 67 | .Future<_i3.Either<_i6.Failure, List<_i9.Movie>>>); 68 | } 69 | 70 | /// A class which mocks [GetWatchListStatus]. 71 | /// 72 | /// See the documentation for Mockito's code generation for more information. 73 | class MockGetWatchListStatus extends _i1.Mock 74 | implements _i10.GetWatchListStatus { 75 | MockGetWatchListStatus() { 76 | _i1.throwOnMissingStub(this); 77 | } 78 | 79 | @override 80 | _i2.MovieRepository get repository => 81 | (super.noSuchMethod(Invocation.getter(#repository), 82 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 83 | @override 84 | _i5.Future execute(int? id) => 85 | (super.noSuchMethod(Invocation.method(#execute, [id]), 86 | returnValue: Future.value(false)) as _i5.Future); 87 | } 88 | 89 | /// A class which mocks [SaveWatchlist]. 90 | /// 91 | /// See the documentation for Mockito's code generation for more information. 92 | class MockSaveWatchlist extends _i1.Mock implements _i11.SaveWatchlist { 93 | MockSaveWatchlist() { 94 | _i1.throwOnMissingStub(this); 95 | } 96 | 97 | @override 98 | _i2.MovieRepository get repository => 99 | (super.noSuchMethod(Invocation.getter(#repository), 100 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 101 | @override 102 | _i5.Future<_i3.Either<_i6.Failure, String>> execute(_i7.MovieDetail? movie) => 103 | (super.noSuchMethod(Invocation.method(#execute, [movie]), 104 | returnValue: Future<_i3.Either<_i6.Failure, String>>.value( 105 | _FakeEither<_i6.Failure, String>())) 106 | as _i5.Future<_i3.Either<_i6.Failure, String>>); 107 | } 108 | 109 | /// A class which mocks [RemoveWatchlist]. 110 | /// 111 | /// See the documentation for Mockito's code generation for more information. 112 | class MockRemoveWatchlist extends _i1.Mock implements _i12.RemoveWatchlist { 113 | MockRemoveWatchlist() { 114 | _i1.throwOnMissingStub(this); 115 | } 116 | 117 | @override 118 | _i2.MovieRepository get repository => 119 | (super.noSuchMethod(Invocation.getter(#repository), 120 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 121 | @override 122 | _i5.Future<_i3.Either<_i6.Failure, String>> execute(_i7.MovieDetail? movie) => 123 | (super.noSuchMethod(Invocation.method(#execute, [movie]), 124 | returnValue: Future<_i3.Either<_i6.Failure, String>>.value( 125 | _FakeEither<_i6.Failure, String>())) 126 | as _i5.Future<_i3.Either<_i6.Failure, String>>); 127 | } 128 | -------------------------------------------------------------------------------- /test/presentation/provider/movie_list_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/domain/entities/movie.dart'; 3 | import 'package:ditonton/domain/usecases/get_now_playing_movies.dart'; 4 | import 'package:ditonton/common/failure.dart'; 5 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 6 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 7 | import 'package:ditonton/presentation/provider/movie_list_notifier.dart'; 8 | import 'package:ditonton/common/state_enum.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mockito/annotations.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import 'movie_list_notifier_test.mocks.dart'; 14 | 15 | @GenerateMocks([GetNowPlayingMovies, GetPopularMovies, GetTopRatedMovies]) 16 | void main() { 17 | late MovieListNotifier provider; 18 | late MockGetNowPlayingMovies mockGetNowPlayingMovies; 19 | late MockGetPopularMovies mockGetPopularMovies; 20 | late MockGetTopRatedMovies mockGetTopRatedMovies; 21 | late int listenerCallCount; 22 | 23 | setUp(() { 24 | listenerCallCount = 0; 25 | mockGetNowPlayingMovies = MockGetNowPlayingMovies(); 26 | mockGetPopularMovies = MockGetPopularMovies(); 27 | mockGetTopRatedMovies = MockGetTopRatedMovies(); 28 | provider = MovieListNotifier( 29 | getNowPlayingMovies: mockGetNowPlayingMovies, 30 | getPopularMovies: mockGetPopularMovies, 31 | getTopRatedMovies: mockGetTopRatedMovies, 32 | )..addListener(() { 33 | listenerCallCount += 1; 34 | }); 35 | }); 36 | 37 | final tMovie = Movie( 38 | adult: false, 39 | backdropPath: 'backdropPath', 40 | genreIds: [1, 2, 3], 41 | id: 1, 42 | originalTitle: 'originalTitle', 43 | overview: 'overview', 44 | popularity: 1, 45 | posterPath: 'posterPath', 46 | releaseDate: 'releaseDate', 47 | title: 'title', 48 | video: false, 49 | voteAverage: 1, 50 | voteCount: 1, 51 | ); 52 | final tMovieList = [tMovie]; 53 | 54 | group('now playing movies', () { 55 | test('initialState should be Empty', () { 56 | expect(provider.nowPlayingState, equals(RequestState.Empty)); 57 | }); 58 | 59 | test('should get data from the usecase', () async { 60 | // arrange 61 | when(mockGetNowPlayingMovies.execute()) 62 | .thenAnswer((_) async => Right(tMovieList)); 63 | // act 64 | provider.fetchNowPlayingMovies(); 65 | // assert 66 | verify(mockGetNowPlayingMovies.execute()); 67 | }); 68 | 69 | test('should change state to Loading when usecase is called', () { 70 | // arrange 71 | when(mockGetNowPlayingMovies.execute()) 72 | .thenAnswer((_) async => Right(tMovieList)); 73 | // act 74 | provider.fetchNowPlayingMovies(); 75 | // assert 76 | expect(provider.nowPlayingState, RequestState.Loading); 77 | }); 78 | 79 | test('should change movies when data is gotten successfully', () async { 80 | // arrange 81 | when(mockGetNowPlayingMovies.execute()) 82 | .thenAnswer((_) async => Right(tMovieList)); 83 | // act 84 | await provider.fetchNowPlayingMovies(); 85 | // assert 86 | expect(provider.nowPlayingState, RequestState.Loaded); 87 | expect(provider.nowPlayingMovies, tMovieList); 88 | expect(listenerCallCount, 2); 89 | }); 90 | 91 | test('should return error when data is unsuccessful', () async { 92 | // arrange 93 | when(mockGetNowPlayingMovies.execute()) 94 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 95 | // act 96 | await provider.fetchNowPlayingMovies(); 97 | // assert 98 | expect(provider.nowPlayingState, RequestState.Error); 99 | expect(provider.message, 'Server Failure'); 100 | expect(listenerCallCount, 2); 101 | }); 102 | }); 103 | 104 | group('popular movies', () { 105 | test('should change state to loading when usecase is called', () async { 106 | // arrange 107 | when(mockGetPopularMovies.execute()) 108 | .thenAnswer((_) async => Right(tMovieList)); 109 | // act 110 | provider.fetchPopularMovies(); 111 | // assert 112 | expect(provider.popularMoviesState, RequestState.Loading); 113 | // verify(provider.setState(RequestState.Loading)); 114 | }); 115 | 116 | test('should change movies data when data is gotten successfully', 117 | () async { 118 | // arrange 119 | when(mockGetPopularMovies.execute()) 120 | .thenAnswer((_) async => Right(tMovieList)); 121 | // act 122 | await provider.fetchPopularMovies(); 123 | // assert 124 | expect(provider.popularMoviesState, RequestState.Loaded); 125 | expect(provider.popularMovies, tMovieList); 126 | expect(listenerCallCount, 2); 127 | }); 128 | 129 | test('should return error when data is unsuccessful', () async { 130 | // arrange 131 | when(mockGetPopularMovies.execute()) 132 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 133 | // act 134 | await provider.fetchPopularMovies(); 135 | // assert 136 | expect(provider.popularMoviesState, RequestState.Error); 137 | expect(provider.message, 'Server Failure'); 138 | expect(listenerCallCount, 2); 139 | }); 140 | }); 141 | 142 | group('top rated movies', () { 143 | test('should change state to loading when usecase is called', () async { 144 | // arrange 145 | when(mockGetTopRatedMovies.execute()) 146 | .thenAnswer((_) async => Right(tMovieList)); 147 | // act 148 | provider.fetchTopRatedMovies(); 149 | // assert 150 | expect(provider.topRatedMoviesState, RequestState.Loading); 151 | }); 152 | 153 | test('should change movies data when data is gotten successfully', 154 | () async { 155 | // arrange 156 | when(mockGetTopRatedMovies.execute()) 157 | .thenAnswer((_) async => Right(tMovieList)); 158 | // act 159 | await provider.fetchTopRatedMovies(); 160 | // assert 161 | expect(provider.topRatedMoviesState, RequestState.Loaded); 162 | expect(provider.topRatedMovies, tMovieList); 163 | expect(listenerCallCount, 2); 164 | }); 165 | 166 | test('should return error when data is unsuccessful', () async { 167 | // arrange 168 | when(mockGetTopRatedMovies.execute()) 169 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 170 | // act 171 | await provider.fetchTopRatedMovies(); 172 | // assert 173 | expect(provider.topRatedMoviesState, RequestState.Error); 174 | expect(provider.message, 'Server Failure'); 175 | expect(listenerCallCount, 2); 176 | }); 177 | }); 178 | } 179 | -------------------------------------------------------------------------------- /test/presentation/provider/movie_list_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/provider/movie_list_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:dartz/dartz.dart' as _i3; 8 | import 'package:ditonton/common/failure.dart' as _i6; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i7; 10 | import 'package:ditonton/domain/repositories/movie_repository.dart' as _i2; 11 | import 'package:ditonton/domain/usecases/get_now_playing_movies.dart' as _i4; 12 | import 'package:ditonton/domain/usecases/get_popular_movies.dart' as _i8; 13 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart' as _i9; 14 | import 'package:mockito/mockito.dart' as _i1; 15 | 16 | // ignore_for_file: avoid_redundant_argument_values 17 | // ignore_for_file: comment_references 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 | 22 | class _FakeMovieRepository extends _i1.Fake implements _i2.MovieRepository {} 23 | 24 | class _FakeEither extends _i1.Fake implements _i3.Either {} 25 | 26 | /// A class which mocks [GetNowPlayingMovies]. 27 | /// 28 | /// See the documentation for Mockito's code generation for more information. 29 | class MockGetNowPlayingMovies extends _i1.Mock 30 | implements _i4.GetNowPlayingMovies { 31 | MockGetNowPlayingMovies() { 32 | _i1.throwOnMissingStub(this); 33 | } 34 | 35 | @override 36 | _i2.MovieRepository get repository => 37 | (super.noSuchMethod(Invocation.getter(#repository), 38 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 39 | @override 40 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 41 | (super.noSuchMethod(Invocation.method(#execute, []), 42 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 43 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 44 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 45 | } 46 | 47 | /// A class which mocks [GetPopularMovies]. 48 | /// 49 | /// See the documentation for Mockito's code generation for more information. 50 | class MockGetPopularMovies extends _i1.Mock implements _i8.GetPopularMovies { 51 | MockGetPopularMovies() { 52 | _i1.throwOnMissingStub(this); 53 | } 54 | 55 | @override 56 | _i2.MovieRepository get repository => 57 | (super.noSuchMethod(Invocation.getter(#repository), 58 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 59 | @override 60 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 61 | (super.noSuchMethod(Invocation.method(#execute, []), 62 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 63 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 64 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 65 | } 66 | 67 | /// A class which mocks [GetTopRatedMovies]. 68 | /// 69 | /// See the documentation for Mockito's code generation for more information. 70 | class MockGetTopRatedMovies extends _i1.Mock implements _i9.GetTopRatedMovies { 71 | MockGetTopRatedMovies() { 72 | _i1.throwOnMissingStub(this); 73 | } 74 | 75 | @override 76 | _i2.MovieRepository get repository => 77 | (super.noSuchMethod(Invocation.getter(#repository), 78 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 79 | @override 80 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 81 | (super.noSuchMethod(Invocation.method(#execute, []), 82 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 83 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 84 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 85 | } 86 | -------------------------------------------------------------------------------- /test/presentation/provider/movie_search_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/common/state_enum.dart'; 4 | import 'package:ditonton/domain/entities/movie.dart'; 5 | import 'package:ditonton/domain/usecases/search_movies.dart'; 6 | import 'package:ditonton/presentation/provider/movie_search_notifier.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import 'movie_search_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([SearchMovies]) 14 | void main() { 15 | late MovieSearchNotifier provider; 16 | late MockSearchMovies mockSearchMovies; 17 | late int listenerCallCount; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockSearchMovies = MockSearchMovies(); 22 | provider = MovieSearchNotifier(searchMovies: mockSearchMovies) 23 | ..addListener(() { 24 | listenerCallCount += 1; 25 | }); 26 | }); 27 | 28 | final tMovieModel = Movie( 29 | adult: false, 30 | backdropPath: '/muth4OYamXf41G2evdrLEg8d3om.jpg', 31 | genreIds: [14, 28], 32 | id: 557, 33 | originalTitle: 'Spider-Man', 34 | overview: 35 | 'After being bitten by a genetically altered spider, nerdy high school student Peter Parker is endowed with amazing powers to become the Amazing superhero known as Spider-Man.', 36 | popularity: 60.441, 37 | posterPath: '/rweIrveL43TaxUN0akQEaAXL6x0.jpg', 38 | releaseDate: '2002-05-01', 39 | title: 'Spider-Man', 40 | video: false, 41 | voteAverage: 7.2, 42 | voteCount: 13507, 43 | ); 44 | final tMovieList = [tMovieModel]; 45 | final tQuery = 'spiderman'; 46 | 47 | group('search movies', () { 48 | test('should change state to loading when usecase is called', () async { 49 | // arrange 50 | when(mockSearchMovies.execute(tQuery)) 51 | .thenAnswer((_) async => Right(tMovieList)); 52 | // act 53 | provider.fetchMovieSearch(tQuery); 54 | // assert 55 | expect(provider.state, RequestState.Loading); 56 | }); 57 | 58 | test('should change search result data when data is gotten successfully', 59 | () async { 60 | // arrange 61 | when(mockSearchMovies.execute(tQuery)) 62 | .thenAnswer((_) async => Right(tMovieList)); 63 | // act 64 | await provider.fetchMovieSearch(tQuery); 65 | // assert 66 | expect(provider.state, RequestState.Loaded); 67 | expect(provider.searchResult, tMovieList); 68 | expect(listenerCallCount, 2); 69 | }); 70 | 71 | test('should return error when data is unsuccessful', () async { 72 | // arrange 73 | when(mockSearchMovies.execute(tQuery)) 74 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 75 | // act 76 | await provider.fetchMovieSearch(tQuery); 77 | // assert 78 | expect(provider.state, RequestState.Error); 79 | expect(provider.message, 'Server Failure'); 80 | expect(listenerCallCount, 2); 81 | }); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /test/presentation/provider/movie_search_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/test/presentation/provider/movie_search_notifier_test.dart. 3 | // Do not manually edit this file. 4 | 5 | import 'dart:async' as _i5; 6 | 7 | import 'package:dartz/dartz.dart' as _i3; 8 | import 'package:ditonton/common/failure.dart' as _i6; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i7; 10 | import 'package:ditonton/domain/repositories/movie_repository.dart' as _i2; 11 | import 'package:ditonton/domain/usecases/search_movies.dart' as _i4; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: invalid_use_of_visible_for_testing_member 17 | // ignore_for_file: prefer_const_constructors 18 | // ignore_for_file: unnecessary_parenthesis 19 | 20 | class _FakeMovieRepository extends _i1.Fake implements _i2.MovieRepository {} 21 | 22 | class _FakeEither extends _i1.Fake implements _i3.Either {} 23 | 24 | /// A class which mocks [SearchMovies]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockSearchMovies extends _i1.Mock implements _i4.SearchMovies { 28 | MockSearchMovies() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i2.MovieRepository get repository => 34 | (super.noSuchMethod(Invocation.getter(#repository), 35 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 36 | @override 37 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute(String? query) => 38 | (super.noSuchMethod(Invocation.method(#execute, [query]), 39 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 40 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 41 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 42 | } 43 | -------------------------------------------------------------------------------- /test/presentation/provider/popular_movies_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/common/state_enum.dart'; 4 | import 'package:ditonton/domain/entities/movie.dart'; 5 | import 'package:ditonton/domain/usecases/get_popular_movies.dart'; 6 | import 'package:ditonton/presentation/provider/popular_movies_notifier.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import 'popular_movies_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([GetPopularMovies]) 14 | void main() { 15 | late MockGetPopularMovies mockGetPopularMovies; 16 | late PopularMoviesNotifier notifier; 17 | late int listenerCallCount; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockGetPopularMovies = MockGetPopularMovies(); 22 | notifier = PopularMoviesNotifier(mockGetPopularMovies) 23 | ..addListener(() { 24 | listenerCallCount++; 25 | }); 26 | }); 27 | 28 | final tMovie = Movie( 29 | adult: false, 30 | backdropPath: 'backdropPath', 31 | genreIds: [1, 2, 3], 32 | id: 1, 33 | originalTitle: 'originalTitle', 34 | overview: 'overview', 35 | popularity: 1, 36 | posterPath: 'posterPath', 37 | releaseDate: 'releaseDate', 38 | title: 'title', 39 | video: false, 40 | voteAverage: 1, 41 | voteCount: 1, 42 | ); 43 | 44 | final tMovieList = [tMovie]; 45 | 46 | test('should change state to loading when usecase is called', () async { 47 | // arrange 48 | when(mockGetPopularMovies.execute()) 49 | .thenAnswer((_) async => Right(tMovieList)); 50 | // act 51 | notifier.fetchPopularMovies(); 52 | // assert 53 | expect(notifier.state, RequestState.Loading); 54 | expect(listenerCallCount, 1); 55 | }); 56 | 57 | test('should change movies data when data is gotten successfully', () async { 58 | // arrange 59 | when(mockGetPopularMovies.execute()) 60 | .thenAnswer((_) async => Right(tMovieList)); 61 | // act 62 | await notifier.fetchPopularMovies(); 63 | // assert 64 | expect(notifier.state, RequestState.Loaded); 65 | expect(notifier.movies, tMovieList); 66 | expect(listenerCallCount, 2); 67 | }); 68 | 69 | test('should return error when data is unsuccessful', () async { 70 | // arrange 71 | when(mockGetPopularMovies.execute()) 72 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 73 | // act 74 | await notifier.fetchPopularMovies(); 75 | // assert 76 | expect(notifier.state, RequestState.Error); 77 | expect(notifier.message, 'Server Failure'); 78 | expect(listenerCallCount, 2); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /test/presentation/provider/popular_movies_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/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:dartz/dartz.dart' as _i3; 8 | import 'package:ditonton/common/failure.dart' as _i6; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i7; 10 | import 'package:ditonton/domain/repositories/movie_repository.dart' as _i2; 11 | import 'package:ditonton/domain/usecases/get_popular_movies.dart' as _i4; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: invalid_use_of_visible_for_testing_member 17 | // ignore_for_file: prefer_const_constructors 18 | // ignore_for_file: unnecessary_parenthesis 19 | 20 | class _FakeMovieRepository extends _i1.Fake implements _i2.MovieRepository {} 21 | 22 | class _FakeEither extends _i1.Fake implements _i3.Either {} 23 | 24 | /// A class which mocks [GetPopularMovies]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockGetPopularMovies extends _i1.Mock implements _i4.GetPopularMovies { 28 | MockGetPopularMovies() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i2.MovieRepository get repository => 34 | (super.noSuchMethod(Invocation.getter(#repository), 35 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 36 | @override 37 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 38 | (super.noSuchMethod(Invocation.method(#execute, []), 39 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 40 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 41 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 42 | } 43 | -------------------------------------------------------------------------------- /test/presentation/provider/top_rated_movies_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/common/state_enum.dart'; 4 | import 'package:ditonton/domain/entities/movie.dart'; 5 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart'; 6 | import 'package:ditonton/presentation/provider/top_rated_movies_notifier.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | import 'top_rated_movies_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([GetTopRatedMovies]) 14 | void main() { 15 | late MockGetTopRatedMovies mockGetTopRatedMovies; 16 | late TopRatedMoviesNotifier notifier; 17 | late int listenerCallCount; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockGetTopRatedMovies = MockGetTopRatedMovies(); 22 | notifier = TopRatedMoviesNotifier(getTopRatedMovies: mockGetTopRatedMovies) 23 | ..addListener(() { 24 | listenerCallCount++; 25 | }); 26 | }); 27 | 28 | final tMovie = Movie( 29 | adult: false, 30 | backdropPath: 'backdropPath', 31 | genreIds: [1, 2, 3], 32 | id: 1, 33 | originalTitle: 'originalTitle', 34 | overview: 'overview', 35 | popularity: 1, 36 | posterPath: 'posterPath', 37 | releaseDate: 'releaseDate', 38 | title: 'title', 39 | video: false, 40 | voteAverage: 1, 41 | voteCount: 1, 42 | ); 43 | 44 | final tMovieList = [tMovie]; 45 | 46 | test('should change state to loading when usecase is called', () async { 47 | // arrange 48 | when(mockGetTopRatedMovies.execute()) 49 | .thenAnswer((_) async => Right(tMovieList)); 50 | // act 51 | notifier.fetchTopRatedMovies(); 52 | // assert 53 | expect(notifier.state, RequestState.Loading); 54 | expect(listenerCallCount, 1); 55 | }); 56 | 57 | test('should change movies data when data is gotten successfully', () async { 58 | // arrange 59 | when(mockGetTopRatedMovies.execute()) 60 | .thenAnswer((_) async => Right(tMovieList)); 61 | // act 62 | await notifier.fetchTopRatedMovies(); 63 | // assert 64 | expect(notifier.state, RequestState.Loaded); 65 | expect(notifier.movies, tMovieList); 66 | expect(listenerCallCount, 2); 67 | }); 68 | 69 | test('should return error when data is unsuccessful', () async { 70 | // arrange 71 | when(mockGetTopRatedMovies.execute()) 72 | .thenAnswer((_) async => Left(ServerFailure('Server Failure'))); 73 | // act 74 | await notifier.fetchTopRatedMovies(); 75 | // assert 76 | expect(notifier.state, RequestState.Error); 77 | expect(notifier.message, 'Server Failure'); 78 | expect(listenerCallCount, 2); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /test/presentation/provider/top_rated_movies_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/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:dartz/dartz.dart' as _i3; 8 | import 'package:ditonton/common/failure.dart' as _i6; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i7; 10 | import 'package:ditonton/domain/repositories/movie_repository.dart' as _i2; 11 | import 'package:ditonton/domain/usecases/get_top_rated_movies.dart' as _i4; 12 | import 'package:mockito/mockito.dart' as _i1; 13 | 14 | // ignore_for_file: avoid_redundant_argument_values 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: invalid_use_of_visible_for_testing_member 17 | // ignore_for_file: prefer_const_constructors 18 | // ignore_for_file: unnecessary_parenthesis 19 | 20 | class _FakeMovieRepository extends _i1.Fake implements _i2.MovieRepository {} 21 | 22 | class _FakeEither extends _i1.Fake implements _i3.Either {} 23 | 24 | /// A class which mocks [GetTopRatedMovies]. 25 | /// 26 | /// See the documentation for Mockito's code generation for more information. 27 | class MockGetTopRatedMovies extends _i1.Mock implements _i4.GetTopRatedMovies { 28 | MockGetTopRatedMovies() { 29 | _i1.throwOnMissingStub(this); 30 | } 31 | 32 | @override 33 | _i2.MovieRepository get repository => 34 | (super.noSuchMethod(Invocation.getter(#repository), 35 | returnValue: _FakeMovieRepository()) as _i2.MovieRepository); 36 | @override 37 | _i5.Future<_i3.Either<_i6.Failure, List<_i7.Movie>>> execute() => 38 | (super.noSuchMethod(Invocation.method(#execute, []), 39 | returnValue: Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>.value( 40 | _FakeEither<_i6.Failure, List<_i7.Movie>>())) as _i5 41 | .Future<_i3.Either<_i6.Failure, List<_i7.Movie>>>); 42 | } 43 | -------------------------------------------------------------------------------- /test/presentation/provider/watchlist_movie_notifier_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:ditonton/common/failure.dart'; 3 | import 'package:ditonton/common/state_enum.dart'; 4 | import 'package:ditonton/domain/usecases/get_watchlist_movies.dart'; 5 | import 'package:ditonton/presentation/provider/watchlist_movie_notifier.dart'; 6 | import 'package:flutter_test/flutter_test.dart'; 7 | import 'package:mockito/annotations.dart'; 8 | import 'package:mockito/mockito.dart'; 9 | 10 | import '../../dummy_data/dummy_objects.dart'; 11 | import 'watchlist_movie_notifier_test.mocks.dart'; 12 | 13 | @GenerateMocks([GetWatchlistMovies]) 14 | void main() { 15 | late WatchlistMovieNotifier provider; 16 | late MockGetWatchlistMovies mockGetWatchlistMovies; 17 | late int listenerCallCount; 18 | 19 | setUp(() { 20 | listenerCallCount = 0; 21 | mockGetWatchlistMovies = MockGetWatchlistMovies(); 22 | provider = WatchlistMovieNotifier( 23 | getWatchlistMovies: mockGetWatchlistMovies, 24 | )..addListener(() { 25 | listenerCallCount += 1; 26 | }); 27 | }); 28 | 29 | test('should change movies data when data is gotten successfully', () async { 30 | // arrange 31 | when(mockGetWatchlistMovies.execute()) 32 | .thenAnswer((_) async => Right([testWatchlistMovie])); 33 | // act 34 | await provider.fetchWatchlistMovies(); 35 | // assert 36 | expect(provider.watchlistState, RequestState.Loaded); 37 | expect(provider.watchlistMovies, [testWatchlistMovie]); 38 | expect(listenerCallCount, 2); 39 | }); 40 | 41 | test('should return error when data is unsuccessful', () async { 42 | // arrange 43 | when(mockGetWatchlistMovies.execute()) 44 | .thenAnswer((_) async => Left(DatabaseFailure("Can't get data"))); 45 | // act 46 | await provider.fetchWatchlistMovies(); 47 | // assert 48 | expect(provider.watchlistState, RequestState.Error); 49 | expect(provider.message, "Can't get data"); 50 | expect(listenerCallCount, 2); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/presentation/provider/watchlist_movie_notifier_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.0.8 from annotations 2 | // in ditonton/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:dartz/dartz.dart' as _i2; 8 | import 'package:ditonton/common/failure.dart' as _i5; 9 | import 'package:ditonton/domain/entities/movie.dart' as _i6; 10 | import 'package:ditonton/domain/usecases/get_watchlist_movies.dart' as _i3; 11 | import 'package:mockito/mockito.dart' as _i1; 12 | 13 | // ignore_for_file: avoid_redundant_argument_values 14 | // ignore_for_file: comment_references 15 | // ignore_for_file: invalid_use_of_visible_for_testing_member 16 | // ignore_for_file: prefer_const_constructors 17 | // ignore_for_file: unnecessary_parenthesis 18 | 19 | class _FakeEither extends _i1.Fake implements _i2.Either {} 20 | 21 | /// A class which mocks [GetWatchlistMovies]. 22 | /// 23 | /// See the documentation for Mockito's code generation for more information. 24 | class MockGetWatchlistMovies extends _i1.Mock 25 | implements _i3.GetWatchlistMovies { 26 | MockGetWatchlistMovies() { 27 | _i1.throwOnMissingStub(this); 28 | } 29 | 30 | @override 31 | _i4.Future<_i2.Either<_i5.Failure, List<_i6.Movie>>> execute() => 32 | (super.noSuchMethod(Invocation.method(#execute, []), 33 | returnValue: Future<_i2.Either<_i5.Failure, List<_i6.Movie>>>.value( 34 | _FakeEither<_i5.Failure, List<_i6.Movie>>())) as _i4 35 | .Future<_i2.Either<_i5.Failure, List<_i6.Movie>>>); 36 | } 37 | --------------------------------------------------------------------------------