├── .editorconfig ├── .fvmrc ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── advanced_flutter │ │ │ │ └── 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 ├── backend ├── index.js ├── package-lock.json └── package.json ├── docs ├── dependencies.drawio ├── event.json └── next_event.md ├── 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 ├── domain │ └── entities │ │ ├── errors.dart │ │ ├── next_event.dart │ │ └── next_event_player.dart ├── infra │ ├── api │ │ ├── adapters │ │ │ └── http_adapter.dart │ │ ├── clients │ │ │ ├── authorized_http_get_client.dart │ │ │ └── http_get_client.dart │ │ └── repositories │ │ │ └── load_next_event_api_repo.dart │ ├── cache │ │ ├── adapters │ │ │ └── cache_manager_adapter.dart │ │ ├── clients │ │ │ ├── cache_get_client.dart │ │ │ └── cache_save_client.dart │ │ └── repositories │ │ │ └── load_next_event_cache_repo.dart │ ├── mappers │ │ ├── mapper.dart │ │ ├── next_event_mapper.dart │ │ └── next_event_player_mapper.dart │ ├── respositories │ │ └── load_next_event_from_api_with_cache_fallback_repo.dart │ └── types │ │ └── json.dart ├── main │ ├── factories │ │ ├── infra │ │ │ ├── api │ │ │ │ ├── adapters │ │ │ │ │ └── http_adapter_factory.dart │ │ │ │ ├── clients │ │ │ │ │ ├── api_url_factory.dart │ │ │ │ │ └── authorized_http_get_client_factory.dart │ │ │ │ └── repositories │ │ │ │ │ └── load_next_event_api_repo_factory.dart │ │ │ ├── cache │ │ │ │ ├── adapters │ │ │ │ │ └── cache_manager_adapter_factory.dart │ │ │ │ └── repositories │ │ │ │ │ └── load_next_event_cache_repo_factory.dart │ │ │ ├── mappers │ │ │ │ ├── next_event_mapper_factory.dart │ │ │ │ └── next_event_player_mapper_factory.dart │ │ │ └── repositories │ │ │ │ └── load_next_event_from_api_with_cache_fallback_repo_factory.dart │ │ └── ui │ │ │ └── pages │ │ │ └── next_event_page_factory.dart │ └── main.dart ├── presentation │ ├── presenters │ │ └── next_event_presenter.dart │ ├── rx │ │ └── next_event_rx_presenter.dart │ └── viewmodels │ │ ├── next_event_player_viewmodel.dart │ │ └── next_event_viewmodel.dart └── ui │ ├── components │ ├── player_photo.dart │ ├── player_position.dart │ └── player_status.dart │ └── pages │ └── next_event_page.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── domain ├── entities │ └── next_event_player_test.dart └── mocks │ └── next_event_loader_spy.dart ├── e2e └── next_event_page_test.dart ├── infra ├── api │ ├── adapters │ │ └── http_adapter_test.dart │ ├── clients │ │ └── authorized_http_get_client_test.dart │ ├── mocks │ │ ├── client_spy.dart │ │ └── http_get_client_spy.dart │ └── repositories │ │ └── load_next_event_api_repo_test.dart ├── cache │ ├── adapters │ │ └── cache_manager_adapter_test.dart │ ├── mocks │ │ ├── cache_get_client_spy.dart │ │ ├── cache_manager_spy.dart │ │ ├── cache_save_client_mock.dart │ │ └── file_spy.dart │ └── repositories │ │ └── load_next_event_cache_repo_test.dart ├── mappers │ ├── next_event_mapper_test.dart │ └── next_event_player_mapper_test.dart ├── mocks │ ├── list_mapper_spy.dart │ ├── load_next_event_repo_spy.dart │ └── mapper_spy.dart └── repositories │ └── load_next_event_from_api_with_cache_fallback_repo_test.dart ├── mocks └── fakes.dart ├── presentation ├── mocks │ └── next_event_presenter_spy.dart ├── rx │ └── next_event_rx_presenter_test.dart └── viewmodels │ └── next_event_player_viewmodel_test.dart └── ui ├── components ├── player_photo_test.dart ├── player_position_test.dart └── player_status_test.dart └── pages └── next_event_page_test.dart /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.27.3" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | # FVM Version Cache 49 | .fvm/ 50 | 51 | # Backend 52 | /backend/node_modules 53 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 796c8ef79279f9c774545b3771238c3098dbefab 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 17 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 18 | - platform: android 19 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 20 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 21 | - platform: ios 22 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 23 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 24 | - platform: linux 25 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 26 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 27 | - platform: macos 28 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 29 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 30 | - platform: web 31 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 32 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 33 | - platform: windows 34 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 35 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "advanced_flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "lib/main/main.dart" 12 | }, 13 | { 14 | "name": "advanced_flutter (profile mode)", 15 | "request": "launch", 16 | "type": "dart", 17 | "flutterMode": "profile" 18 | }, 19 | { 20 | "name": "advanced_flutter (release mode)", 21 | "request": "launch", 22 | "type": "dart", 23 | "flutterMode": "release" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/versions/3.27.3" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # advanced_flutter 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /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 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.example.advanced_flutter" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.advanced_flutter" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/advanced_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.advanced_flutter 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | app.get('/api/groups/:groupId/next_event', (req, res) => { 4 | if (req.headers['authorization'] !== 'valid_token') return res.status(401).send(Error('Unauthorized')) 5 | if (req.params['groupId'] !== 'valid_id') return res.status(400).send(Error('Invalid id')) 6 | res.send({ 7 | "id": "1", 8 | "groupName": "Pelada Chega+", 9 | "date": "2024-01-11T11:10:00.000Z", 10 | "players": [{ 11 | "id": "1", 12 | "name": "Cristiano Ronaldo", 13 | "position": "forward", 14 | "isConfirmed": true, 15 | "confirmationDate": "2024-01-10T11:07:00.000Z" 16 | }, { 17 | "id": "2", 18 | "name": "Lionel Messi", 19 | "position": "midfielder", 20 | "isConfirmed": true, 21 | "confirmationDate": "2024-01-10T11:08:00.000Z" 22 | }, { 23 | "id": "3", 24 | "name": "Dida", 25 | "position": "goalkeeper", 26 | "isConfirmed": true, 27 | "confirmationDate": "2024-01-10T09:10:00.000Z" 28 | }, { 29 | "id": "4", 30 | "name": "Romario", 31 | "position": "forward", 32 | "isConfirmed": true, 33 | "confirmationDate": "2024-01-10T11:10:00.000Z" 34 | }, { 35 | "id": "5", 36 | "name": "Claudio Gamarra", 37 | "position": "defender", 38 | "isConfirmed": false, 39 | "confirmationDate": "2024-01-10T13:10:00.000Z" 40 | }, { 41 | "id": "6", 42 | "name": "Diego Forlan", 43 | "position": "defender", 44 | "isConfirmed": false, 45 | "confirmationDate": "2024-01-10T14:10:00.000Z" 46 | }, { 47 | "id": "7", 48 | "name": "Zé Ninguém", 49 | "isConfirmed": false 50 | }, { 51 | "id": "8", 52 | "name": "Rodrigo Manguinho", 53 | "isConfirmed": false 54 | }, { 55 | "id": "9", 56 | "name": "Claudio Taffarel", 57 | "position": "goalkeeper", 58 | "isConfirmed": true, 59 | "confirmationDate": "2024-01-10T09:15:00.000Z" 60 | }] 61 | }) 62 | }) 63 | app.listen(8080, () => console.log('Server running at http://localhost:8080')) 64 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.19.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/dependencies.drawio: -------------------------------------------------------------------------------- 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /docs/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "groupName": "Pelada Chega+", 4 | "date": "2024-01-11T11:10:00.000Z", 5 | "players": [{ 6 | "id": "1", 7 | "name": "Cristiano Ronaldo", 8 | "position": "forward", 9 | "isConfirmed": true, 10 | "confirmationDate": "2024-01-10T11:07:00.000Z" 11 | }, { 12 | "id": "2", 13 | "name": "Lionel Messi", 14 | "position": "midfielder", 15 | "isConfirmed": true, 16 | "confirmationDate": "2024-01-10T11:08:00.000Z" 17 | }, { 18 | "id": "3", 19 | "name": "Dida", 20 | "position": "goalkeeper", 21 | "isConfirmed": true, 22 | "confirmationDate": "2024-01-10T09:10:00.000Z" 23 | }, { 24 | "id": "4", 25 | "name": "Romario", 26 | "position": "forward", 27 | "isConfirmed": true, 28 | "confirmationDate": "2024-01-10T11:10:00.000Z" 29 | }, { 30 | "id": "5", 31 | "name": "Claudio Gamarra", 32 | "position": "defender", 33 | "isConfirmed": false, 34 | "confirmationDate": "2024-01-10T13:10:00.000Z" 35 | }, { 36 | "id": "6", 37 | "name": "Diego Forlan", 38 | "position": "defender", 39 | "isConfirmed": false, 40 | "confirmationDate": "2024-01-10T14:10:00.000Z" 41 | }, { 42 | "id": "7", 43 | "name": "Zé Ninguém", 44 | "isConfirmed": false 45 | }, { 46 | "id": "8", 47 | "name": "Rodrigo Manguinho", 48 | "isConfirmed": false 49 | }, { 50 | "id": "9", 51 | "name": "Claudio Taffarel", 52 | "position": "goalkeeper", 53 | "isConfirmed": true, 54 | "confirmationDate": "2024-01-10T09:15:00.000Z" 55 | }] 56 | } 57 | -------------------------------------------------------------------------------- /docs/next_event.md: -------------------------------------------------------------------------------- 1 | # Próximo Evento 2 | - Obtem dados do evento (nome do grupo, data do evento e uma lista de jogadores) 3 | - Os jogadores que não confirmaram presença devem ser exibidos em ordem alfabética 4 | - Os jogadores que confirmaram presença devem ser exibidos por ordem de confirmação 5 | - Separar quem confirmou dentro e fora 6 | - Separar quem confirmou dentro por posição (goleiros e jogadores de linha) 7 | 8 | # Jogador 9 | - Cada jogador tem: nome, foto, posição, status de confirmação e data de confirmação 10 | - Caso o jogador não tenha foto exibir as iniciais dele no local da foto 11 | - As iniciais em nomes com sobrenome devem sempre ser a primeira letra do primeiro e do último nome 12 | - Caso não tenha sobrenome, mostrar as 2 primeiras letras do primeiro nome 13 | - Se o nome tiver apenas 1 letra, mostrar essa letra na inicial 14 | - Se o nome estiver vazio, mostrar um hífen (-) nas iniciais 15 | - Ignorar espaços em branco, no final, no início ou entre os nomes 16 | - Sempre mostrar as iniciais em maiúsculo 17 | 18 | # API 19 | - GET para a rota https://domain.com/api/groups/:groupId/next_event 20 | - Enviar headers "content-type" e "accept", ambos com valor "application/json" 21 | - Retornar Unexpected Error em caso de 400, 403, 404 e 500 22 | - Retornar Session Expired Error em caso de 401 23 | - Retornar dados do evento em caso de 200 24 | 25 | # CacheManagerAdapter Get 26 | - Precisa executar o método getFileFromCache com a key correta 27 | - Precisa executar o método getFileFromCache apenas uma vez 28 | - Retornar NULL se o FileInfo for NULL 29 | - Retornar NULL se o cache estiver vencido 30 | - Precisa executar o método exists apenas uma vez 31 | - Retornar NULL se o File não existir 32 | - Precisa executar o método readAsString apenas uma vez 33 | - Retornar NULL se o Cache for inválido 34 | - Retornar NULL se o método getFileFromCache der erro 35 | - Retornar NULL se o método exists der erro 36 | - Retornar NULL se o método readAsString der erro 37 | - Retornar um JSON se o cache for válido 38 | 39 | # CacheManagerAdapter Save 40 | - Precisa executar o método putFile com a url correta 41 | - Precisa executar o método putFile com a fileExtension correto 42 | - Precisa executar o método putFile com a fileBytes correto 43 | - Precisa executar o método putFile apenas uma vez 44 | - Retornar UnexpectedError se o método putFile der erro 45 | -------------------------------------------------------------------------------- /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: 080d55be775b7414fd5a5ef3ac137b97b097e564 26 | sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 27 | 28 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 29 | 30 | COCOAPODS: 1.16.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 UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmanguinho/advanced-flutter/f5f78319b1b23f4f4f55c262bee69c99554b2d19/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 | Advanced Flutter 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | advanced_flutter 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 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /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/domain/entities/errors.dart: -------------------------------------------------------------------------------- 1 | sealed class DomainError {} 2 | final class UnexpectedError implements DomainError {} 3 | final class SessionExpiredError implements DomainError {} 4 | -------------------------------------------------------------------------------- /lib/domain/entities/next_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | 3 | final class NextEvent { 4 | final String groupName; 5 | final DateTime date; 6 | final List players; 7 | 8 | const NextEvent({ 9 | required this.groupName, 10 | required this.date, 11 | required this.players 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/entities/next_event_player.dart: -------------------------------------------------------------------------------- 1 | final class NextEventPlayer { 2 | final String id; 3 | final String name; 4 | final String initials; 5 | final String? photo; 6 | final String? position; 7 | final bool isConfirmed; 8 | final DateTime? confirmationDate; 9 | 10 | const NextEventPlayer._({ 11 | required this.id, 12 | required this.name, 13 | required this.initials, 14 | required this.isConfirmed, 15 | this.photo, 16 | this.position, 17 | this.confirmationDate 18 | }); 19 | 20 | factory NextEventPlayer({ 21 | required String id, 22 | required String name, 23 | required bool isConfirmed, 24 | String? photo, 25 | String? position, 26 | DateTime? confirmationDate 27 | }) => NextEventPlayer._( 28 | id: id, 29 | name: name, 30 | photo: photo, 31 | position: position, 32 | initials: _getInitials(name), 33 | isConfirmed: isConfirmed, 34 | confirmationDate: confirmationDate 35 | ); 36 | 37 | static String _getInitials(String name) { 38 | final names = name.toUpperCase().trim().split(' '); 39 | final firstChar = names.first.split('').firstOrNull ?? '-'; 40 | final lastChar = names.last.split('').elementAtOrNull(names.length == 1 ? 1 : 0) ?? ''; 41 | return '$firstChar$lastChar'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/infra/api/adapters/http_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:advanced_flutter/domain/entities/errors.dart'; 4 | import 'package:advanced_flutter/infra/api/clients/http_get_client.dart'; 5 | import 'package:advanced_flutter/infra/types/json.dart'; 6 | 7 | import 'package:dartx/dartx.dart'; 8 | import 'package:http/http.dart'; 9 | 10 | final class HttpAdapter implements HttpGetClient { 11 | final Client client; 12 | 13 | const HttpAdapter({ 14 | required this.client 15 | }); 16 | 17 | @override 18 | Future get({ required String url, Json? headers, Json? params, Json? queryString }) async { 19 | final response = await client.get( 20 | _buildUri(url: url, params: params, queryString: queryString), 21 | headers: _buildHeaders(url: url, headers: headers) 22 | ); 23 | return _handleResponse(response); 24 | } 25 | 26 | dynamic _handleResponse(Response response) { 27 | switch (response.statusCode) { 28 | case 200: { 29 | if (response.body.isEmpty) return null; 30 | return jsonDecode(response.body); 31 | } 32 | case 204: return null; 33 | case 401: throw SessionExpiredError(); 34 | default: throw UnexpectedError(); 35 | } 36 | } 37 | 38 | Map _buildHeaders({ required String url, Json? headers }) { 39 | final defaultHeaders = { 'content-type': 'application/json', 'accept': 'application/json' }; 40 | return defaultHeaders..addAll({ for (final key in (headers ?? {}).keys) key: headers![key].toString() }); 41 | } 42 | 43 | Uri _buildUri({ required String url, Json? params, Json? queryString }) { 44 | url = params?.keys.fold(url, (result, key) => result.replaceFirst(':$key', params[key]?.toString() ?? '')).removeSuffix('/') ?? url; 45 | url = queryString?.keys.fold('$url?', (result, key) => '$result$key=${queryString[key]}&').removeSuffix('&') ?? url; 46 | return Uri.parse(url); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/infra/api/clients/authorized_http_get_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/clients/http_get_client.dart'; 2 | import 'package:advanced_flutter/infra/cache/clients/cache_get_client.dart'; 3 | import 'package:advanced_flutter/infra/types/json.dart'; 4 | 5 | final class AuthorizedHttpGetClient implements HttpGetClient { 6 | final CacheGetClient cacheClient; 7 | final HttpGetClient httpClient; 8 | 9 | const AuthorizedHttpGetClient({ 10 | required this.cacheClient, 11 | required this.httpClient 12 | }); 13 | 14 | @override 15 | Future get({ required String url, Json? params, Json? queryString, Json? headers }) async { 16 | final user = await cacheClient.get(key: 'current_user'); 17 | if (user?['accessToken'] != null) headers = (headers ?? {})..addAll({ 'authorization': user['accessToken'] }); 18 | return httpClient.get(url: url, params: params, queryString: queryString, headers: headers); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/infra/api/clients/http_get_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/types/json.dart'; 2 | 3 | abstract interface class HttpGetClient { 4 | Future get({ required String url, Json? headers, Json? params, Json? queryString }); 5 | } 6 | -------------------------------------------------------------------------------- /lib/infra/api/repositories/load_next_event_api_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/api/clients/http_get_client.dart'; 4 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 5 | 6 | final class LoadNextEventApiRepository { 7 | final HttpGetClient httpClient; 8 | final String url; 9 | final DtoMapper mapper; 10 | 11 | const LoadNextEventApiRepository({ 12 | required this.httpClient, 13 | required this.url, 14 | required this.mapper 15 | }); 16 | 17 | Future loadNextEvent({ required String groupId }) async { 18 | final json = await httpClient.get(url: url, params: { 'groupId': groupId }); 19 | if (json == null) throw UnexpectedError(); 20 | return mapper.toDto(json); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/infra/cache/adapters/cache_manager_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:advanced_flutter/domain/entities/errors.dart'; 4 | import 'package:advanced_flutter/infra/cache/clients/cache_get_client.dart'; 5 | import 'package:advanced_flutter/infra/cache/clients/cache_save_client.dart'; 6 | 7 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 8 | 9 | final class CacheManagerAdapter implements CacheGetClient, CacheSaveClient { 10 | final BaseCacheManager client; 11 | 12 | const CacheManagerAdapter({ 13 | required this.client 14 | }); 15 | 16 | @override 17 | Future get({ required String key }) async { 18 | try { 19 | final info = await client.getFileFromCache(key); 20 | if (info?.validTill.isBefore(DateTime.now()) != false || !await info!.file.exists()) return null; 21 | final data = await info.file.readAsString(); 22 | return jsonDecode(data); 23 | } catch (err) { 24 | return null; 25 | } 26 | } 27 | 28 | @override 29 | Future save({ required String key, required dynamic value }) async { 30 | try { 31 | await client.putFile(key, utf8.encode(jsonEncode(value)), fileExtension: 'json'); 32 | } catch (err) { 33 | throw UnexpectedError(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/infra/cache/clients/cache_get_client.dart: -------------------------------------------------------------------------------- 1 | abstract interface class CacheGetClient { 2 | Future get({ required String key }); 3 | } 4 | -------------------------------------------------------------------------------- /lib/infra/cache/clients/cache_save_client.dart: -------------------------------------------------------------------------------- 1 | abstract interface class CacheSaveClient { 2 | Future save({ required String key, required dynamic value }); 3 | } 4 | -------------------------------------------------------------------------------- /lib/infra/cache/repositories/load_next_event_cache_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/cache/clients/cache_get_client.dart'; 4 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 5 | 6 | final class LoadNextEventCacheRepository { 7 | final CacheGetClient cacheClient; 8 | final String key; 9 | final DtoMapper mapper; 10 | 11 | const LoadNextEventCacheRepository({ 12 | required this.cacheClient, 13 | required this.key, 14 | required this.mapper 15 | }); 16 | 17 | Future loadNextEvent({ required String groupId }) async { 18 | final json = await cacheClient.get(key: '$key:$groupId'); 19 | if (json == null) throw UnexpectedError(); 20 | return mapper.toDto(json); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/infra/mappers/mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/types/json.dart'; 2 | 3 | abstract interface class DtoMapper { 4 | Dto toDto(Json json); 5 | } 6 | 7 | abstract interface class JsonMapper { 8 | Json toJson(Dto dto); 9 | } 10 | 11 | abstract interface class Mapper implements DtoMapper, JsonMapper {} 12 | 13 | mixin DtoListMapper implements DtoMapper { 14 | List toDtoList(dynamic arr) => arr.map(toDto).toList(); 15 | } 16 | 17 | mixin JsonArrMapper implements JsonMapper { 18 | JsonArr toJsonArr(List list) => list.map(toJson).toList(); 19 | } 20 | 21 | abstract base class ListMapper with DtoListMapper, JsonArrMapper {} 22 | -------------------------------------------------------------------------------- /lib/infra/mappers/next_event_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 3 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 4 | import 'package:advanced_flutter/infra/types/json.dart'; 5 | 6 | final class NextEventMapper implements Mapper { 7 | final ListMapper playerMapper; 8 | 9 | const NextEventMapper({ 10 | required this.playerMapper 11 | }); 12 | 13 | @override 14 | NextEvent toDto(Json json) => NextEvent( 15 | groupName: json['groupName'], 16 | date: DateTime.parse(json['date']), 17 | players: playerMapper.toDtoList(json['players']) 18 | ); 19 | 20 | @override 21 | Json toJson(NextEvent dto) => { 22 | 'groupName': dto.groupName, 23 | 'date': dto.date.toIso8601String(), 24 | 'players': playerMapper.toJsonArr(dto.players) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/infra/mappers/next_event_player_mapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 3 | import 'package:advanced_flutter/infra/types/json.dart'; 4 | 5 | final class NextEventPlayerMapper extends ListMapper { 6 | @override 7 | NextEventPlayer toDto(dynamic json) => NextEventPlayer( 8 | id: json['id'], 9 | name: json['name'], 10 | position: json['position'], 11 | photo: json['photo'], 12 | confirmationDate: DateTime.tryParse(json['confirmationDate'] ?? ''), 13 | isConfirmed: json['isConfirmed'] 14 | ); 15 | 16 | @override 17 | Json toJson(NextEventPlayer player) => { 18 | 'id': player.id, 19 | 'name': player.name, 20 | 'position': player.position, 21 | 'photo': player.photo, 22 | 'confirmationDate': player.confirmationDate?.toIso8601String(), 23 | 'isConfirmed': player.isConfirmed 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /lib/infra/respositories/load_next_event_from_api_with_cache_fallback_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/cache/clients/cache_save_client.dart'; 4 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 5 | 6 | typedef LoadNextEventRepository = Future Function({ required String groupId }); 7 | 8 | final class LoadNextEventFromApiWithCacheFallbackRepository { 9 | final LoadNextEventRepository loadNextEventFromApi; 10 | final LoadNextEventRepository loadNextEventFromCache; 11 | final CacheSaveClient cacheClient; 12 | final String key; 13 | final JsonMapper mapper; 14 | 15 | const LoadNextEventFromApiWithCacheFallbackRepository({ 16 | required this.loadNextEventFromApi, 17 | required this.loadNextEventFromCache, 18 | required this.cacheClient, 19 | required this.mapper, 20 | required this.key 21 | }); 22 | 23 | Future loadNextEvent({ required String groupId }) async { 24 | try { 25 | final event = await loadNextEventFromApi(groupId: groupId); 26 | await cacheClient.save(key: '$key:$groupId', value: mapper.toJson(event)); 27 | return event; 28 | } on SessionExpiredError { 29 | rethrow; 30 | } catch (error) { 31 | return loadNextEventFromCache(groupId: groupId); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/infra/types/json.dart: -------------------------------------------------------------------------------- 1 | typedef Json = Map; 2 | typedef JsonArr = List; 3 | -------------------------------------------------------------------------------- /lib/main/factories/infra/api/adapters/http_adapter_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/adapters/http_adapter.dart'; 2 | 3 | import 'package:http/http.dart'; 4 | 5 | HttpAdapter makeHttpAdapter() { 6 | return HttpAdapter(client: Client()); 7 | } 8 | -------------------------------------------------------------------------------- /lib/main/factories/infra/api/clients/api_url_factory.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | String makeApiUrl(String path) => 'http://${Platform.isIOS ? '127.0.0.1' : '10.0.2.2'}:8080/api/$path'; 4 | -------------------------------------------------------------------------------- /lib/main/factories/infra/api/clients/authorized_http_get_client_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/clients/authorized_http_get_client.dart'; 2 | import 'package:advanced_flutter/main/factories/infra/api/adapters/http_adapter_factory.dart'; 3 | import 'package:advanced_flutter/main/factories/infra/cache/adapters/cache_manager_adapter_factory.dart'; 4 | 5 | AuthorizedHttpGetClient makeAuthorizedHttpGetClient() { 6 | return AuthorizedHttpGetClient( 7 | cacheClient: makeCacheManagerAdapter(), 8 | httpClient: makeHttpAdapter() 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/main/factories/infra/api/repositories/load_next_event_api_repo_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/repositories/load_next_event_api_repo.dart'; 2 | import 'package:advanced_flutter/main/factories/infra/api/clients/api_url_factory.dart'; 3 | import 'package:advanced_flutter/main/factories/infra/api/clients/authorized_http_get_client_factory.dart'; 4 | import 'package:advanced_flutter/main/factories/infra/mappers/next_event_mapper_factory.dart'; 5 | 6 | LoadNextEventApiRepository makeLoadNextEventApiRepository() { 7 | return LoadNextEventApiRepository( 8 | httpClient: makeAuthorizedHttpGetClient(), 9 | url: makeApiUrl('groups/:groupId/next_event'), 10 | mapper: makeNextEventMapper() 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/main/factories/infra/cache/adapters/cache_manager_adapter_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/cache/adapters/cache_manager_adapter.dart'; 2 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 3 | 4 | CacheManagerAdapter makeCacheManagerAdapter() { 5 | return CacheManagerAdapter(client: DefaultCacheManager()); 6 | } 7 | -------------------------------------------------------------------------------- /lib/main/factories/infra/cache/repositories/load_next_event_cache_repo_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/cache/repositories/load_next_event_cache_repo.dart'; 2 | import 'package:advanced_flutter/main/factories/infra/cache/adapters/cache_manager_adapter_factory.dart'; 3 | import 'package:advanced_flutter/main/factories/infra/mappers/next_event_mapper_factory.dart'; 4 | 5 | LoadNextEventCacheRepository makeLoadNextEventCacheRepository() { 6 | return LoadNextEventCacheRepository( 7 | cacheClient: makeCacheManagerAdapter(), 8 | key: 'next_event', 9 | mapper: makeNextEventMapper() 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /lib/main/factories/infra/mappers/next_event_mapper_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/mappers/next_event_mapper.dart'; 2 | import 'package:advanced_flutter/main/factories/infra/mappers/next_event_player_mapper_factory.dart'; 3 | 4 | NextEventMapper makeNextEventMapper() => NextEventMapper(playerMapper: makeNextEventPLayerMapper()); 5 | -------------------------------------------------------------------------------- /lib/main/factories/infra/mappers/next_event_player_mapper_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/mappers/next_event_player_mapper.dart'; 2 | 3 | NextEventPlayerMapper makeNextEventPLayerMapper() => NextEventPlayerMapper(); 4 | -------------------------------------------------------------------------------- /lib/main/factories/infra/repositories/load_next_event_from_api_with_cache_fallback_repo_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/respositories/load_next_event_from_api_with_cache_fallback_repo.dart'; 2 | import 'package:advanced_flutter/main/factories/infra/api/repositories/load_next_event_api_repo_factory.dart'; 3 | import 'package:advanced_flutter/main/factories/infra/cache/adapters/cache_manager_adapter_factory.dart'; 4 | import 'package:advanced_flutter/main/factories/infra/cache/repositories/load_next_event_cache_repo_factory.dart'; 5 | import 'package:advanced_flutter/main/factories/infra/mappers/next_event_mapper_factory.dart'; 6 | 7 | LoadNextEventFromApiWithCacheFallbackRepository makeLoadNextEventFromApiWithCacheFallbackRepository() { 8 | return LoadNextEventFromApiWithCacheFallbackRepository( 9 | loadNextEventFromApi: makeLoadNextEventApiRepository().loadNextEvent, 10 | loadNextEventFromCache: makeLoadNextEventCacheRepository().loadNextEvent, 11 | cacheClient: makeCacheManagerAdapter(), 12 | key: 'next_event', 13 | mapper: makeNextEventMapper() 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/main/factories/ui/pages/next_event_page_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/main/factories/infra/repositories/load_next_event_from_api_with_cache_fallback_repo_factory.dart'; 2 | import 'package:advanced_flutter/presentation/rx/next_event_rx_presenter.dart'; 3 | import 'package:advanced_flutter/ui/pages/next_event_page.dart'; 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | Widget makeNextEventPage() { 8 | final repo = makeLoadNextEventFromApiWithCacheFallbackRepository(); 9 | final presenter = NextEventRxPresenter(nextEventLoader: repo.loadNextEvent); 10 | return NextEventPage(presenter: presenter, groupId: 'valid_id'); 11 | } 12 | -------------------------------------------------------------------------------- /lib/main/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/main/factories/infra/cache/adapters/cache_manager_adapter_factory.dart'; 2 | import 'package:advanced_flutter/main/factories/ui/pages/next_event_page_factory.dart'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() async { 7 | WidgetsFlutterBinding.ensureInitialized(); 8 | await makeCacheManagerAdapter().save(key: 'current_user', value: { 'accessToken': 'valid_token' }); 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | const MyApp({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final colorScheme = ColorScheme.fromSeed(seedColor: Colors.teal, brightness: Brightness.dark); 18 | return MaterialApp( 19 | title: 'Flutter Demo', 20 | debugShowCheckedModeBanner: false, 21 | darkTheme: ThemeData( 22 | dividerTheme: const DividerThemeData(space: 0), 23 | appBarTheme: AppBarTheme(color: colorScheme.primaryContainer), 24 | brightness: Brightness.dark, 25 | colorScheme: colorScheme, 26 | useMaterial3: true 27 | ), 28 | themeMode: ThemeMode.dark, 29 | home: makeNextEventPage() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/presentation/presenters/next_event_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/presentation/viewmodels/next_event_viewmodel.dart'; 2 | 3 | abstract class NextEventPresenter { 4 | Stream get nextEventStream; 5 | Stream get isBusyStream; 6 | 7 | Future loadNextEvent({ required String groupId, bool isReload }); 8 | } 9 | -------------------------------------------------------------------------------- /lib/presentation/rx/next_event_rx_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | import 'package:advanced_flutter/presentation/presenters/next_event_presenter.dart'; 3 | import 'package:advanced_flutter/presentation/viewmodels/next_event_viewmodel.dart'; 4 | 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | final class NextEventRxPresenter implements NextEventPresenter { 8 | final Future Function({ required String groupId }) nextEventLoader; 9 | final nextEventSubject = BehaviorSubject(); 10 | final isBusySubject = BehaviorSubject(); 11 | 12 | NextEventRxPresenter({ 13 | required this.nextEventLoader 14 | }); 15 | 16 | @override 17 | Stream get nextEventStream => nextEventSubject.stream; 18 | 19 | @override 20 | Stream get isBusyStream => isBusySubject.stream; 21 | 22 | @override 23 | Future loadNextEvent({ required String groupId, bool isReload = false }) async { 24 | try { 25 | if (isReload) isBusySubject.add(true); 26 | final event = await nextEventLoader(groupId: groupId); 27 | nextEventSubject.add(NextEventViewModel.fromEntity(event)); 28 | } catch (error) { 29 | nextEventSubject.addError(error); 30 | } finally { 31 | if (isReload) isBusySubject.add(false); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/presentation/viewmodels/next_event_player_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | 3 | import 'package:dartx/dartx.dart'; 4 | 5 | final class NextEventPlayerViewModel { 6 | final String name; 7 | final String initials; 8 | final String? photo; 9 | final String? position; 10 | final bool? isConfirmed; 11 | 12 | const NextEventPlayerViewModel({ 13 | required this.name, 14 | required this.initials, 15 | this.photo, 16 | this.position, 17 | this.isConfirmed 18 | }); 19 | 20 | factory NextEventPlayerViewModel.fromEntity(NextEventPlayer player) => NextEventPlayerViewModel( 21 | name: player.name, 22 | initials: player.initials, 23 | photo: player.photo, 24 | position: player.position, 25 | isConfirmed: player.confirmationDate == null ? null : player.isConfirmed 26 | ); 27 | 28 | static List mapDoubtPlayers(List players) => _mapPlayers(players.where((player) => player.confirmationDate == null).sortedBy(_sortByName)); 29 | static List mapGoalkeepers(List players) => _mapPlayers(_in(players).where((player) => player.position == 'goalkeeper').sortedBy(_sortByDate)); 30 | static List mapInPlayers(List players) => _mapPlayers(_in(players).where((player) => player.position != 'goalkeeper').sortedBy(_sortByDate)); 31 | static List mapOutPlayers(List players) => _mapPlayers(_confirmed(players).where((player) => !player.isConfirmed).sortedBy(_sortByDate)); 32 | 33 | static List _mapPlayers(List players) => players.map(NextEventPlayerViewModel.fromEntity).toList(); 34 | static Iterable _confirmed(List players) => players.where((player) => player.confirmationDate != null); 35 | static Iterable _in(List players) => _confirmed(players).where((player) => player.isConfirmed); 36 | static Comparable _sortByDate(NextEventPlayer player) => player.confirmationDate!; 37 | static Comparable _sortByName(NextEventPlayer player) => player.name; 38 | } 39 | -------------------------------------------------------------------------------- /lib/presentation/viewmodels/next_event_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | import 'package:advanced_flutter/presentation/viewmodels/next_event_player_viewmodel.dart'; 3 | 4 | final class NextEventViewModel { 5 | final List goalkeepers; 6 | final List players; 7 | final List out; 8 | final List doubt; 9 | 10 | const NextEventViewModel({ 11 | this.goalkeepers = const [], 12 | this.players = const [], 13 | this.out = const [], 14 | this.doubt = const [] 15 | }); 16 | 17 | factory NextEventViewModel.fromEntity(NextEvent event) => NextEventViewModel( 18 | doubt: NextEventPlayerViewModel.mapDoubtPlayers(event.players), 19 | out: NextEventPlayerViewModel.mapOutPlayers(event.players), 20 | goalkeepers: NextEventPlayerViewModel.mapGoalkeepers(event.players), 21 | players: NextEventPlayerViewModel.mapInPlayers(event.players) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/components/player_photo.dart: -------------------------------------------------------------------------------- 1 | import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | final class PlayerPhoto extends StatelessWidget { 5 | final String initials; 6 | final String? photo; 7 | 8 | const PlayerPhoto({ 9 | required this.initials, 10 | this.photo, 11 | super.key 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return CircleAvatar( 17 | radius: 25, 18 | foregroundImage: photo != null ? NetworkImage(photo!) : null, 19 | child: photo == null ? Text(initials, style: context.textStyles.labelLarge) : null 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/components/player_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | final class PlayerPosition extends StatelessWidget { 5 | final String? position; 6 | 7 | const PlayerPosition({ 8 | this.position, 9 | super.key 10 | }); 11 | 12 | String buildPositionLabel() => switch (position) { 13 | 'goalkeeper' => 'Goleiro', 14 | 'defender' => 'Zagueiro', 15 | 'midfielder' => 'Meia', 16 | 'forward' => 'Atacante', 17 | _ => 'Gandula' 18 | }; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Text(buildPositionLabel(), style: context.textStyles.labelMedium.apply(color: context.colors.scheme.primary.withValues(alpha: 0.7))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/components/player_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final class PlayerStatus extends StatelessWidget { 4 | final bool? isConfirmed; 5 | 6 | const PlayerStatus({ 7 | this.isConfirmed, 8 | super.key 9 | }); 10 | 11 | Color getColor() => switch (isConfirmed) { 12 | true => Colors.teal, 13 | false => Colors.pink, 14 | null => Colors.blueGrey 15 | }; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | width: 12, 21 | height: 12, 22 | decoration: BoxDecoration( 23 | shape: BoxShape.circle, 24 | color: getColor() 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/ui/pages/next_event_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/presentation/presenters/next_event_presenter.dart'; 2 | import 'package:advanced_flutter/presentation/viewmodels/next_event_player_viewmodel.dart'; 3 | import 'package:advanced_flutter/presentation/viewmodels/next_event_viewmodel.dart'; 4 | import 'package:advanced_flutter/ui/components/player_photo.dart'; 5 | import 'package:advanced_flutter/ui/components/player_position.dart'; 6 | import 'package:advanced_flutter/ui/components/player_status.dart'; 7 | 8 | import 'package:awesome_flutter_extensions/awesome_flutter_extensions.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | final class NextEventPage extends StatefulWidget { 12 | final NextEventPresenter presenter; 13 | final String groupId; 14 | 15 | const NextEventPage({ 16 | required this.presenter, 17 | required this.groupId, 18 | super.key 19 | }); 20 | 21 | @override 22 | State createState() => _NextEventPageState(); 23 | } 24 | 25 | class _NextEventPageState extends State { 26 | @override 27 | void initState() { 28 | widget.presenter.loadNextEvent(groupId: widget.groupId); 29 | widget.presenter.isBusyStream.listen((isBusy) => isBusy ? showLoading() : hideLoading()); 30 | super.initState(); 31 | } 32 | 33 | void showLoading() => showDialog( 34 | context: context, 35 | builder: (context) => SimpleDialog( 36 | children: [ 37 | Column( 38 | children: [ 39 | Text('Aguarde...', style: context.textStyles.labelLarge), 40 | const SizedBox(height: 16), 41 | const CircularProgressIndicator() 42 | ] 43 | ) 44 | ] 45 | ) 46 | ); 47 | 48 | void hideLoading() => Navigator.of(context).maybePop(); 49 | 50 | Widget buildErrorLayout() => Center( 51 | child: Column( 52 | mainAxisAlignment: MainAxisAlignment.center, 53 | children: [ 54 | Text('Algo errado aconteceu, tente novamente.', style: context.textStyles.bodyLarge), 55 | const SizedBox(height: 16), 56 | ElevatedButton( 57 | onPressed: () => widget.presenter.loadNextEvent(groupId: widget.groupId, isReload: true), 58 | child: Text('RECARREGAR', style: context.textStyles.labelLarge) 59 | ) 60 | ] 61 | ), 62 | ); 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return Scaffold( 67 | appBar: AppBar(title: const Text('Próximo Jogo')), 68 | body: StreamBuilder( 69 | stream: widget.presenter.nextEventStream, 70 | builder: (context, snapshot) { 71 | if (snapshot.connectionState != ConnectionState.active) return const Center(child: CircularProgressIndicator()); 72 | if (snapshot.hasError) return buildErrorLayout(); 73 | final viewModel = snapshot.data!; 74 | return RefreshIndicator( 75 | onRefresh: () async => widget.presenter.loadNextEvent(groupId: widget.groupId, isReload: true), 76 | child: ListView( 77 | children: [ 78 | if (viewModel.goalkeepers.isNotEmpty) ListSection(title: 'DENTRO - GOLEIROS', items: viewModel.goalkeepers), 79 | if (viewModel.players.isNotEmpty) ListSection(title: 'DENTRO - JOGADORES', items: viewModel.players), 80 | if (viewModel.out.isNotEmpty) ListSection(title: 'FORA', items: viewModel.out), 81 | if (viewModel.doubt.isNotEmpty) ListSection(title: 'DÚVIDA', items: viewModel.doubt) 82 | ] 83 | ) 84 | ); 85 | } 86 | ) 87 | ); 88 | } 89 | } 90 | 91 | final class ListSection extends StatelessWidget { 92 | final String title; 93 | final List items; 94 | 95 | const ListSection({ 96 | required this.title, 97 | required this.items, 98 | super.key 99 | }); 100 | 101 | @override 102 | Widget build(BuildContext context) { 103 | return Column( 104 | children: [ 105 | Padding( 106 | padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 32), 107 | child: Row( 108 | children: [ 109 | Expanded(child: Text(title, style: context.textStyles.titleSmall)), 110 | Text(items.length.toString(), style: context.textStyles.titleSmall) 111 | ] 112 | ) 113 | ), 114 | const Divider(), 115 | ...items.map((player) => Container( 116 | color: context.colors.scheme.onSurface.withValues(alpha: 0.03), 117 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 118 | child: Row( 119 | children: [ 120 | PlayerPhoto(initials: player.initials, photo: player.photo), 121 | const SizedBox(width: 16), 122 | Expanded( 123 | child: Column( 124 | crossAxisAlignment: CrossAxisAlignment.start, 125 | children: [ 126 | Text(player.name, style: context.textStyles.labelLarge), 127 | PlayerPosition(position: player.position) 128 | ] 129 | ) 130 | ), 131 | PlayerStatus(isConfirmed: player.isConfirmed) 132 | ] 133 | ), 134 | )).separatedBy(const Divider(indent: 82)), 135 | const Divider() 136 | ] 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.5.0" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | awesome_flutter_extensions: 37 | dependency: "direct main" 38 | description: 39 | name: awesome_flutter_extensions 40 | sha256: ce853175d082072bbb52350cd964b04a0a352b0ef364cc6ef2f5a999e9010a66 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.3.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.1" 52 | build: 53 | dependency: transitive 54 | description: 55 | name: build 56 | sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.4.1" 60 | built_collection: 61 | dependency: transitive 62 | description: 63 | name: built_collection 64 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "5.1.1" 68 | built_value: 69 | dependency: transitive 70 | description: 71 | name: built_value 72 | sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "8.9.2" 76 | characters: 77 | dependency: transitive 78 | description: 79 | name: characters 80 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.0" 84 | clock: 85 | dependency: transitive 86 | description: 87 | name: clock 88 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.1.1" 92 | code_builder: 93 | dependency: transitive 94 | description: 95 | name: code_builder 96 | sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "4.10.0" 100 | collection: 101 | dependency: transitive 102 | description: 103 | name: collection 104 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.19.0" 108 | convert: 109 | dependency: transitive 110 | description: 111 | name: convert 112 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.1.1" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.0.3" 124 | cupertino_icons: 125 | dependency: "direct main" 126 | description: 127 | name: cupertino_icons 128 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.0.8" 132 | dart_style: 133 | dependency: transitive 134 | description: 135 | name: dart_style 136 | sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.3.6" 140 | dartx: 141 | dependency: "direct main" 142 | description: 143 | name: dartx 144 | sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.2.0" 148 | fake_async: 149 | dependency: transitive 150 | description: 151 | name: fake_async 152 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.3.1" 156 | ffi: 157 | dependency: transitive 158 | description: 159 | name: ffi 160 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "2.1.2" 164 | file: 165 | dependency: "direct dev" 166 | description: 167 | name: file 168 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "7.0.1" 172 | fixnum: 173 | dependency: transitive 174 | description: 175 | name: fixnum 176 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "1.1.0" 180 | flutter: 181 | dependency: "direct main" 182 | description: flutter 183 | source: sdk 184 | version: "0.0.0" 185 | flutter_cache_manager: 186 | dependency: "direct main" 187 | description: 188 | name: flutter_cache_manager 189 | sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" 190 | url: "https://pub.dev" 191 | source: hosted 192 | version: "3.4.1" 193 | flutter_lints: 194 | dependency: "direct dev" 195 | description: 196 | name: flutter_lints 197 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 198 | url: "https://pub.dev" 199 | source: hosted 200 | version: "5.0.0" 201 | flutter_test: 202 | dependency: "direct dev" 203 | description: flutter 204 | source: sdk 205 | version: "0.0.0" 206 | glob: 207 | dependency: transitive 208 | description: 209 | name: glob 210 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 211 | url: "https://pub.dev" 212 | source: hosted 213 | version: "2.1.2" 214 | http: 215 | dependency: "direct main" 216 | description: 217 | name: http 218 | sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f 219 | url: "https://pub.dev" 220 | source: hosted 221 | version: "1.3.0" 222 | http_parser: 223 | dependency: transitive 224 | description: 225 | name: http_parser 226 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 227 | url: "https://pub.dev" 228 | source: hosted 229 | version: "4.0.2" 230 | leak_tracker: 231 | dependency: transitive 232 | description: 233 | name: leak_tracker 234 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 235 | url: "https://pub.dev" 236 | source: hosted 237 | version: "10.0.7" 238 | leak_tracker_flutter_testing: 239 | dependency: transitive 240 | description: 241 | name: leak_tracker_flutter_testing 242 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 243 | url: "https://pub.dev" 244 | source: hosted 245 | version: "3.0.8" 246 | leak_tracker_testing: 247 | dependency: transitive 248 | description: 249 | name: leak_tracker_testing 250 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 251 | url: "https://pub.dev" 252 | source: hosted 253 | version: "3.0.1" 254 | lints: 255 | dependency: transitive 256 | description: 257 | name: lints 258 | sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 259 | url: "https://pub.dev" 260 | source: hosted 261 | version: "5.1.1" 262 | logging: 263 | dependency: transitive 264 | description: 265 | name: logging 266 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 267 | url: "https://pub.dev" 268 | source: hosted 269 | version: "1.2.0" 270 | matcher: 271 | dependency: transitive 272 | description: 273 | name: matcher 274 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 275 | url: "https://pub.dev" 276 | source: hosted 277 | version: "0.12.16+1" 278 | material_color_utilities: 279 | dependency: transitive 280 | description: 281 | name: material_color_utilities 282 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 283 | url: "https://pub.dev" 284 | source: hosted 285 | version: "0.11.1" 286 | meta: 287 | dependency: transitive 288 | description: 289 | name: meta 290 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "1.15.0" 294 | mockito: 295 | dependency: transitive 296 | description: 297 | name: mockito 298 | sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "5.4.4" 302 | network_image_mock: 303 | dependency: "direct dev" 304 | description: 305 | name: network_image_mock 306 | sha256: "855cdd01d42440e0cffee0d6c2370909fc31b3bcba308a59829f24f64be42db7" 307 | url: "https://pub.dev" 308 | source: hosted 309 | version: "2.1.1" 310 | package_config: 311 | dependency: transitive 312 | description: 313 | name: package_config 314 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 315 | url: "https://pub.dev" 316 | source: hosted 317 | version: "2.1.0" 318 | path: 319 | dependency: transitive 320 | description: 321 | name: path 322 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 323 | url: "https://pub.dev" 324 | source: hosted 325 | version: "1.9.0" 326 | path_provider: 327 | dependency: transitive 328 | description: 329 | name: path_provider 330 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 331 | url: "https://pub.dev" 332 | source: hosted 333 | version: "2.1.5" 334 | path_provider_android: 335 | dependency: transitive 336 | description: 337 | name: path_provider_android 338 | sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "2.2.15" 342 | path_provider_foundation: 343 | dependency: transitive 344 | description: 345 | name: path_provider_foundation 346 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "2.4.0" 350 | path_provider_linux: 351 | dependency: transitive 352 | description: 353 | name: path_provider_linux 354 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 355 | url: "https://pub.dev" 356 | source: hosted 357 | version: "2.2.1" 358 | path_provider_platform_interface: 359 | dependency: transitive 360 | description: 361 | name: path_provider_platform_interface 362 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 363 | url: "https://pub.dev" 364 | source: hosted 365 | version: "2.1.2" 366 | path_provider_windows: 367 | dependency: transitive 368 | description: 369 | name: path_provider_windows 370 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 371 | url: "https://pub.dev" 372 | source: hosted 373 | version: "2.3.0" 374 | platform: 375 | dependency: transitive 376 | description: 377 | name: platform 378 | sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" 379 | url: "https://pub.dev" 380 | source: hosted 381 | version: "3.1.5" 382 | plugin_platform_interface: 383 | dependency: transitive 384 | description: 385 | name: plugin_platform_interface 386 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 387 | url: "https://pub.dev" 388 | source: hosted 389 | version: "2.1.8" 390 | pub_semver: 391 | dependency: transitive 392 | description: 393 | name: pub_semver 394 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 395 | url: "https://pub.dev" 396 | source: hosted 397 | version: "2.1.4" 398 | rxdart: 399 | dependency: "direct main" 400 | description: 401 | name: rxdart 402 | sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" 403 | url: "https://pub.dev" 404 | source: hosted 405 | version: "0.28.0" 406 | sky_engine: 407 | dependency: transitive 408 | description: flutter 409 | source: sdk 410 | version: "0.0.0" 411 | source_gen: 412 | dependency: transitive 413 | description: 414 | name: source_gen 415 | sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "1.5.0" 419 | source_span: 420 | dependency: transitive 421 | description: 422 | name: source_span 423 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "1.10.0" 427 | sprintf: 428 | dependency: transitive 429 | description: 430 | name: sprintf 431 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 432 | url: "https://pub.dev" 433 | source: hosted 434 | version: "7.0.0" 435 | sqflite: 436 | dependency: transitive 437 | description: 438 | name: sqflite 439 | sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d 440 | url: "https://pub.dev" 441 | source: hosted 442 | version: "2.3.3+1" 443 | sqflite_common: 444 | dependency: transitive 445 | description: 446 | name: sqflite_common 447 | sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" 448 | url: "https://pub.dev" 449 | source: hosted 450 | version: "2.5.4" 451 | stack_trace: 452 | dependency: transitive 453 | description: 454 | name: stack_trace 455 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 456 | url: "https://pub.dev" 457 | source: hosted 458 | version: "1.12.0" 459 | stream_channel: 460 | dependency: transitive 461 | description: 462 | name: stream_channel 463 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 464 | url: "https://pub.dev" 465 | source: hosted 466 | version: "2.1.2" 467 | string_scanner: 468 | dependency: transitive 469 | description: 470 | name: string_scanner 471 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 472 | url: "https://pub.dev" 473 | source: hosted 474 | version: "1.3.0" 475 | synchronized: 476 | dependency: transitive 477 | description: 478 | name: synchronized 479 | sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" 480 | url: "https://pub.dev" 481 | source: hosted 482 | version: "3.1.0+1" 483 | term_glyph: 484 | dependency: transitive 485 | description: 486 | name: term_glyph 487 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 488 | url: "https://pub.dev" 489 | source: hosted 490 | version: "1.2.1" 491 | test_api: 492 | dependency: transitive 493 | description: 494 | name: test_api 495 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 496 | url: "https://pub.dev" 497 | source: hosted 498 | version: "0.7.3" 499 | time: 500 | dependency: transitive 501 | description: 502 | name: time 503 | sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 504 | url: "https://pub.dev" 505 | source: hosted 506 | version: "2.1.4" 507 | typed_data: 508 | dependency: transitive 509 | description: 510 | name: typed_data 511 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 512 | url: "https://pub.dev" 513 | source: hosted 514 | version: "1.3.2" 515 | uuid: 516 | dependency: transitive 517 | description: 518 | name: uuid 519 | sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" 520 | url: "https://pub.dev" 521 | source: hosted 522 | version: "4.4.2" 523 | vector_math: 524 | dependency: transitive 525 | description: 526 | name: vector_math 527 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 528 | url: "https://pub.dev" 529 | source: hosted 530 | version: "2.1.4" 531 | vm_service: 532 | dependency: transitive 533 | description: 534 | name: vm_service 535 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 536 | url: "https://pub.dev" 537 | source: hosted 538 | version: "14.3.0" 539 | watcher: 540 | dependency: transitive 541 | description: 542 | name: watcher 543 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 544 | url: "https://pub.dev" 545 | source: hosted 546 | version: "1.1.0" 547 | web: 548 | dependency: transitive 549 | description: 550 | name: web 551 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" 552 | url: "https://pub.dev" 553 | source: hosted 554 | version: "0.5.1" 555 | xdg_directories: 556 | dependency: transitive 557 | description: 558 | name: xdg_directories 559 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 560 | url: "https://pub.dev" 561 | source: hosted 562 | version: "1.0.4" 563 | yaml: 564 | dependency: transitive 565 | description: 566 | name: yaml 567 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 568 | url: "https://pub.dev" 569 | source: hosted 570 | version: "3.1.2" 571 | sdks: 572 | dart: ">=3.6.0 <4.0.0" 573 | flutter: ">=3.27.0" 574 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: advanced_flutter 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: '>=3.0.5 <4.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | awesome_flutter_extensions: 1.3.0 12 | cupertino_icons: 1.0.8 13 | dartx: 1.2.0 14 | flutter_cache_manager: 3.4.1 15 | http: 1.3.0 16 | rxdart: 0.28.0 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | file: 7.0.1 22 | flutter_lints: 5.0.0 23 | network_image_mock: 2.1.1 24 | 25 | flutter: 26 | uses-material-design: true 27 | -------------------------------------------------------------------------------- /test/domain/entities/next_event_player_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | String initialsOf(String name) => NextEventPlayer(id: '', name: name, isConfirmed: true).initials; 7 | 8 | test('should return the first letter of the first and last names', () { 9 | expect(initialsOf('Rodrigo Manguinho'), 'RM'); 10 | expect(initialsOf('Pedro Carvalho'), 'PC'); 11 | expect(initialsOf('Ingrid Mota da Silva'), 'IS'); 12 | }); 13 | 14 | test('should return the first letters of the first name', () { 15 | expect(initialsOf('Rodrigo'), 'RO'); 16 | expect(initialsOf('R'), 'R'); 17 | }); 18 | 19 | test('should return "-" when name is empty', () { 20 | expect(initialsOf(''), '-'); 21 | }); 22 | 23 | test('should convert to uppercase', () { 24 | expect(initialsOf('rodrigo manguinho'), 'RM'); 25 | expect(initialsOf('rodrigo'), 'RO'); 26 | expect(initialsOf('r'), 'R'); 27 | }); 28 | 29 | test('should ignore extra whitespaces', () { 30 | expect(initialsOf('Rodrigo Manguinho '), 'RM'); 31 | expect(initialsOf(' Rodrigo Manguinho'), 'RM'); 32 | expect(initialsOf('Rodrigo Manguinho'), 'RM'); 33 | expect(initialsOf(' Rodrigo Manguinho '), 'RM'); 34 | expect(initialsOf(' Rodrigo '), 'RO'); 35 | expect(initialsOf(' R '), 'R'); 36 | expect(initialsOf(' '), '-'); 37 | expect(initialsOf(' '), '-'); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/domain/mocks/next_event_loader_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 3 | 4 | import '../../mocks/fakes.dart'; 5 | 6 | final class NextEventLoaderSpy { 7 | int callsCount = 0; 8 | String? groupId; 9 | Error? error; 10 | NextEvent output = NextEvent(groupName: anyString(), date: anyDate(), players: []); 11 | 12 | void simulatePlayers(List players) => output = NextEvent(groupName: anyString(), date: anyDate(), players: players); 13 | 14 | Future call({ required String groupId }) async { 15 | this.groupId = groupId; 16 | callsCount++; 17 | if (error != null) throw error!; 18 | return output; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/next_event_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/adapters/http_adapter.dart'; 2 | import 'package:advanced_flutter/infra/api/repositories/load_next_event_api_repo.dart'; 3 | import 'package:advanced_flutter/infra/cache/adapters/cache_manager_adapter.dart'; 4 | import 'package:advanced_flutter/infra/cache/repositories/load_next_event_cache_repo.dart'; 5 | import 'package:advanced_flutter/infra/mappers/next_event_mapper.dart'; 6 | import 'package:advanced_flutter/infra/respositories/load_next_event_from_api_with_cache_fallback_repo.dart'; 7 | import 'package:advanced_flutter/main/factories/infra/mappers/next_event_mapper_factory.dart'; 8 | import 'package:advanced_flutter/presentation/rx/next_event_rx_presenter.dart'; 9 | import 'package:advanced_flutter/ui/pages/next_event_page.dart'; 10 | 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | 14 | import '../infra/api/mocks/client_spy.dart'; 15 | import '../infra/cache/mocks/cache_manager_spy.dart'; 16 | import '../mocks/fakes.dart'; 17 | 18 | void main() { 19 | late String responseJson; 20 | late String key; 21 | late NextEventMapper mapper; 22 | late ClientSpy client; 23 | late CacheManagerSpy cacheManager; 24 | late HttpAdapter httpClient; 25 | late CacheManagerAdapter cacheClient; 26 | late LoadNextEventApiRepository apiRepo; 27 | late LoadNextEventCacheRepository cacheRepo; 28 | late LoadNextEventFromApiWithCacheFallbackRepository repo; 29 | late NextEventRxPresenter presenter; 30 | late MaterialApp sut; 31 | 32 | setUpAll(() { 33 | responseJson = ''' 34 | { 35 | "id": "1", 36 | "groupName": "Pelada Chega+", 37 | "date": "2024-01-11T11:10:00.000Z", 38 | "players": [{ 39 | "id": "1", 40 | "name": "Cristiano Ronaldo", 41 | "position": "forward", 42 | "isConfirmed": true, 43 | "confirmationDate": "2024-01-10T11:07:00.000Z" 44 | }, { 45 | "id": "2", 46 | "name": "Lionel Messi", 47 | "position": "midfielder", 48 | "isConfirmed": true, 49 | "confirmationDate": "2024-01-10T11:08:00.000Z" 50 | }, { 51 | "id": "3", 52 | "name": "Dida", 53 | "position": "goalkeeper", 54 | "isConfirmed": true, 55 | "confirmationDate": "2024-01-10T09:10:00.000Z" 56 | }, { 57 | "id": "4", 58 | "name": "Romario", 59 | "position": "forward", 60 | "isConfirmed": true, 61 | "confirmationDate": "2024-01-10T11:10:00.000Z" 62 | }, { 63 | "id": "5", 64 | "name": "Claudio Gamarra", 65 | "position": "defender", 66 | "isConfirmed": false, 67 | "confirmationDate": "2024-01-10T13:10:00.000Z" 68 | }, { 69 | "id": "6", 70 | "name": "Diego Forlan", 71 | "position": "defender", 72 | "isConfirmed": false, 73 | "confirmationDate": "2024-01-10T14:10:00.000Z" 74 | }, { 75 | "id": "7", 76 | "name": "Zé Ninguém", 77 | "isConfirmed": false 78 | }, { 79 | "id": "8", 80 | "name": "Rodrigo Manguinho", 81 | "isConfirmed": false 82 | }, { 83 | "id": "9", 84 | "name": "Claudio Taffarel", 85 | "position": "goalkeeper", 86 | "isConfirmed": true, 87 | "confirmationDate": "2024-01-10T09:15:00.000Z" 88 | }] 89 | } 90 | '''; 91 | }); 92 | 93 | setUp(() { 94 | key = anyString(); 95 | mapper = makeNextEventMapper(); 96 | client = ClientSpy(); 97 | httpClient = HttpAdapter(client: client); 98 | apiRepo = LoadNextEventApiRepository( 99 | httpClient: httpClient, 100 | url: anyString(), 101 | mapper: mapper 102 | ); 103 | cacheManager = CacheManagerSpy(); 104 | cacheClient = CacheManagerAdapter(client: cacheManager); 105 | cacheRepo = LoadNextEventCacheRepository( 106 | cacheClient: cacheClient, 107 | key: key, 108 | mapper: mapper 109 | ); 110 | repo = LoadNextEventFromApiWithCacheFallbackRepository( 111 | loadNextEventFromApi: apiRepo.loadNextEvent, 112 | loadNextEventFromCache: cacheRepo.loadNextEvent, 113 | cacheClient: cacheClient, 114 | key: key, 115 | mapper: mapper 116 | ); 117 | presenter = NextEventRxPresenter(nextEventLoader: repo.loadNextEvent); 118 | sut = MaterialApp(home: NextEventPage(presenter: presenter, groupId: anyString())); 119 | }); 120 | 121 | testWidgets('should present api data', (tester) async { 122 | client.responseJson = responseJson; 123 | await tester.pumpWidget(sut); 124 | await tester.pump(); 125 | await tester.ensureVisible(find.text('Cristiano Ronaldo', skipOffstage: false)); 126 | await tester.pump(); 127 | expect(find.text('Cristiano Ronaldo'), findsOneWidget); 128 | await tester.ensureVisible(find.text('Lionel Messi', skipOffstage: false)); 129 | await tester.pump(); 130 | expect(find.text('Lionel Messi'), findsOneWidget); 131 | await tester.ensureVisible(find.text('Claudio Gamarra', skipOffstage: false)); 132 | await tester.pump(); 133 | expect(find.text('Claudio Gamarra'), findsOneWidget); 134 | }); 135 | 136 | testWidgets('should present cache data', (tester) async { 137 | client.simulateServerError(); 138 | cacheManager.file.simulateResponse(responseJson); 139 | await tester.pumpWidget(sut); 140 | await tester.pump(); 141 | await tester.ensureVisible(find.text('Cristiano Ronaldo', skipOffstage: false)); 142 | await tester.pump(); 143 | expect(find.text('Cristiano Ronaldo'), findsOneWidget); 144 | await tester.ensureVisible(find.text('Lionel Messi', skipOffstage: false)); 145 | await tester.pump(); 146 | expect(find.text('Lionel Messi'), findsOneWidget); 147 | await tester.ensureVisible(find.text('Claudio Gamarra', skipOffstage: false)); 148 | await tester.pump(); 149 | expect(find.text('Claudio Gamarra'), findsOneWidget); 150 | }); 151 | 152 | testWidgets('should present error message', (tester) async { 153 | client.simulateServerError(); 154 | cacheManager.file.simulateInvalidResponse(); 155 | await tester.pumpWidget(sut); 156 | await tester.pump(); 157 | await tester.ensureVisible(find.text('Algo errado aconteceu, tente novamente.', skipOffstage: false)); 158 | await tester.pump(); 159 | expect(find.text('Algo errado aconteceu, tente novamente.'), findsOneWidget); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /test/infra/api/adapters/http_adapter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/infra/api/adapters/http_adapter.dart'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../../mocks/fakes.dart'; 7 | import '../mocks/client_spy.dart'; 8 | 9 | void main() { 10 | late ClientSpy client; 11 | late HttpAdapter sut; 12 | late String url; 13 | 14 | setUp(() { 15 | client = ClientSpy(); 16 | client.responseJson = ''' 17 | { 18 | "key1": "value1", 19 | "key2": "value2" 20 | } 21 | '''; 22 | url = anyString(); 23 | sut = HttpAdapter(client: client); 24 | }); 25 | 26 | group('get', () { 27 | test('should request with correct method', () async { 28 | await sut.get(url: url); 29 | expect(client.method, 'get'); 30 | expect(client.callsCount, 1); 31 | }); 32 | 33 | test('should request with correct url', () async { 34 | await sut.get(url: url); 35 | expect(client.url, url); 36 | }); 37 | 38 | test('should request with default headers', () async { 39 | await sut.get(url: url); 40 | expect(client.headers?['content-type'], 'application/json'); 41 | expect(client.headers?['accept'], 'application/json'); 42 | }); 43 | 44 | test('should append headers', () async { 45 | await sut.get(url: url, headers: { 'h1': 'value1', 'h2': 'value2', 'h3': 123 }); 46 | expect(client.headers?['content-type'], 'application/json'); 47 | expect(client.headers?['accept'], 'application/json'); 48 | expect(client.headers?['h1'], 'value1'); 49 | expect(client.headers?['h2'], 'value2'); 50 | expect(client.headers?['h3'], '123'); 51 | }); 52 | 53 | test('should request with correct params', () async { 54 | url = 'http://anyurl.com/:p1/:p2/:p3'; 55 | await sut.get(url: url, params: { 'p1': 'v1', 'p2': 'v2', 'p3': 123 }); 56 | expect(client.url, 'http://anyurl.com/v1/v2/123'); 57 | }); 58 | 59 | test('should request with optional param', () async { 60 | url = 'http://anyurl.com/:p1/:p2'; 61 | await sut.get(url: url, params: { 'p1': 'v1', 'p2': null }); 62 | expect(client.url, 'http://anyurl.com/v1'); 63 | }); 64 | 65 | test('should request with invalid params', () async { 66 | url = 'http://anyurl.com/:p1/:p2'; 67 | await sut.get(url: url, params: { 'p3': 'v3' }); 68 | expect(client.url, 'http://anyurl.com/:p1/:p2'); 69 | }); 70 | 71 | test('should request with correct queryStrings', () async { 72 | await sut.get(url: url, queryString: { 'q1': 'v1', 'q2': 'v2', 'q3': 123 }); 73 | expect(client.url, '$url?q1=v1&q2=v2&q3=123'); 74 | }); 75 | 76 | test('should request with correct queryStrings and params', () async { 77 | url = 'http://anyurl.com/:p3/:p4'; 78 | await sut.get(url: url, queryString: { 'q1': 'v1', 'q2': 'v2' }, params: { 'p3': 'v3', 'p4': 'v4' }); 79 | expect(client.url, 'http://anyurl.com/v3/v4?q1=v1&q2=v2'); 80 | }); 81 | 82 | test('should throw UnexpectedError on 400', () async { 83 | client.simulateBadRequestError(); 84 | final future = sut.get(url: url); 85 | expect(future, throwsA(isA())); 86 | }); 87 | 88 | test('should throw SessionExpiredError on 401', () async { 89 | client.simulateUnauthorizedError(); 90 | final future = sut.get(url: url); 91 | expect(future, throwsA(isA())); 92 | }); 93 | 94 | test('should throw UnexpectedError on 403', () async { 95 | client.simulateForbiddenError(); 96 | final future = sut.get(url: url); 97 | expect(future, throwsA(isA())); 98 | }); 99 | 100 | test('should throw UnexpectedError on 404', () async { 101 | client.simulateNotFoundError(); 102 | final future = sut.get(url: url); 103 | expect(future, throwsA(isA())); 104 | }); 105 | 106 | test('should throw UnexpectedError on 500', () async { 107 | client.simulateServerError(); 108 | final future = sut.get(url: url); 109 | expect(future, throwsA(isA())); 110 | }); 111 | 112 | test('should return a Map', () async { 113 | final data = await sut.get(url: url); 114 | expect(data?['key1'], 'value1'); 115 | expect(data?['key2'], 'value2'); 116 | }); 117 | 118 | test('should return a List', () async { 119 | client.responseJson = ''' 120 | [{ 121 | "key": "value1" 122 | }, { 123 | "key": "value2" 124 | }] 125 | '''; 126 | final data = await sut.get(url: url); 127 | expect(data?[0]['key'], 'value1'); 128 | expect(data?[1]['key'], 'value2'); 129 | }); 130 | 131 | test('should return a Map with List', () async { 132 | client.responseJson = ''' 133 | { 134 | "key1": "value1", 135 | "key2": [{ 136 | "key": "value1" 137 | }, { 138 | "key": "value2" 139 | }] 140 | } 141 | '''; 142 | final data = await sut.get(url: url); 143 | expect(data?['key1'], 'value1'); 144 | expect(data?['key2'][0]['key'], 'value1'); 145 | expect(data?['key2'][1]['key'], 'value2'); 146 | }); 147 | 148 | test('should return null on 200 with empty response', () async { 149 | client.responseJson = ''; 150 | final data = await sut.get(url: url); 151 | expect(data, isNull); 152 | }); 153 | 154 | test('should return null on 204', () async { 155 | client.simulateNoContent(); 156 | final data = await sut.get(url: url); 157 | expect(data, isNull); 158 | }); 159 | }); 160 | } 161 | -------------------------------------------------------------------------------- /test/infra/api/clients/authorized_http_get_client_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/clients/authorized_http_get_client.dart'; 2 | import 'package:advanced_flutter/infra/types/json.dart'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../../mocks/fakes.dart'; 7 | import '../../cache/mocks/cache_get_client_spy.dart'; 8 | import '../mocks/http_get_client_spy.dart'; 9 | 10 | void main() { 11 | late CacheGetClientSpy cacheClient; 12 | late HttpGetClientSpy httpClient; 13 | late AuthorizedHttpGetClient sut; 14 | late String url; 15 | late Json params; 16 | late Json queryString; 17 | late Json headers; 18 | late String token; 19 | 20 | setUp(() { 21 | url = anyString(); 22 | token = anyString(); 23 | params = anyJson(); 24 | queryString = anyJson(); 25 | headers = anyJson(); 26 | cacheClient = CacheGetClientSpy(); 27 | httpClient = HttpGetClientSpy(); 28 | sut = AuthorizedHttpGetClient(cacheClient: cacheClient, httpClient: httpClient); 29 | }); 30 | 31 | test('should call CacheClient with correct input', () async { 32 | await sut.get(url: url); 33 | expect(cacheClient.callsCount, 1); 34 | expect(cacheClient.key, 'current_user'); 35 | }); 36 | 37 | test('should call HttpClient with correct input', () async { 38 | await sut.get(url: url, params: params, queryString: queryString); 39 | expect(httpClient.callsCount, 1); 40 | expect(httpClient.url, url); 41 | expect(httpClient.params, params); 42 | expect(httpClient.queryString, queryString); 43 | }); 44 | 45 | test('should call HttpClient with null headers', () async { 46 | cacheClient.response = null; 47 | await sut.get(url: url, headers: null); 48 | expect(httpClient.headers, isNull); 49 | }); 50 | 51 | test('should call HttpClient with current headers', () async { 52 | cacheClient.response = null; 53 | await sut.get(url: url, headers: headers); 54 | expect(httpClient.headers, headers); 55 | }); 56 | 57 | test('should call HttpClient with authorization headers', () async { 58 | cacheClient.response = { 'accessToken': token }; 59 | await sut.get(url: url, headers: null); 60 | expect(httpClient.headers, { 'authorization': token }); 61 | }); 62 | 63 | test('should call HttpClient with merged headers', () async { 64 | cacheClient.response = { 'accessToken': token }; 65 | await sut.get(url: url, headers: { 'q1': 'v1', 'q2': 'v2' }); 66 | expect(httpClient.headers, { 'authorization': token, 'q1': 'v1', 'q2': 'v2' }); 67 | }); 68 | 69 | test('should call HttpClient with invalid cache', () async { 70 | cacheClient.response = { 'invalid': 'invalid' }; 71 | await sut.get(url: url, headers: { 'q1': 'v1', 'q2': 'v2' }); 72 | expect(httpClient.headers, { 'q1': 'v1', 'q2': 'v2' }); 73 | }); 74 | 75 | test('should call HttpClient with invalid cache and null headers', () async { 76 | cacheClient.response = { 'invalid': 'invalid' }; 77 | await sut.get(url: url, headers: null); 78 | expect(httpClient.headers, isNull); 79 | }); 80 | 81 | test('should rethrow on CacheClient error', () async { 82 | final error = Error(); 83 | cacheClient.error = error; 84 | final future = sut.get(url: url); 85 | expect(future, throwsA(error)); 86 | }); 87 | 88 | test('should rethrow on HttpClient error', () async { 89 | final error = Error(); 90 | httpClient.error = error; 91 | final future = sut.get(url: url); 92 | expect(future, throwsA(error)); 93 | }); 94 | 95 | test('should return same result as HttpClient', () async { 96 | final response = await sut.get(url: url); 97 | expect(response, httpClient.response); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /test/infra/api/mocks/client_spy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:http/http.dart'; 5 | 6 | final class ClientSpy implements Client { 7 | String? method; 8 | String? url; 9 | int callsCount = 0; 10 | Map? headers; 11 | String responseJson = ''; 12 | int statusCode = 200; 13 | 14 | void simulateNoContent() => statusCode = 204; 15 | void simulateBadRequestError() => statusCode = 400; 16 | void simulateUnauthorizedError() => statusCode = 401; 17 | void simulateForbiddenError() => statusCode = 403; 18 | void simulateNotFoundError() => statusCode = 404; 19 | void simulateServerError() => statusCode = 500; 20 | 21 | @override 22 | Future get(Uri url, {Map? headers}) async { 23 | method = 'get'; 24 | callsCount++; 25 | this.url = url.toString(); 26 | this.headers = headers; 27 | return Response(responseJson, statusCode); 28 | } 29 | 30 | @override 31 | void close() {} 32 | 33 | @override 34 | Future delete(Uri url, {Map? headers, Object? body, Encoding? encoding}) { 35 | throw UnimplementedError(); 36 | } 37 | 38 | @override 39 | Future head(Uri url, {Map? headers}) { 40 | throw UnimplementedError(); 41 | } 42 | 43 | @override 44 | Future patch(Uri url, {Map? headers, Object? body, Encoding? encoding}) { 45 | throw UnimplementedError(); 46 | } 47 | 48 | @override 49 | Future post(Uri url, {Map? headers, Object? body, Encoding? encoding}) { 50 | throw UnimplementedError(); 51 | } 52 | 53 | @override 54 | Future put(Uri url, {Map? headers, Object? body, Encoding? encoding}) { 55 | throw UnimplementedError(); 56 | } 57 | 58 | @override 59 | Future read(Uri url, {Map? headers}) { 60 | throw UnimplementedError(); 61 | } 62 | 63 | @override 64 | Future readBytes(Uri url, {Map? headers}) { 65 | throw UnimplementedError(); 66 | } 67 | 68 | @override 69 | Future send(BaseRequest request) { 70 | throw UnimplementedError(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/infra/api/mocks/http_get_client_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/api/clients/http_get_client.dart'; 2 | import 'package:advanced_flutter/infra/types/json.dart'; 3 | 4 | import '../../../mocks/fakes.dart'; 5 | 6 | final class HttpGetClientSpy implements HttpGetClient { 7 | String? url; 8 | int callsCount = 0; 9 | Json? params; 10 | Json? queryString; 11 | Json? headers; 12 | dynamic response = anyJson(); 13 | Error? error; 14 | 15 | @override 16 | Future get({ required String url, Json? headers, Json? params, Json? queryString }) async { 17 | this.url = url; 18 | this.params = params; 19 | this.queryString = queryString; 20 | this.headers = headers; 21 | callsCount++; 22 | if (error != null) throw error!; 23 | return response; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/infra/api/repositories/load_next_event_api_repo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/api/repositories/load_next_event_api_repo.dart'; 4 | 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import '../../../mocks/fakes.dart'; 8 | import '../../mocks/mapper_spy.dart'; 9 | import '../mocks/http_get_client_spy.dart'; 10 | 11 | void main() { 12 | late String groupId; 13 | late String url; 14 | late HttpGetClientSpy httpClient; 15 | late MapperSpy mapper; 16 | late LoadNextEventApiRepository sut; 17 | 18 | setUp(() { 19 | groupId = anyString(); 20 | url = anyString(); 21 | httpClient = HttpGetClientSpy(); 22 | mapper = MapperSpy(toDtoOutput: anyNextEvent()); 23 | sut = LoadNextEventApiRepository(httpClient: httpClient, url: url, mapper: mapper); 24 | }); 25 | 26 | test('should call HttpClient with correct input', () async { 27 | await sut.loadNextEvent(groupId: groupId); 28 | expect(httpClient.url, url); 29 | expect(httpClient.params, { 'groupId': groupId }); 30 | expect(httpClient.callsCount, 1); 31 | }); 32 | 33 | test('should return NextEvent on success', () async { 34 | final event = await sut.loadNextEvent(groupId: groupId); 35 | expect(mapper.toDtoIntput, httpClient.response); 36 | expect(mapper.toDtoIntputCallsCount, 1); 37 | expect(event, mapper.toDtoOutput); 38 | }); 39 | 40 | test('should rethrow on error', () async { 41 | final error = Error(); 42 | httpClient.error = error; 43 | final future = sut.loadNextEvent(groupId: groupId); 44 | expect(future, throwsA(error)); 45 | }); 46 | 47 | test('should throw UnexpectedError on null response', () async { 48 | httpClient.response = null; 49 | final future = sut.loadNextEvent(groupId: groupId); 50 | expect(future, throwsA(isA())); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/infra/cache/adapters/cache_manager_adapter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/infra/cache/adapters/cache_manager_adapter.dart'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../../mocks/fakes.dart'; 7 | import '../mocks/cache_manager_spy.dart'; 8 | 9 | void main() { 10 | late String key; 11 | late CacheManagerSpy client; 12 | late CacheManagerAdapter sut; 13 | 14 | setUp(() { 15 | key = anyString(); 16 | client = CacheManagerSpy(); 17 | sut = CacheManagerAdapter(client: client); 18 | }); 19 | 20 | group('get', () { 21 | test('should call getFileFromCache with correct input', () async { 22 | await sut.get(key: key); 23 | expect(client.key, key); 24 | expect(client.getFileFromCacheCallsCount, 1); 25 | }); 26 | 27 | test('should return null if FileInfo is empty', () async { 28 | client.simulateEmptyFileInfo(); 29 | final json = await sut.get(key: key); 30 | expect(json, isNull); 31 | }); 32 | 33 | test('should return null if cache is old', () async { 34 | client.simulateCacheOld(); 35 | final json = await sut.get(key: key); 36 | expect(json, isNull); 37 | }); 38 | 39 | test('should call file.exists only once', () async { 40 | await sut.get(key: key); 41 | expect(client.file.existsCallsCount, 1); 42 | }); 43 | 44 | test('should return null if file is empty', () async { 45 | client.file.simulateFileEmpty(); 46 | final json = await sut.get(key: key); 47 | expect(json, isNull); 48 | }); 49 | 50 | test('should call file.readAsString only once', () async { 51 | await sut.get(key: key); 52 | expect(client.file.readAsStringCallsCount, 1); 53 | }); 54 | 55 | test('should return null if cache is invalid', () async { 56 | client.file.simulateInvalidResponse(); 57 | final json = await sut.get(key: key); 58 | expect(json, isNull); 59 | }); 60 | 61 | test('should return json if cache is valid', () async { 62 | client.file.simulateResponse(''' 63 | { 64 | "key1": "value1", 65 | "key2": "value2" 66 | } 67 | '''); 68 | final json = await sut.get(key: key); 69 | expect(json['key1'], 'value1'); 70 | expect(json['key2'], 'value2'); 71 | }); 72 | 73 | test('should return null if file.readAsString fails', () async { 74 | client.file.simulateReadAsStringError(); 75 | final json = await sut.get(key: key); 76 | expect(json, isNull); 77 | }); 78 | 79 | test('should return null if file.exists fails', () async { 80 | client.file.simulateExistsError(); 81 | final json = await sut.get(key: key); 82 | expect(json, isNull); 83 | }); 84 | 85 | test('should return null if getFileFromCache fails', () async { 86 | client.simulateGetFileFromCacheError(); 87 | final json = await sut.get(key: key); 88 | expect(json, isNull); 89 | }); 90 | }); 91 | 92 | group('save', () { 93 | late Map value; 94 | 95 | setUp(() { 96 | value = { 97 | 'key1': anyString(), 98 | 'key2': anyIsoDate(), 99 | 'key3': anyBool(), 100 | 'key4': anyInt() 101 | }; 102 | }); 103 | 104 | test('should call putFile with correct input', () async { 105 | await sut.save(key: key, value: value); 106 | expect(client.key, key); 107 | expect(client.fileExtension, 'json'); 108 | expect(client.fileBytesDecoded, value); 109 | expect(client.putFileCallsCount, 1); 110 | }); 111 | 112 | test('should throw UnexpectedError when putFile fails', () async { 113 | client.simulatePutFileError(); 114 | final future = sut.save(key: key, value: value); 115 | expect(future, throwsA(isA())); 116 | }); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /test/infra/cache/mocks/cache_get_client_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/cache/clients/cache_get_client.dart'; 2 | 3 | import '../../../mocks/fakes.dart'; 4 | 5 | final class CacheGetClientSpy implements CacheGetClient { 6 | String? key; 7 | int callsCount = 0; 8 | dynamic response = anyJson(); 9 | Error? error; 10 | 11 | @override 12 | Future get({ required String key }) async { 13 | this.key = key; 14 | callsCount++; 15 | if (error != null) throw error!; 16 | return response; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/infra/cache/mocks/cache_manager_spy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:file/file.dart'; 5 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 6 | 7 | import 'file_spy.dart'; 8 | 9 | final class CacheManagerSpy implements BaseCacheManager { 10 | int getFileFromCacheCallsCount = 0; 11 | int putFileCallsCount = 0; 12 | String? key; 13 | String? fileExtension; 14 | dynamic fileBytesDecoded; 15 | FileSpy file = FileSpy(); 16 | bool _isFileInfoEmpty = false; 17 | DateTime _validTill = DateTime.now().add(const Duration(seconds: 2)); 18 | Error? _getFileFromCachError; 19 | Error? _putFileError; 20 | 21 | void simulateEmptyFileInfo() => _isFileInfoEmpty = true; 22 | void simulateCacheOld() => _validTill = DateTime.now().subtract(const Duration(seconds: 2)); 23 | void simulateGetFileFromCacheError() => _getFileFromCachError = Error(); 24 | void simulatePutFileError() => _putFileError = Error(); 25 | 26 | @override 27 | Future getFileFromCache(String key, {bool ignoreMemCache = false}) async { 28 | getFileFromCacheCallsCount++; 29 | this.key = key; 30 | if (_getFileFromCachError != null) throw _getFileFromCachError!; 31 | return _isFileInfoEmpty ? null : FileInfo(file, FileSource.Cache, _validTill, ''); 32 | } 33 | 34 | @override 35 | Future putFile(String url, Uint8List fileBytes, {String? key, String? eTag, Duration maxAge = const Duration(days: 30), String fileExtension = 'file'}) async { 36 | putFileCallsCount++; 37 | this.key = url; 38 | this.fileExtension = fileExtension; 39 | fileBytesDecoded = jsonDecode(utf8.decode(fileBytes)); 40 | if (_putFileError != null) throw _putFileError!; 41 | return file; 42 | } 43 | 44 | @override 45 | Future dispose() => throw UnimplementedError(); 46 | 47 | @override 48 | Future downloadFile(String url, {String? key, Map? authHeaders, bool force = false}) => throw UnimplementedError(); 49 | 50 | @override 51 | Future emptyCache() => throw UnimplementedError(); 52 | 53 | @override 54 | Stream getFile(String url, {String? key, Map? headers}) => throw UnimplementedError(); 55 | 56 | @override 57 | Future getFileFromMemory(String key) => throw UnimplementedError(); 58 | 59 | @override 60 | Stream getFileStream(String url, {String? key, Map? headers, bool? withProgress}) => throw UnimplementedError(); 61 | 62 | @override 63 | Future getSingleFile(String url, {String? key, Map? headers}) => throw UnimplementedError(); 64 | 65 | @override 66 | Future putFileStream(String url, Stream> source, {String? key, String? eTag, Duration maxAge = const Duration(days: 30), String fileExtension = 'file'}) => throw UnimplementedError(); 67 | 68 | @override 69 | Future removeFile(String key) => throw UnimplementedError(); 70 | } 71 | -------------------------------------------------------------------------------- /test/infra/cache/mocks/cache_save_client_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/cache/clients/cache_save_client.dart'; 2 | 3 | final class CacheSaveClientMock implements CacheSaveClient { 4 | String? key; 5 | dynamic value; 6 | 7 | @override 8 | Future save({ required String key, required dynamic value }) async { 9 | this.key = key; 10 | this.value = value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/infra/cache/mocks/file_spy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:file/file.dart'; 5 | 6 | final class FileSpy implements File { 7 | int existsCallsCount = 0; 8 | int readAsStringCallsCount = 0; 9 | bool _fileExists = true; 10 | String _response = '{}'; 11 | Error? _readAsStringError; 12 | Error? _existsError; 13 | 14 | void simulateFileEmpty() => _fileExists = false; 15 | void simulateReadAsStringError() => _readAsStringError = Error(); 16 | void simulateExistsError() => _existsError = Error(); 17 | void simulateInvalidResponse() => _response = 'invalid_json'; 18 | void simulateResponse(String response) => _response = response; 19 | 20 | @override 21 | Future exists() async { 22 | existsCallsCount++; 23 | if (_existsError != null) throw _existsError!; 24 | return _fileExists; 25 | } 26 | 27 | @override 28 | Future readAsString({Encoding encoding = utf8}) async { 29 | readAsStringCallsCount++; 30 | if (_readAsStringError != null) throw _readAsStringError!; 31 | return _response; 32 | } 33 | 34 | @override 35 | File get absolute => throw UnimplementedError(); 36 | 37 | @override 38 | String get basename => throw UnimplementedError(); 39 | 40 | @override 41 | Future copy(String newPath) => throw UnimplementedError(); 42 | 43 | @override 44 | File copySync(String newPath) => throw UnimplementedError(); 45 | 46 | @override 47 | Future create({bool recursive = false, bool exclusive = false}) => throw UnimplementedError(); 48 | 49 | @override 50 | void createSync({bool recursive = false, bool exclusive = false}) => throw UnimplementedError(); 51 | 52 | @override 53 | Future delete({bool recursive = false}) => throw UnimplementedError(); 54 | 55 | @override 56 | void deleteSync({bool recursive = false}) => throw UnimplementedError(); 57 | 58 | @override 59 | String get dirname => throw UnimplementedError(); 60 | 61 | @override 62 | bool existsSync() => throw UnimplementedError(); 63 | 64 | @override 65 | FileSystem get fileSystem => throw UnimplementedError(); 66 | 67 | @override 68 | bool get isAbsolute => throw UnimplementedError(); 69 | 70 | @override 71 | Future lastAccessed() => throw UnimplementedError(); 72 | 73 | @override 74 | DateTime lastAccessedSync() => throw UnimplementedError(); 75 | 76 | @override 77 | Future lastModified() => throw UnimplementedError(); 78 | 79 | @override 80 | DateTime lastModifiedSync() => throw UnimplementedError(); 81 | 82 | @override 83 | Future length() => throw UnimplementedError(); 84 | 85 | @override 86 | int lengthSync() => throw UnimplementedError(); 87 | 88 | @override 89 | Future open({FileMode mode = FileMode.read}) => throw UnimplementedError(); 90 | 91 | @override 92 | Stream> openRead([int? start, int? end]) => throw UnimplementedError(); 93 | 94 | @override 95 | RandomAccessFile openSync({FileMode mode = FileMode.read}) => throw UnimplementedError(); 96 | 97 | @override 98 | IOSink openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8}) => throw UnimplementedError(); 99 | 100 | @override 101 | Directory get parent => throw UnimplementedError(); 102 | 103 | @override 104 | String get path => throw UnimplementedError(); 105 | 106 | @override 107 | Future readAsBytes() => throw UnimplementedError(); 108 | 109 | @override 110 | Uint8List readAsBytesSync() => throw UnimplementedError(); 111 | 112 | @override 113 | Future> readAsLines({Encoding encoding = utf8}) => throw UnimplementedError(); 114 | 115 | @override 116 | List readAsLinesSync({Encoding encoding = utf8}) => throw UnimplementedError(); 117 | 118 | @override 119 | String readAsStringSync({Encoding encoding = utf8}) => throw UnimplementedError(); 120 | 121 | @override 122 | Future rename(String newPath) => throw UnimplementedError(); 123 | 124 | @override 125 | File renameSync(String newPath) => throw UnimplementedError(); 126 | 127 | @override 128 | Future resolveSymbolicLinks() => throw UnimplementedError(); 129 | 130 | @override 131 | String resolveSymbolicLinksSync() => throw UnimplementedError(); 132 | 133 | @override 134 | Future setLastAccessed(DateTime time) => throw UnimplementedError(); 135 | 136 | @override 137 | void setLastAccessedSync(DateTime time) => throw UnimplementedError(); 138 | 139 | @override 140 | Future setLastModified(DateTime time) => throw UnimplementedError(); 141 | 142 | @override 143 | void setLastModifiedSync(DateTime time) => throw UnimplementedError(); 144 | 145 | @override 146 | Future stat() => throw UnimplementedError(); 147 | 148 | @override 149 | FileStat statSync() => throw UnimplementedError(); 150 | 151 | @override 152 | Uri get uri => throw UnimplementedError(); 153 | 154 | @override 155 | Stream watch({int events = FileSystemEvent.all, bool recursive = false}) => throw UnimplementedError(); 156 | 157 | @override 158 | Future writeAsBytes(List bytes, {FileMode mode = FileMode.write, bool flush = false}) => throw UnimplementedError(); 159 | 160 | @override 161 | void writeAsBytesSync(List bytes, {FileMode mode = FileMode.write, bool flush = false}) => throw UnimplementedError(); 162 | 163 | @override 164 | Future writeAsString(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) => throw UnimplementedError(); 165 | 166 | @override 167 | void writeAsStringSync(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) => throw UnimplementedError(); 168 | } 169 | -------------------------------------------------------------------------------- /test/infra/cache/repositories/load_next_event_cache_repo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/cache/repositories/load_next_event_cache_repo.dart'; 4 | 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import '../../../mocks/fakes.dart'; 8 | import '../../mocks/mapper_spy.dart'; 9 | import '../mocks/cache_get_client_spy.dart'; 10 | 11 | void main() { 12 | late String groupId; 13 | late String key; 14 | late CacheGetClientSpy cacheClient; 15 | late MapperSpy mapper; 16 | late LoadNextEventCacheRepository sut; 17 | 18 | setUp(() { 19 | groupId = anyString(); 20 | key = anyString(); 21 | cacheClient = CacheGetClientSpy(); 22 | mapper = MapperSpy(toDtoOutput: anyNextEvent()); 23 | sut = LoadNextEventCacheRepository(cacheClient: cacheClient, key: key, mapper: mapper); 24 | }); 25 | 26 | test('should call CacheClient with correct input', () async { 27 | await sut.loadNextEvent(groupId: groupId); 28 | expect(cacheClient.key, '$key:$groupId'); 29 | expect(cacheClient.callsCount, 1); 30 | }); 31 | 32 | test('should return NextEvent on success', () async { 33 | final event = await sut.loadNextEvent(groupId: groupId); 34 | expect(mapper.toDtoIntput, cacheClient.response); 35 | expect(mapper.toDtoIntputCallsCount, 1); 36 | expect(event, mapper.toDtoOutput); 37 | }); 38 | 39 | test('should rethrow on error', () async { 40 | final error = Error(); 41 | cacheClient.error = error; 42 | final future = sut.loadNextEvent(groupId: groupId); 43 | expect(future, throwsA(error)); 44 | }); 45 | 46 | test('should throw UnexpectedError on null response', () async { 47 | cacheClient.response = null; 48 | final future = sut.loadNextEvent(groupId: groupId); 49 | expect(future, throwsA(isA())); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/infra/mappers/next_event_mapper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 3 | import 'package:advanced_flutter/infra/mappers/next_event_mapper.dart'; 4 | 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import '../../mocks/fakes.dart'; 8 | import '../mocks/list_mapper_spy.dart'; 9 | 10 | void main() { 11 | late ListMapperSpy playerMapper; 12 | late NextEventMapper sut; 13 | 14 | setUp(() { 15 | playerMapper = ListMapperSpy(toDtoListOutput: anyNextEventPlayerList()); 16 | sut = NextEventMapper(playerMapper: playerMapper); 17 | }); 18 | 19 | test('should map to dto', () { 20 | final json = { 21 | 'groupName': anyString(), 22 | 'date': '2024-08-29T11:00:00.000', 23 | 'players': anyJsonArr() 24 | }; 25 | final dto = sut.toDto(json); 26 | expect(dto.groupName, json['groupName']); 27 | expect(dto.date, DateTime(2024, 8, 29, 11, 0)); 28 | expect(playerMapper.toDtoListIntput, json['players']); 29 | expect(playerMapper.toDtoListCallsCount, 1); 30 | expect(dto.players, playerMapper.toDtoListOutput); 31 | }); 32 | 33 | test('should map to json', () { 34 | final dto = NextEvent( 35 | groupName: anyString(), 36 | date: DateTime(2024, 8, 29, 13, 0), 37 | players: anyNextEventPlayerList() 38 | ); 39 | final json = sut.toJson(dto); 40 | expect(json['groupName'], dto.groupName); 41 | expect(json['date'], '2024-08-29T13:00:00.000'); 42 | expect(playerMapper.toJsonArrIntput, dto.players); 43 | expect(playerMapper.toJsonArrCallsCount, 1); 44 | expect(json['players'], playerMapper.toJsonArrOutput); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/infra/mappers/next_event_player_mapper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | import 'package:advanced_flutter/infra/mappers/next_event_player_mapper.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../../mocks/fakes.dart'; 6 | 7 | void main() { 8 | late NextEventPlayerMapper sut; 9 | 10 | setUp(() { 11 | sut = NextEventPlayerMapper(); 12 | }); 13 | 14 | test('should map to dto', () { 15 | final json = { 16 | 'id': anyString(), 17 | 'name': anyString(), 18 | 'position': anyString(), 19 | 'photo': anyString(), 20 | 'confirmationDate': '2024-08-29T11:00:00.000', 21 | 'isConfirmed': anyBool() 22 | }; 23 | final dto = sut.toDto(json); 24 | expect(dto.id, json['id']); 25 | expect(dto.name, json['name']); 26 | expect(dto.position, json['position']); 27 | expect(dto.photo, json['photo']); 28 | expect(dto.confirmationDate, DateTime(2024, 8, 29, 11, 0)); 29 | expect(dto.isConfirmed, json['isConfirmed']); 30 | }); 31 | 32 | test('should map to dto with empty fields', () { 33 | final json = { 34 | 'id': anyString(), 35 | 'name': anyString(), 36 | 'isConfirmed': anyBool() 37 | }; 38 | final dto = sut.toDto(json); 39 | expect(dto.id, json['id']); 40 | expect(dto.name, json['name']); 41 | expect(dto.position, isNull); 42 | expect(dto.photo, isNull); 43 | expect(dto.confirmationDate, isNull); 44 | expect(dto.isConfirmed, json['isConfirmed']); 45 | }); 46 | 47 | test('should map to json', () { 48 | final dto = NextEventPlayer( 49 | id: anyString(), 50 | name: anyString(), 51 | isConfirmed: anyBool(), 52 | photo: anyString(), 53 | position: anyString(), 54 | confirmationDate: DateTime(2024, 8, 29, 13, 0) 55 | ); 56 | final json = sut.toJson(dto); 57 | expect(json['id'], dto.id); 58 | expect(json['name'], dto.name); 59 | expect(json['position'], dto.position); 60 | expect(json['photo'], dto.photo); 61 | expect(json['confirmationDate'], '2024-08-29T13:00:00.000'); 62 | expect(json['isConfirmed'], dto.isConfirmed); 63 | }); 64 | 65 | test('should map to json with empty fields', () { 66 | final dto = NextEventPlayer( 67 | id: anyString(), 68 | name: anyString(), 69 | isConfirmed: anyBool() 70 | ); 71 | final json = sut.toJson(dto); 72 | expect(json['id'], dto.id); 73 | expect(json['name'], dto.name); 74 | expect(json['position'], isNull); 75 | expect(json['photo'], isNull); 76 | expect(json['confirmationDate'], isNull); 77 | expect(json['isConfirmed'], dto.isConfirmed); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/infra/mocks/list_mapper_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 2 | import 'package:advanced_flutter/infra/types/json.dart'; 3 | 4 | import '../../mocks/fakes.dart'; 5 | 6 | final class ListMapperSpy extends ListMapper { 7 | dynamic toDtoListIntput; 8 | List? toJsonArrIntput; 9 | int toDtoListCallsCount = 0; 10 | int toJsonArrCallsCount = 0; 11 | List toDtoListOutput; 12 | JsonArr toJsonArrOutput = anyJsonArr(); 13 | 14 | ListMapperSpy({ 15 | required this.toDtoListOutput 16 | }); 17 | 18 | @override 19 | List toDtoList(dynamic arr) { 20 | toDtoListIntput = arr; 21 | toDtoListCallsCount++; 22 | return toDtoListOutput; 23 | } 24 | 25 | @override 26 | JsonArr toJsonArr(List list) { 27 | toJsonArrIntput = list; 28 | toJsonArrCallsCount++; 29 | return toJsonArrOutput; 30 | } 31 | 32 | @override 33 | Dto toDto(Json json) => throw UnimplementedError(); 34 | 35 | @override 36 | Json toJson(Dto dto) => throw UnimplementedError(); 37 | } 38 | -------------------------------------------------------------------------------- /test/infra/mocks/load_next_event_repo_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 2 | 3 | import '../../mocks/fakes.dart'; 4 | 5 | final class LoadNextEventRepositorySpy { 6 | String? groupId; 7 | int callsCount = 0; 8 | NextEvent output = anyNextEvent(); 9 | Object? error; 10 | 11 | Future loadNextEvent({ required String groupId }) async { 12 | this.groupId = groupId; 13 | callsCount++; 14 | if (error != null) throw error!; 15 | return output; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/infra/mocks/mapper_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/infra/mappers/mapper.dart'; 2 | import 'package:advanced_flutter/infra/types/json.dart'; 3 | 4 | import '../../mocks/fakes.dart'; 5 | 6 | final class MapperSpy implements Mapper { 7 | Json? toDtoIntput; 8 | int toDtoIntputCallsCount = 0; 9 | int toJsonCallsCount = 0; 10 | Dto toDtoOutput; 11 | Dto? toJsonIntput; 12 | Json toJsonOutput = anyJson(); 13 | 14 | MapperSpy({ 15 | required this.toDtoOutput 16 | }); 17 | 18 | @override 19 | Dto toDto(Json json) { 20 | toDtoIntput = json; 21 | toDtoIntputCallsCount++; 22 | return toDtoOutput; 23 | } 24 | 25 | @override 26 | Json toJson(Dto dto) { 27 | toJsonIntput = dto; 28 | toJsonCallsCount++; 29 | return toJsonOutput; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/infra/repositories/load_next_event_from_api_with_cache_fallback_repo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/errors.dart'; 2 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 3 | import 'package:advanced_flutter/infra/respositories/load_next_event_from_api_with_cache_fallback_repo.dart'; 4 | 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | import '../../mocks/fakes.dart'; 8 | import '../cache/mocks/cache_save_client_mock.dart'; 9 | import '../mocks/load_next_event_repo_spy.dart'; 10 | import '../mocks/mapper_spy.dart'; 11 | 12 | void main() { 13 | late String groupId; 14 | late String key; 15 | late LoadNextEventRepositorySpy apiRepo; 16 | late LoadNextEventRepositorySpy cacheRepo; 17 | late CacheSaveClientMock cacheClient; 18 | late MapperSpy mapper; 19 | late LoadNextEventFromApiWithCacheFallbackRepository sut; 20 | 21 | setUp(() { 22 | groupId = anyString(); 23 | key = anyString(); 24 | apiRepo = LoadNextEventRepositorySpy(); 25 | cacheRepo = LoadNextEventRepositorySpy(); 26 | cacheClient = CacheSaveClientMock(); 27 | mapper = MapperSpy(toDtoOutput: anyNextEvent()); 28 | sut = LoadNextEventFromApiWithCacheFallbackRepository( 29 | key: key, 30 | cacheClient: cacheClient, 31 | loadNextEventFromApi: apiRepo.loadNextEvent, 32 | loadNextEventFromCache: cacheRepo.loadNextEvent, 33 | mapper: mapper 34 | ); 35 | }); 36 | 37 | test('should load event data from api repo', () async { 38 | await sut.loadNextEvent(groupId: groupId); 39 | expect(apiRepo.groupId, groupId); 40 | expect(apiRepo.callsCount, 1); 41 | }); 42 | 43 | test('should save event data from api on cache', () async { 44 | await sut.loadNextEvent(groupId: groupId); 45 | expect(cacheClient.key, '$key:$groupId'); 46 | expect(cacheClient.value, mapper.toJsonOutput); 47 | expect(mapper.toJsonIntput, apiRepo.output); 48 | expect(mapper.toJsonCallsCount, 1); 49 | }); 50 | 51 | test('should return api data on success', () async { 52 | final event = await sut.loadNextEvent(groupId: groupId); 53 | expect(event, apiRepo.output); 54 | }); 55 | 56 | test('should rethrow api error when its SessionExpiredError', () async { 57 | apiRepo.error = SessionExpiredError(); 58 | final future = sut.loadNextEvent(groupId: groupId); 59 | expect(future, throwsA(apiRepo.error)); 60 | }); 61 | 62 | test('should load event data from cache repo when api fails', () async { 63 | apiRepo.error = Error(); 64 | await sut.loadNextEvent(groupId: groupId); 65 | expect(cacheRepo.groupId, groupId); 66 | expect(cacheRepo.callsCount, 1); 67 | }); 68 | 69 | test('should return cache data when api fails', () async { 70 | apiRepo.error = Error(); 71 | final event = await sut.loadNextEvent(groupId: groupId); 72 | expect(event, cacheRepo.output); 73 | }); 74 | 75 | test('should rethrow cache error when api and cache fails', () async { 76 | apiRepo.error = Error(); 77 | cacheRepo.error = Error(); 78 | final future = sut.loadNextEvent(groupId: groupId); 79 | expect(future, throwsA(cacheRepo.error)); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /test/mocks/fakes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:advanced_flutter/domain/entities/next_event.dart'; 4 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 5 | import 'package:advanced_flutter/infra/types/json.dart'; 6 | 7 | int anyInt([int max = 999999999]) => Random().nextInt(max); 8 | String anyString() => anyInt().toString(); 9 | bool anyBool() => Random().nextBool(); 10 | DateTime anyDate() => DateTime.fromMillisecondsSinceEpoch(anyInt()); 11 | String anyIsoDate() => anyDate().toIso8601String(); 12 | Json anyJson() => { anyString(): anyString() }; 13 | JsonArr anyJsonArr() => List.generate(anyInt(5), (index) => anyJson()); 14 | NextEvent anyNextEvent() => NextEvent(groupName: anyString(), date: anyDate(), players: anyNextEventPlayerList()); 15 | NextEventPlayer anyNextEventPlayer() => NextEventPlayer(id: anyString(), name: anyString(), isConfirmed: anyBool()); 16 | List anyNextEventPlayerList() => List.generate(anyInt(5), (index) => anyNextEventPlayer()); 17 | -------------------------------------------------------------------------------- /test/presentation/mocks/next_event_presenter_spy.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/presentation/presenters/next_event_presenter.dart'; 2 | import 'package:advanced_flutter/presentation/viewmodels/next_event_player_viewmodel.dart'; 3 | import 'package:advanced_flutter/presentation/viewmodels/next_event_viewmodel.dart'; 4 | 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | final class NextEventPresenterSpy implements NextEventPresenter { 8 | int callsCount = 0; 9 | String? groupId; 10 | bool? isReload; 11 | var nextEventSubject = BehaviorSubject(); 12 | var isBusySubject = BehaviorSubject(); 13 | 14 | @override 15 | Stream get nextEventStream => nextEventSubject.stream; 16 | 17 | @override 18 | Stream get isBusyStream => isBusySubject.stream; 19 | 20 | void emitNextEvent([NextEventViewModel? viewModel]) { 21 | nextEventSubject.add(viewModel ?? const NextEventViewModel()); 22 | } 23 | 24 | void emitNextEventWith({ List goalkeepers = const [], List players = const [], List out = const [], List doubt = const [] }) { 25 | nextEventSubject.add(NextEventViewModel(goalkeepers: goalkeepers, players: players, out: out, doubt: doubt)); 26 | } 27 | 28 | void emitError() { 29 | nextEventSubject.addError(Error()); 30 | } 31 | 32 | void emitIsBusy([bool isBusy = true]) { 33 | isBusySubject.add(isBusy); 34 | } 35 | 36 | @override 37 | Future loadNextEvent({ required String groupId, bool isReload = false }) async { 38 | callsCount++; 39 | this.groupId = groupId; 40 | this.isReload = isReload; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/presentation/rx/next_event_rx_presenter_test.dart: -------------------------------------------------------------------------------- 1 | @Timeout(Duration(seconds: 1)) library; 2 | 3 | import 'package:advanced_flutter/presentation/rx/next_event_rx_presenter.dart'; 4 | import 'package:advanced_flutter/presentation/viewmodels/next_event_viewmodel.dart'; 5 | 6 | import 'package:flutter_test/flutter_test.dart'; 7 | 8 | import '../../domain/mocks/next_event_loader_spy.dart'; 9 | import '../../mocks/fakes.dart'; 10 | 11 | void main() { 12 | late NextEventLoaderSpy nextEventLoader; 13 | late String groupId; 14 | late NextEventRxPresenter sut; 15 | 16 | setUp(() { 17 | nextEventLoader = NextEventLoaderSpy(); 18 | groupId = anyString(); 19 | sut = NextEventRxPresenter(nextEventLoader: nextEventLoader.call); 20 | }); 21 | 22 | test('should get event data', () async { 23 | await sut.loadNextEvent(groupId: groupId); 24 | expect(nextEventLoader.callsCount, 1); 25 | expect(nextEventLoader.groupId, groupId); 26 | }); 27 | 28 | test('should emit correct events on reload with error', () async { 29 | nextEventLoader.error = Error(); 30 | expectLater(sut.nextEventStream, emitsError(nextEventLoader.error)); 31 | expectLater(sut.isBusyStream, emitsInOrder([true, false])); 32 | await sut.loadNextEvent(groupId: groupId, isReload: true); 33 | }); 34 | 35 | test('should emit correct events on load with error', () async { 36 | nextEventLoader.error = Error(); 37 | expectLater(sut.nextEventStream, emitsError(nextEventLoader.error)); 38 | sut.isBusyStream.listen(neverCalled); 39 | await sut.loadNextEvent(groupId: groupId); 40 | }); 41 | 42 | test('should emit correct events on reload with success', () async { 43 | expectLater(sut.isBusyStream, emitsInOrder([true, false])); 44 | expectLater(sut.nextEventStream, emits(isA())); 45 | await sut.loadNextEvent(groupId: groupId, isReload: true); 46 | }); 47 | 48 | test('should emit correct events on load with success', () async { 49 | sut.isBusyStream.listen(neverCalled); 50 | expectLater(sut.nextEventStream, emits(isA())); 51 | await sut.loadNextEvent(groupId: groupId); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/presentation/viewmodels/next_event_player_viewmodel_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/domain/entities/next_event_player.dart'; 2 | import 'package:advanced_flutter/presentation/viewmodels/next_event_player_viewmodel.dart'; 3 | 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../mocks/fakes.dart'; 7 | 8 | void main() { 9 | test('should build doubt list sorted by name', () async { 10 | final doubt = NextEventPlayerViewModel.mapDoubtPlayers([ 11 | NextEventPlayer(id: anyString(), name: 'C', isConfirmed: anyBool()), 12 | NextEventPlayer(id: anyString(), name: 'A', isConfirmed: anyBool()), 13 | NextEventPlayer(id: anyString(), name: 'B', isConfirmed: anyBool(), confirmationDate: anyDate()), 14 | NextEventPlayer(id: anyString(), name: 'D', isConfirmed: anyBool()) 15 | ]); 16 | expect(doubt.length, 3); 17 | expect(doubt[0].name, 'A'); 18 | expect(doubt[1].name, 'C'); 19 | expect(doubt[2].name, 'D'); 20 | }); 21 | 22 | test('should build out list sorted by confirmation date', () async { 23 | final out = NextEventPlayerViewModel.mapOutPlayers([ 24 | NextEventPlayer(id: anyString(), name: 'C', isConfirmed: false, confirmationDate: DateTime(2024, 1, 1, 10)), 25 | NextEventPlayer(id: anyString(), name: 'A', isConfirmed: anyBool()), 26 | NextEventPlayer(id: anyString(), name: 'B', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 11)), 27 | NextEventPlayer(id: anyString(), name: 'E', isConfirmed: false, confirmationDate: DateTime(2024, 1, 1, 9)), 28 | NextEventPlayer(id: anyString(), name: 'D', isConfirmed: false, confirmationDate: DateTime(2024, 1, 1, 12)) 29 | ]); 30 | expect(out.length, 3); 31 | expect(out[0].name, 'E'); 32 | expect(out[1].name, 'C'); 33 | expect(out[2].name, 'D'); 34 | }); 35 | 36 | test('should build goalkeepers list sorted by confirmation date', () async { 37 | final goalkeepers = NextEventPlayerViewModel.mapGoalkeepers([ 38 | NextEventPlayer(id: anyString(), name: 'C', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 10), position: 'goalkeeper'), 39 | NextEventPlayer(id: anyString(), name: 'A', isConfirmed: anyBool()), 40 | NextEventPlayer(id: anyString(), name: 'B', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 11), position: 'defender'), 41 | NextEventPlayer(id: anyString(), name: 'E', isConfirmed: false, confirmationDate: DateTime(2024, 1, 1, 9)), 42 | NextEventPlayer(id: anyString(), name: 'D', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 12)), 43 | NextEventPlayer(id: anyString(), name: 'F', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 8), position: 'goalkeeper') 44 | ]); 45 | expect(goalkeepers.length, 2); 46 | expect(goalkeepers[0].name, 'F'); 47 | expect(goalkeepers[1].name, 'C'); 48 | }); 49 | 50 | test('should build players list sorted by confirmation date', () async { 51 | final players = NextEventPlayerViewModel.mapInPlayers([ 52 | NextEventPlayer(id: anyString(), name: 'C', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 10), position: 'goalkeeper'), 53 | NextEventPlayer(id: anyString(), name: 'A', isConfirmed: anyBool()), 54 | NextEventPlayer(id: anyString(), name: 'B', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 11), position: 'defender'), 55 | NextEventPlayer(id: anyString(), name: 'E', isConfirmed: false, confirmationDate: DateTime(2024, 1, 1, 9)), 56 | NextEventPlayer(id: anyString(), name: 'D', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 12)), 57 | NextEventPlayer(id: anyString(), name: 'F', isConfirmed: true, confirmationDate: DateTime(2024, 1, 1, 8), position: 'goalkeeper') 58 | ]); 59 | expect(players.length, 2); 60 | expect(players[0].name, 'B'); 61 | expect(players[1].name, 'D'); 62 | }); 63 | 64 | test('should map player without confirmation date', () async { 65 | final player = NextEventPlayer(id: anyString(), name: anyString(), isConfirmed: anyBool(), photo: anyString(), position: anyString(), confirmationDate: null); 66 | final viewmodel = NextEventPlayerViewModel.fromEntity(player); 67 | expect(viewmodel.name, player.name); 68 | expect(viewmodel.initials, player.initials); 69 | expect(viewmodel.isConfirmed, null); 70 | expect(viewmodel.photo, player.photo); 71 | expect(viewmodel.position, player.position); 72 | }); 73 | 74 | test('should map player with confirmation date', () async { 75 | final player = NextEventPlayer(id: anyString(), name: anyString(), isConfirmed: anyBool(), photo: anyString(), position: anyString(), confirmationDate: anyDate()); 76 | final viewmodel = NextEventPlayerViewModel.fromEntity(player); 77 | expect(viewmodel.name, player.name); 78 | expect(viewmodel.initials, player.initials); 79 | expect(viewmodel.isConfirmed, player.isConfirmed); 80 | expect(viewmodel.photo, player.photo); 81 | expect(viewmodel.position, player.position); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /test/ui/components/player_photo_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/ui/components/player_photo.dart'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:network_image_mock/network_image_mock.dart'; 6 | 7 | void main() { 8 | testWidgets('should present initials when there is no photo', (tester) async { 9 | await tester.pumpWidget(const MaterialApp(home: PlayerPhoto(initials: 'RO', photo: null))); 10 | expect(find.text('RO'), findsOneWidget); 11 | }); 12 | 13 | testWidgets('should hide initials when there is photo', (tester) async { 14 | mockNetworkImagesFor(() async { 15 | await tester.pumpWidget(const MaterialApp(home: PlayerPhoto(initials: 'RO', photo: 'http://any-url.com'))); 16 | expect(find.text('RO'), findsNothing); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/ui/components/player_position_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/ui/components/player_position.dart'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | testWidgets('should handle goalkeeper position', (tester) async { 8 | await tester.pumpWidget(const MaterialApp(home: PlayerPosition(position: 'goalkeeper'))); 9 | expect(find.text('Goleiro'), findsOneWidget); 10 | }); 11 | 12 | testWidgets('should handle defender position', (tester) async { 13 | await tester.pumpWidget(const MaterialApp(home: PlayerPosition(position: 'defender'))); 14 | expect(find.text('Zagueiro'), findsOneWidget); 15 | }); 16 | 17 | testWidgets('should handle midfielder position', (tester) async { 18 | await tester.pumpWidget(const MaterialApp(home: PlayerPosition(position: 'midfielder'))); 19 | expect(find.text('Meia'), findsOneWidget); 20 | }); 21 | 22 | testWidgets('should handle forward position', (tester) async { 23 | await tester.pumpWidget(const MaterialApp(home: PlayerPosition(position: 'forward'))); 24 | expect(find.text('Atacante'), findsOneWidget); 25 | }); 26 | 27 | testWidgets('should handle positionless', (tester) async { 28 | await tester.pumpWidget(const MaterialApp(home: PlayerPosition(position: null))); 29 | expect(find.text('Gandula'), findsOneWidget); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/ui/components/player_status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/ui/components/player_status.dart'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() { 7 | testWidgets('should present green status', (tester) async { 8 | await tester.pumpWidget(const MaterialApp(home: PlayerStatus(isConfirmed: true))); 9 | final decoration = tester.firstWidget(find.byType(Container)).decoration as BoxDecoration; 10 | expect(decoration.color, Colors.teal); 11 | }); 12 | 13 | testWidgets('should present red status', (tester) async { 14 | await tester.pumpWidget(const MaterialApp(home: PlayerStatus(isConfirmed: false))); 15 | final decoration = tester.firstWidget(find.byType(Container)).decoration as BoxDecoration; 16 | expect(decoration.color, Colors.pink); 17 | }); 18 | 19 | testWidgets('should present grey status', (tester) async { 20 | await tester.pumpWidget(const MaterialApp(home: PlayerStatus(isConfirmed: null))); 21 | final decoration = tester.firstWidget(find.byType(Container)).decoration as BoxDecoration; 22 | expect(decoration.color, Colors.blueGrey); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/ui/pages/next_event_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:advanced_flutter/presentation/viewmodels/next_event_player_viewmodel.dart'; 2 | import 'package:advanced_flutter/ui/components/player_photo.dart'; 3 | import 'package:advanced_flutter/ui/components/player_position.dart'; 4 | import 'package:advanced_flutter/ui/components/player_status.dart'; 5 | import 'package:advanced_flutter/ui/pages/next_event_page.dart'; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import '../../mocks/fakes.dart'; 11 | import '../../presentation/mocks/next_event_presenter_spy.dart'; 12 | 13 | void main() { 14 | late NextEventPresenterSpy presenter; 15 | late String groupId; 16 | late Widget sut; 17 | 18 | setUp(() { 19 | presenter = NextEventPresenterSpy(); 20 | groupId = anyString(); 21 | sut = MaterialApp(home: NextEventPage(presenter: presenter, groupId: groupId)); 22 | }); 23 | 24 | testWidgets('should load event data on page init', (tester) async { 25 | await tester.pumpWidget(sut); 26 | expect(presenter.callsCount, 1); 27 | expect(presenter.groupId, groupId); 28 | expect(presenter.isReload, false); 29 | }); 30 | 31 | testWidgets('should present spinner while data is loading', (tester) async { 32 | await tester.pumpWidget(sut); 33 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 34 | }); 35 | 36 | testWidgets('should hide spinner on load success', (tester) async { 37 | await tester.pumpWidget(sut); 38 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 39 | presenter.emitNextEvent(); 40 | await tester.pump(); 41 | expect(find.byType(CircularProgressIndicator), findsNothing); 42 | }); 43 | 44 | testWidgets('should hide spinner on load error', (tester) async { 45 | await tester.pumpWidget(sut); 46 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 47 | presenter.emitError(); 48 | await tester.pump(); 49 | expect(find.byType(CircularProgressIndicator), findsNothing); 50 | }); 51 | 52 | testWidgets('should present goalkeepers section', (tester) async { 53 | await tester.pumpWidget(sut); 54 | presenter.emitNextEventWith(goalkeepers: [ 55 | NextEventPlayerViewModel(name: 'Rodrigo', initials: anyString()), 56 | NextEventPlayerViewModel(name: 'Rafael', initials: anyString()), 57 | NextEventPlayerViewModel(name: 'Pedro', initials: anyString()) 58 | ]); 59 | await tester.pump(); 60 | expect(find.text('DENTRO - GOLEIROS'), findsOneWidget); 61 | expect(find.text('3'), findsOneWidget); 62 | expect(find.text('Rodrigo'), findsOneWidget); 63 | expect(find.text('Rafael'), findsOneWidget); 64 | expect(find.text('Pedro'), findsOneWidget); 65 | expect(find.byType(PlayerPosition), findsExactly(3)); 66 | expect(find.byType(PlayerStatus), findsExactly(3)); 67 | expect(find.byType(PlayerPhoto), findsExactly(3)); 68 | }); 69 | 70 | testWidgets('should present players section', (tester) async { 71 | await tester.pumpWidget(sut); 72 | presenter.emitNextEventWith(players: [ 73 | NextEventPlayerViewModel(name: 'Rodrigo', initials: anyString()), 74 | NextEventPlayerViewModel(name: 'Rafael', initials: anyString()), 75 | NextEventPlayerViewModel(name: 'Pedro', initials: anyString()) 76 | ]); 77 | await tester.pump(); 78 | expect(find.text('DENTRO - JOGADORES'), findsOneWidget); 79 | expect(find.text('3'), findsOneWidget); 80 | expect(find.text('Rodrigo'), findsOneWidget); 81 | expect(find.text('Rafael'), findsOneWidget); 82 | expect(find.text('Pedro'), findsOneWidget); 83 | expect(find.byType(PlayerPosition), findsExactly(3)); 84 | expect(find.byType(PlayerStatus), findsExactly(3)); 85 | expect(find.byType(PlayerPhoto), findsExactly(3)); 86 | }); 87 | 88 | testWidgets('should present out section', (tester) async { 89 | await tester.pumpWidget(sut); 90 | presenter.emitNextEventWith(out: [ 91 | NextEventPlayerViewModel(name: 'Rodrigo', initials: anyString()), 92 | NextEventPlayerViewModel(name: 'Rafael', initials: anyString()), 93 | NextEventPlayerViewModel(name: 'Pedro', initials: anyString()) 94 | ]); 95 | await tester.pump(); 96 | expect(find.text('FORA'), findsOneWidget); 97 | expect(find.text('3'), findsOneWidget); 98 | expect(find.text('Rodrigo'), findsOneWidget); 99 | expect(find.text('Rafael'), findsOneWidget); 100 | expect(find.text('Pedro'), findsOneWidget); 101 | expect(find.byType(PlayerPosition), findsExactly(3)); 102 | expect(find.byType(PlayerStatus), findsExactly(3)); 103 | expect(find.byType(PlayerPhoto), findsExactly(3)); 104 | }); 105 | 106 | testWidgets('should present doubt section', (tester) async { 107 | await tester.pumpWidget(sut); 108 | presenter.emitNextEventWith(doubt: [ 109 | NextEventPlayerViewModel(name: 'Rodrigo', initials: anyString()), 110 | NextEventPlayerViewModel(name: 'Rafael', initials: anyString()), 111 | NextEventPlayerViewModel(name: 'Pedro', initials: anyString()) 112 | ]); 113 | await tester.pump(); 114 | expect(find.text('DÚVIDA'), findsOneWidget); 115 | expect(find.text('3'), findsOneWidget); 116 | expect(find.text('Rodrigo'), findsOneWidget); 117 | expect(find.text('Rafael'), findsOneWidget); 118 | expect(find.text('Pedro'), findsOneWidget); 119 | expect(find.byType(PlayerPosition), findsExactly(3)); 120 | expect(find.byType(PlayerStatus), findsExactly(3)); 121 | expect(find.byType(PlayerPhoto), findsExactly(3)); 122 | }); 123 | 124 | testWidgets('should hide all sections', (tester) async { 125 | await tester.pumpWidget(sut); 126 | presenter.emitNextEvent(); 127 | await tester.pump(); 128 | expect(find.text('DENTRO - GOLEIROS'), findsNothing); 129 | expect(find.text('DENTRO - JOGADORES'), findsNothing); 130 | expect(find.text('FORA'), findsNothing); 131 | expect(find.text('DÚVIDA'), findsNothing); 132 | expect(find.byType(PlayerPosition), findsNothing); 133 | expect(find.byType(PlayerStatus), findsNothing); 134 | expect(find.byType(PlayerPhoto), findsNothing); 135 | }); 136 | 137 | testWidgets('should present error message on load error', (tester) async { 138 | await tester.pumpWidget(sut); 139 | presenter.emitError(); 140 | await tester.pump(); 141 | expect(find.text('DENTRO - GOLEIROS'), findsNothing); 142 | expect(find.text('DENTRO - JOGADORES'), findsNothing); 143 | expect(find.text('FORA'), findsNothing); 144 | expect(find.text('DÚVIDA'), findsNothing); 145 | expect(find.byType(PlayerPosition), findsNothing); 146 | expect(find.byType(PlayerStatus), findsNothing); 147 | expect(find.byType(PlayerPhoto), findsNothing); 148 | expect(find.text('Algo errado aconteceu, tente novamente.'), findsOneWidget); 149 | expect(find.text('RECARREGAR'), findsOneWidget); 150 | }); 151 | 152 | testWidgets('should load event data on reload click', (tester) async { 153 | await tester.pumpWidget(sut); 154 | expect(presenter.callsCount, 1); 155 | expect(presenter.groupId, groupId); 156 | expect(presenter.isReload, false); 157 | presenter.emitError(); 158 | await tester.pump(); 159 | await tester.tap(find.text('RECARREGAR')); 160 | expect(presenter.callsCount, 2); 161 | expect(presenter.groupId, groupId); 162 | expect(presenter.isReload, true); 163 | }); 164 | 165 | testWidgets('should handle spinner on page busy event', (tester) async { 166 | await tester.pumpWidget(sut); 167 | presenter.emitError(); 168 | await tester.pump(); 169 | presenter.emitIsBusy(); 170 | await tester.pump(); 171 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 172 | presenter.emitIsBusy(false); 173 | await tester.pump(); 174 | expect(find.byType(CircularProgressIndicator), findsNothing); 175 | }); 176 | 177 | testWidgets('should load event data on pull to refresh', (tester) async { 178 | await tester.pumpWidget(sut); 179 | expect(presenter.callsCount, 1); 180 | expect(presenter.groupId, groupId); 181 | expect(presenter.isReload, false); 182 | presenter.emitNextEvent(); 183 | await tester.pump(); 184 | await tester.flingFrom(const Offset(50, 100), const Offset(0, 400), 800); 185 | await tester.pumpAndSettle(); 186 | expect(presenter.callsCount, 2); 187 | expect(presenter.groupId, groupId); 188 | expect(presenter.isReload, true); 189 | }); 190 | } 191 | --------------------------------------------------------------------------------