├── .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 |
--------------------------------------------------------------------------------