├── .fvmrc
├── ios
├── Runner
│ ├── Runner-Bridging-Header.h
│ ├── Assets.xcassets
│ │ ├── LaunchImage.imageset
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ ├── README.md
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── 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-1024x1024@1x.png
│ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── Flutter
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── AppFrameworkInfo.plist
├── Runner.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ └── project.pbxproj
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── .gitignore
├── Podfile.lock
└── Podfile
├── web
├── favicon.png
├── icons
│ ├── Icon-192.png
│ ├── Icon-512.png
│ ├── Icon-maskable-192.png
│ └── Icon-maskable-512.png
├── manifest.json
└── index.html
├── scripts
├── check_dependencies.sh
└── generate_models.sh
├── android
├── gradle.properties
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── 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
│ │ │ │ ├── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── values
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values-night
│ │ │ │ │ └── styles.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── flutter_arch_comp
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ └── build.gradle
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── .gitignore
├── build.gradle
└── settings.gradle
├── assets
└── images
│ ├── flutter_logo.png
│ ├── 2.0x
│ └── flutter_logo.png
│ ├── 3.0x
│ └── flutter_logo.png
│ └── void.svg
├── macos
├── Runner
│ ├── Configs
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ ├── Warnings.xcconfig
│ │ └── AppInfo.xcconfig
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ ├── app_icon_16.png
│ │ │ ├── app_icon_32.png
│ │ │ ├── app_icon_64.png
│ │ │ ├── app_icon_1024.png
│ │ │ ├── app_icon_128.png
│ │ │ ├── app_icon_256.png
│ │ │ ├── app_icon_512.png
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Release.entitlements
│ ├── DebugProfile.entitlements
│ ├── MainFlutterWindow.swift
│ ├── Info.plist
│ └── Base.lproj
│ │ └── MainMenu.xib
├── .gitignore
├── Flutter
│ ├── Flutter-Debug.xcconfig
│ ├── Flutter-Release.xcconfig
│ └── GeneratedPluginRegistrant.swift
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Runner.xcodeproj
│ ├── project.xcworkspace
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Podfile.lock
└── Podfile
├── l10n.yaml
├── lib
├── src
│ ├── localization
│ │ └── app_en.arb
│ ├── core
│ │ ├── utils
│ │ │ ├── extensions.dart
│ │ │ └── demo_hacks_helper.dart
│ │ ├── views
│ │ │ ├── widgets
│ │ │ │ └── circular_image.dart
│ │ │ └── pages
│ │ │ │ ├── splash_page.dart
│ │ │ │ └── home_page.dart
│ │ ├── models
│ │ │ ├── data
│ │ │ │ ├── resource.dart
│ │ │ │ ├── network_bound_resource.dart
│ │ │ │ └── operation.dart
│ │ │ ├── data_sources
│ │ │ │ └── data_source.dart
│ │ │ └── repositories
│ │ │ │ └── repository.dart
│ │ └── app.dart
│ ├── pokemon
│ │ ├── views
│ │ │ ├── widgets
│ │ │ │ ├── loading_indicator.dart
│ │ │ │ ├── actions_fabs_row.dart
│ │ │ │ ├── pokemon_card.dart
│ │ │ │ └── actions_menu_button.dart
│ │ │ ├── ui_states
│ │ │ │ ├── pokemon_ui_state.dart
│ │ │ │ └── pokemon_details_ui_state.dart
│ │ │ └── pages
│ │ │ │ ├── pokemon_details_page.dart
│ │ │ │ └── pokemon_page.dart
│ │ ├── models
│ │ │ ├── data
│ │ │ │ ├── fake_pokemon.dart
│ │ │ │ ├── pokemon.dart
│ │ │ │ ├── pokemon_api_model.g.dart
│ │ │ │ └── pokemon_api_model.dart
│ │ │ ├── data_sources
│ │ │ │ ├── pokemon_local_data_source.dart
│ │ │ │ └── pokemon_remote_data_source.dart
│ │ │ └── repositories
│ │ │ │ └── pokemon_repository.dart
│ │ └── controllers
│ │ │ ├── pokemon_details_controller.dart
│ │ │ └── pokemon_controller.dart
│ ├── network
│ │ └── utils
│ │ │ └── connectivity.dart
│ └── settings
│ │ ├── services
│ │ └── settings_service.dart
│ │ ├── controllers
│ │ └── settings_controller.dart
│ │ └── views
│ │ └── pages
│ │ └── settings_page.dart
└── main.dart
├── README.md
├── .metadata
├── .vscode
├── settings.json
└── launch.json
├── test
├── unit_test.dart
├── widget_test.dart
└── pokemon
│ ├── views
│ └── ui_states
│ │ └── pokemon_ui_state_test.dart
│ └── models
│ └── data_sources
│ └── pokemon_local_data_source_test.dart
├── .gitignore
├── pubspec.yaml
└── analysis_options.yaml
/.fvmrc:
--------------------------------------------------------------------------------
1 | {
2 | "flutter": "3.27.2"
3 | }
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/web/favicon.png
--------------------------------------------------------------------------------
/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/scripts/check_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | fvm dart pub outdated --no-prereleases --no-transitive
4 |
--------------------------------------------------------------------------------
/scripts/generate_models.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | fvm dart run build_runner build --delete-conflicting-outputs
4 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/assets/images/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/assets/images/flutter_logo.png
--------------------------------------------------------------------------------
/macos/Runner/Configs/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Debug.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/web/icons/Icon-maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/web/icons/Icon-maskable-192.png
--------------------------------------------------------------------------------
/web/icons/Icon-maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/web/icons/Icon-maskable-512.png
--------------------------------------------------------------------------------
/macos/Runner/Configs/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Release.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/assets/images/2.0x/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/assets/images/2.0x/flutter_logo.png
--------------------------------------------------------------------------------
/assets/images/3.0x/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/assets/images/3.0x/flutter_logo.png
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/src/localization
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 |
--------------------------------------------------------------------------------
/macos/.gitignore:
--------------------------------------------------------------------------------
1 | # Flutter-related
2 | **/Flutter/ephemeral/
3 | **/Pods/
4 |
5 | # Xcode-related
6 | **/dgph
7 | **/xcuserdata/
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/lib/src/localization/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "appTitle": "flutter_arch_comp",
3 | "@appTitle": {
4 | "description": "The title of the application"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Demo sample for Flutter Architecture Components
2 |
3 | Presented initially at Flutter Vikings 2022
4 |
5 | Uses the open Pokemon API https://pokeapi.co/
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/macos/Flutter/Flutter-Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/macos/Flutter/Flutter-Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/lib/src/core/utils/extensions.dart:
--------------------------------------------------------------------------------
1 | extension PlainString on List {
2 | String toPlainString() {
3 | return toString().replaceFirst('[', '').replaceFirst(']', '');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/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/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alesalv/flutter_arch_comp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/flutter_arch_comp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.flutter_arch_comp
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | import 'src/core/app.dart';
5 |
6 | void main() async {
7 | runApp(const ProviderScope(child: MyApp()));
8 | }
9 |
--------------------------------------------------------------------------------
/macos/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | @NSApplicationMain
5 | class AppDelegate: FlutterAppDelegate {
6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 | return true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
7 |
--------------------------------------------------------------------------------
/macos/Runner/Release.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/macos/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 |
--------------------------------------------------------------------------------
/macos/Runner.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/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 18116933e77adc82f80866c928266a5b4f1ed645
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/widgets/loading_indicator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class LoadingIndicator extends StatelessWidget {
4 | const LoadingIndicator({super.key});
5 |
6 | @override
7 | Widget build(BuildContext context) {
8 | return const LinearProgressIndicator(
9 | minHeight: 10,
10 | color: Colors.yellow,
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = '../build'
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | subprojects {
13 | project.evaluationDependsOn(':app')
14 | }
15 |
16 | tasks.register("clean", Delete) {
17 | delete rootProject.buildDir
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dart.flutterSdkPath": ".fvm/versions/3.27.1",
3 | "search.exclude": {
4 | "**/.fvm": true,
5 | "**/*.freezed.dart": true,
6 | "**/*.g.dart": true
7 | },
8 | "files.watcherExclude": {
9 | "**/.fvm": true,
10 | "**/*.freezed.dart": true,
11 | "**/*.g.dart": true
12 | },
13 | "files.exclude": {
14 | "**/*.freezed.dart": true,
15 | "**/*.g.dart": true
16 | },
17 | }
--------------------------------------------------------------------------------
/macos/Runner/DebugProfile.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.network.server
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/unit_test.dart:
--------------------------------------------------------------------------------
1 | // This is an example unit test.
2 | //
3 | // A unit test tests a single function, method, or class. To learn more about
4 | // writing unit tests, visit
5 | // https://flutter.dev/docs/cookbook/testing/unit/introduction
6 |
7 | import 'package:flutter_test/flutter_test.dart';
8 |
9 | void main() {
10 | group('Plus Operator', () {
11 | test('should add two numbers together', () {
12 | expect(1 + 1, 2);
13 | });
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/macos/Runner/MainFlutterWindow.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | class MainFlutterWindow: NSWindow {
5 | override func awakeFromNib() {
6 | let flutterViewController = FlutterViewController.init()
7 | let windowFrame = self.frame
8 | self.contentViewController = flutterViewController
9 | self.setFrame(windowFrame, display: true)
10 |
11 | RegisterGeneratedPlugins(registry: flutterViewController)
12 |
13 | super.awakeFromNib()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/macos/Flutter/GeneratedPluginRegistrant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | import FlutterMacOS
6 | import Foundation
7 |
8 | import path_provider_foundation
9 | import shared_preferences_foundation
10 | import sqflite_darwin
11 |
12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
13 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
14 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
15 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data/fake_pokemon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon_api_model.dart';
2 |
3 | const kFakePokemon = [
4 | PokemonApiModel(
5 | id: 1,
6 | name: 'bulbasaur',
7 | height: 7,
8 | weight: 69,
9 | image:
10 | 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png',
11 | ),
12 | PokemonApiModel(
13 | id: 2,
14 | name: 'ivysaur',
15 | height: 10,
16 | weight: 130,
17 | image:
18 | 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png',
19 | ),
20 | ];
21 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
2 | GCC_WARN_UNDECLARED_SELECTOR = YES
3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
6 | CLANG_WARN_PRAGMA_PACK = YES
7 | CLANG_WARN_STRICT_PROTOTYPES = YES
8 | CLANG_WARN_COMMA = YES
9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES
10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
12 | GCC_WARN_SHADOW = YES
13 | CLANG_WARN_UNREACHABLE_CODE = YES
14 |
--------------------------------------------------------------------------------
/macos/Runner/Configs/AppInfo.xcconfig:
--------------------------------------------------------------------------------
1 | // Application-level settings for the Runner target.
2 | //
3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
4 | // future. If not, the values below would default to using the project name when this becomes a
5 | // 'flutter create' template.
6 |
7 | // The application's name. By default this is also the title of the Flutter window.
8 | PRODUCT_NAME = flutter_arch_comp
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterArchComp
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved.
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/src/network/utils/connectivity.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/foundation.dart';
4 |
5 | /// Connectivity represents a singleton helper for retrieving connectivity
6 | /// from the internet
7 | class Connectivity {
8 | static final instance = Connectivity._internal();
9 |
10 | // internal constructor
11 | Connectivity._internal();
12 |
13 | /// Returns true if connected, false otherwise
14 | Future isConnected() async {
15 | try {
16 | final result = await InternetAddress.lookup('google.com');
17 | return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
18 | } on SocketException catch (e) {
19 | debugPrint('Unable to read connectivity from internet, $e');
20 | return false;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "flutter_arch_comp",
9 | "request": "launch",
10 | "type": "dart"
11 | },
12 | {
13 | "name": "flutter_arch_comp (profile mode)",
14 | "request": "launch",
15 | "type": "dart",
16 | "flutterMode": "profile"
17 | },
18 | {
19 | "name": "flutter_arch_comp (release mode)",
20 | "request": "launch",
21 | "type": "dart",
22 | "flutterMode": "release"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "8.3.2" apply false
22 | // note: kotlin's version must match the one defined in android/app/build.gradle
23 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false
24 | }
25 |
26 | include ":app"
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | **/ios/Flutter/.last_build_id
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Ignore fvm folder
35 |
36 | # Web related
37 | lib/generated_plugin_registrant.dart
38 |
39 | # Symbolication related
40 | app.*.symbols
41 |
42 | # Obfuscation related
43 | app.*.map.json
44 |
45 | # Android Studio will place build artifacts here
46 | /android/app/debug
47 | /android/app/profile
48 | /android/app/release
49 |
50 | # FVM Version Cache
51 | .fvm/
--------------------------------------------------------------------------------
/lib/src/settings/services/settings_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | /// A service that stores and retrieves user settings.
5 | ///
6 | /// By default, this class does not persist user settings. If you'd like to
7 | /// persist the user settings locally, use the shared_preferences package. If
8 | /// you'd like to store settings on a web server, use the http package.
9 | class SettingsService {
10 | /// Loads the User's preferred ThemeMode from local or remote storage.
11 | Future themeMode() async => ThemeMode.system;
12 |
13 | /// Persists the user's preferred ThemeMode to local or remote storage.
14 | Future updateThemeMode(ThemeMode theme) async {
15 | // Use the shared_preferences package to persist settings locally or the
16 | // http package to persist settings over the network.
17 | }
18 | }
19 |
20 | /// settingsServiceProvider provides the settings service
21 | final settingsServiceProvider = Provider((_) => SettingsService());
22 |
--------------------------------------------------------------------------------
/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flutter_arch_comp",
3 | "short_name": "flutter_arch_comp",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is an example Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 | //
8 | // Visit https://flutter.dev/docs/cookbook/testing/widget/introduction for
9 | // more information about Widget testing.
10 |
11 | import 'package:flutter/material.dart';
12 | import 'package:flutter_test/flutter_test.dart';
13 |
14 | void main() {
15 | group('MyWidget', () {
16 | testWidgets('should display a string of text', (WidgetTester tester) async {
17 | // Define a Widget
18 | const myWidget = MaterialApp(
19 | home: Scaffold(
20 | body: Text('Hello'),
21 | ),
22 | );
23 |
24 | // Build myWidget and trigger a frame.
25 | await tester.pumpWidget(myWidget);
26 |
27 | // Verify myWidget shows some text
28 | expect(find.byType(Text), findsOneWidget);
29 | });
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/macos/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | $(PRODUCT_COPYRIGHT)
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/src/core/views/widgets/circular_image.dart:
--------------------------------------------------------------------------------
1 | import 'package:cached_network_image/cached_network_image.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | /// CircularImage represents an image rounded by a circular border
5 | class CircularImage extends StatelessWidget {
6 | const CircularImage({required this.imageUrl, required this.size, super.key});
7 | final String imageUrl;
8 | final double size;
9 |
10 | static const _flutterLogo = CircleAvatar(
11 | foregroundImage: AssetImage('assets/images/flutter_logo.png'));
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Container(
16 | width: size,
17 | height: size,
18 | decoration: BoxDecoration(
19 | borderRadius: BorderRadius.all(Radius.circular(size)),
20 | border: Border.all(
21 | color: Colors.lightBlueAccent,
22 | width: size < 100.0 ? 2.0 : 4.0,
23 | ),
24 | ),
25 | child: ClipOval(
26 | child: CachedNetworkImage(
27 | fit: BoxFit.fill,
28 | imageUrl: imageUrl,
29 | width: size,
30 | height: size,
31 | placeholder: (context, url) => _flutterLogo,
32 | errorWidget: (context, url, error) => _flutterLogo,
33 | ),
34 | ),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/core/views/pages/splash_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_arch_comp/src/core/views/pages/home_page.dart';
3 | import 'package:flutter_riverpod/flutter_riverpod.dart';
4 |
5 | import '../../../settings/controllers/settings_controller.dart';
6 |
7 | class SplashPage extends ConsumerStatefulWidget {
8 | const SplashPage({super.key});
9 |
10 | static const routeName = '/';
11 |
12 | @override
13 | ConsumerState createState() => _SplashPageState();
14 | }
15 |
16 | class _SplashPageState extends ConsumerState {
17 | @override
18 | void initState() {
19 | super.initState();
20 | WidgetsBinding.instance
21 | .addPostFrameCallback((final _) => _afterSplash(context));
22 | }
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return const Material(
27 | child: Center(
28 | child: Text('Flutter Architecture Components'),
29 | ),
30 | );
31 | }
32 |
33 | Future _afterSplash(BuildContext context) async {
34 | // initialize the settings controller
35 | await ref.read(settingsControllerProvider).loadSettings();
36 | if (!context.mounted) {
37 | return;
38 | }
39 | Navigator.restorablePushNamed(context, HomePage.routeName);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/src/core/models/data/resource.dart:
--------------------------------------------------------------------------------
1 | /// Lifecycle of a resource:
2 | /// Resource.loading() retrieving started
3 | /// Resource.loading(localData) retrieving continues, local data
4 | /// Resource.success(remoteData) retrieving successful, remote data
5 | /// Resource.error('msg') retrieving unsuccessful, no data
6 | /// Resource.error('msg', localData) retrieving successful, local data
7 | ///
8 | /// Inspired by https://github.com/android/architecture-components-samples/blob/88747993139224a4bb6dbe985adf652d557de621/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
9 | class Resource {
10 | /// Use static methods to initialize this class
11 | Resource(this.status, {this.data, this.msg = ''});
12 |
13 | final Status status;
14 | final T? data;
15 | final String msg;
16 |
17 | static Resource loading({T? data}) =>
18 | Resource(Status.loading, data: data);
19 |
20 | static Resource success(T data) => Resource(Status.success, data: data);
21 |
22 | static Resource error(String msg, {T? data}) =>
23 | Resource(Status.error, data: data, msg: msg);
24 | }
25 |
26 | // TODO(alesalv): rename this to ResourceStatus
27 | // Status represents one of [loading, success, error]
28 | enum Status {
29 | loading,
30 | success,
31 | error,
32 | }
33 |
--------------------------------------------------------------------------------
/lib/src/core/models/data_sources/data_source.dart:
--------------------------------------------------------------------------------
1 | /// DataSource represents a data source for type [T]. Each data source
2 | /// supports one-shot CRUD operations on the data [T]. For more information
3 | /// see https://developer.android.com/jetpack/guide/data-layer
4 | abstract class DataSource {
5 | /// Creates the given data into this data source, replacing it if
6 | /// it exists already. It throws an [Exception] in case of any error
7 | Future create(T data);
8 |
9 | /// Creates all the given data into this data source, replacing them if
10 | /// it exists already. It throws an [Exception] in case of any error
11 | Future createAll(List data);
12 |
13 | /// Returns the data specified by the given id if any, null otherwise. It
14 | /// throws an [Exception] in case of any error
15 | Future read(int id);
16 |
17 | /// Returns all the data if any, an empty list otherwise. It throws an
18 | /// [Exception] in case of any error
19 | Future> readAll();
20 |
21 | /// Updates the given data into this data source. It throws an [Exception]
22 | /// in case of any error
23 | Future update(T data);
24 |
25 | /// Deletes the data specified by the given id if any. It throws an
26 | /// [Exception] in case of any error
27 | Future delete(int id);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/widgets/actions_fabs_row.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | import '../../controllers/pokemon_controller.dart';
5 | import '../../models/data/pokemon.dart';
6 |
7 | class ActionsFabsRow extends ConsumerWidget {
8 | const ActionsFabsRow({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context, WidgetRef ref) {
12 | return Row(
13 | mainAxisAlignment: MainAxisAlignment.end,
14 | children: [
15 | FloatingActionButton(
16 | heroTag: "buttonCreate",
17 | child: const Icon(Icons.add),
18 | onPressed: () =>
19 | ref.read(pokemonControllerProvider).create(const Pokemon())),
20 | const SizedBox(width: 16),
21 | FloatingActionButton(
22 | heroTag: "buttonDelete",
23 | child: const Icon(Icons.remove),
24 | onPressed: () =>
25 | ref.read(pokemonControllerProvider).delete(const Pokemon().id)),
26 | const SizedBox(width: 16),
27 | FloatingActionButton(
28 | heroTag: "buttonRefresh",
29 | child: const Icon(Icons.refresh),
30 | onPressed: () => ref.read(pokemonControllerProvider).refresh()),
31 | ],
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Flutter (1.0.0)
3 | - path_provider_foundation (0.0.1):
4 | - Flutter
5 | - FlutterMacOS
6 | - shared_preferences_foundation (0.0.1):
7 | - Flutter
8 | - FlutterMacOS
9 | - sqflite_darwin (0.0.4):
10 | - Flutter
11 | - FlutterMacOS
12 |
13 | DEPENDENCIES:
14 | - Flutter (from `Flutter`)
15 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
16 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
17 | - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
18 |
19 | EXTERNAL SOURCES:
20 | Flutter:
21 | :path: Flutter
22 | path_provider_foundation:
23 | :path: ".symlinks/plugins/path_provider_foundation/darwin"
24 | shared_preferences_foundation:
25 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
26 | sqflite_darwin:
27 | :path: ".symlinks/plugins/sqflite_darwin/darwin"
28 |
29 | SPEC CHECKSUMS:
30 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
31 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
32 | shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
33 | sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
34 |
35 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
36 |
37 | COCOAPODS: 1.16.2
38 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutter_arch_comp
2 | description: Demo sample for Flutter Architecture Components
3 |
4 | # Prevent accidental publishing to pub.dev.
5 | publish_to: 'none'
6 |
7 | version: 1.0.0+1
8 |
9 | environment:
10 | sdk: '>=3.0.0 <4.0.0'
11 |
12 | dependencies:
13 | flutter:
14 | sdk: flutter
15 | flutter_localizations:
16 | sdk: flutter
17 |
18 | # https://pub.dev/packages/cached_network_image
19 | cached_network_image: ^3.4.1
20 | # https://pub.dev/packages/flutter_riverpod
21 | flutter_riverpod: ^2.6.1
22 | # https://pub.dev/packages/flutter_svg
23 | flutter_svg: ^2.0.17
24 | # https://pub.dev/packages/http
25 | http: ^1.2.2
26 | # https://pub.dev/packages/json_annotation
27 | json_annotation: ^4.9.0
28 | # https://pub.dev/packages/shared_preferences
29 | shared_preferences: ^2.3.5
30 |
31 | dev_dependencies:
32 | flutter_test:
33 | sdk: flutter
34 | # https://pub.dev/packages/build_runner
35 | build_runner: ^2.4.14
36 | # https://pub.dev/packages/flutter_lints
37 | flutter_lints: ^3.0.2
38 | # https://pub.dev/packages/json_serializable
39 | json_serializable: ^6.9.3
40 |
41 | flutter:
42 | uses-material-design: true
43 |
44 | # Enable generation of localized Strings from arb files.
45 | generate: true
46 |
47 | assets:
48 | # Add assets from the images directory to the application.
49 | - assets/images/
50 |
--------------------------------------------------------------------------------
/macos/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - FlutterMacOS (1.0.0)
3 | - path_provider_foundation (0.0.1):
4 | - Flutter
5 | - FlutterMacOS
6 | - shared_preferences_foundation (0.0.1):
7 | - Flutter
8 | - FlutterMacOS
9 | - sqflite (0.0.3):
10 | - Flutter
11 | - FlutterMacOS
12 |
13 | DEPENDENCIES:
14 | - FlutterMacOS (from `Flutter/ephemeral`)
15 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
16 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
17 | - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
18 |
19 | EXTERNAL SOURCES:
20 | FlutterMacOS:
21 | :path: Flutter/ephemeral
22 | path_provider_foundation:
23 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
24 | shared_preferences_foundation:
25 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
26 | sqflite:
27 | :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
28 |
29 | SPEC CHECKSUMS:
30 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
31 | path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
32 | shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
33 | sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
34 |
35 | PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
36 |
37 | COCOAPODS: 1.15.2
38 |
--------------------------------------------------------------------------------
/lib/src/core/views/pages/home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../../pokemon/views/pages/pokemon_page.dart';
4 | import '../../../settings/views/pages/settings_page.dart';
5 |
6 | class HomePage extends StatelessWidget {
7 | const HomePage({super.key});
8 |
9 | static const routeName = 'home';
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Scaffold(
14 | appBar: AppBar(
15 | automaticallyImplyLeading: false,
16 | title: const Text('Flutter Arch Comp'),
17 | actions: [
18 | IconButton(
19 | icon: const Icon(Icons.settings),
20 | onPressed: () {
21 | // Navigate to the settings page. If the user leaves and returns
22 | // to the app after it has been killed while running in the
23 | // background, the navigation stack is restored.
24 | Navigator.restorablePushNamed(context, SettingsPage.routeName);
25 | },
26 | ),
27 | ],
28 | ),
29 | body: Center(
30 | child: Column(
31 | mainAxisAlignment: MainAxisAlignment.center,
32 | children: [
33 | ElevatedButton(
34 | onPressed: () => Navigator.restorablePushNamed(
35 | context, PokemonPage.routeName),
36 | child: const Text('Catch \'em all')),
37 | ],
38 | ),
39 | ),
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/macos/Podfile:
--------------------------------------------------------------------------------
1 | platform :osx, '10.14'
2 |
3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5 |
6 | project 'Runner', {
7 | 'Debug' => :debug,
8 | 'Profile' => :release,
9 | 'Release' => :release,
10 | }
11 |
12 | def flutter_root
13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14 | unless File.exist?(generated_xcode_build_settings_path)
15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16 | end
17 |
18 | File.foreach(generated_xcode_build_settings_path) do |line|
19 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
20 | return matches[1].strip if matches
21 | end
22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23 | end
24 |
25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26 |
27 | flutter_macos_podfile_setup
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 | use_modular_headers!
32 |
33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
34 | end
35 |
36 | post_install do |installer|
37 | installer.pods_project.targets.each do |target|
38 | flutter_additional_macos_build_settings(target)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/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 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/src/core/models/data/network_bound_resource.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_arch_comp/src/core/models/data/resource.dart';
2 |
3 | abstract class NetworkBoundResource {
4 | Future> loadFromDb();
5 |
6 | Future persistToDb(T data);
7 |
8 | Future> loadFromServer();
9 |
10 | bool shouldFetch(Resource resource);
11 |
12 | void setValue(Resource resource);
13 |
14 | Future retrieveAll() async {
15 | /// loading
16 | setValue(Resource.loading());
17 | final localResource = await loadFromDb();
18 |
19 | /// local resource is either 'success' or 'error'
20 | /// fetches from network in case local is outdated, or 'error'
21 | if (shouldFetch(localResource)) {
22 | _fetchFromNetwork(localResource);
23 | } else {
24 | /// success
25 | setValue(Resource.success(localResource.data as T));
26 | }
27 | }
28 |
29 | Future _fetchFromNetwork(Resource localResource) async {
30 | if (Status.success == localResource.status) {
31 | /// loading with data
32 | setValue(Resource.loading(data: localResource.data));
33 | }
34 |
35 | final remoteResource = await loadFromServer();
36 |
37 | if (Status.success == remoteResource.status) {
38 | await persistToDb(remoteResource.data as T);
39 | final persisted = await loadFromDb();
40 |
41 | /// success
42 | setValue(Resource.success(persisted.data as T));
43 | } else {
44 | /// error
45 | setValue(Resource.error(remoteResource.msg,
46 | data: remoteResource.data ?? localResource.data));
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "app_icon_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "app_icon_32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "app_icon_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "app_icon_64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "app_icon_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "app_icon_256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "app_icon_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "app_icon_512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "app_icon_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "app_icon_1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/src/core/models/data/operation.dart:
--------------------------------------------------------------------------------
1 | /// Lifecycle of an operation:
2 | /// Operation.loading() retrieving started
3 | /// Operation.loading(localData) retrieving continues, local data
4 | /// Operation.success(remoteData) retrieving successful, remote data
5 | /// Operation.error('msg') retrieving unsuccessful, no data
6 | /// Operation.error('msg', localData) retrieving successful, local data
7 | ///
8 | /// Inspired by https://github.com/android/architecture-components-samples/blob/88747993139224a4bb6dbe985adf652d557de621/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
9 | class Operation {
10 | /// Use static methods to initialize this class
11 | Operation(this.status, {this.data, this.msg = ''});
12 |
13 | final OperationStatus status;
14 | final T? data;
15 | final String msg;
16 |
17 | bool get isLoading => status == OperationStatus.loading;
18 | bool get isSuccess => status == OperationStatus.success;
19 | bool get isError => status == OperationStatus.error;
20 |
21 | // TODO(alesalv): no need to pass data during loading
22 | static Operation loading({T? data}) =>
23 | Operation(OperationStatus.loading, data: data);
24 |
25 | static Operation success(T data) =>
26 | Operation(OperationStatus.success, data: data);
27 |
28 | static Operation error(String msg, {T? data}) =>
29 | Operation(OperationStatus.error, data: data, msg: msg);
30 | }
31 |
32 | // TODO(alesalv): make OperationStatus private
33 |
34 | // OperationStatus represents one of [loading, success, error]
35 | enum OperationStatus {
36 | loading,
37 | success,
38 | error,
39 | }
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/src/core/models/repositories/repository.dart:
--------------------------------------------------------------------------------
1 | /// Repository represents a repository for type [T]. Each repository supports
2 | /// one-shot CRUD operations on the data [T], and methods be notified of data
3 | /// changes over time. For more information see
4 | /// https://developer.android.com/jetpack/guide/data-layer
5 | abstract class Repository {
6 | /// Watches this repository for changes in the data specified by the given
7 | /// id, if any. Changes are triggered by actions like create, update, delete,
8 | /// and refresh
9 | Stream watch(int id);
10 |
11 | /// Watches this repository for changes in the list of data. Changes are
12 | /// triggered by actions like create, update, delete, and refresh
13 | Stream> watchAll();
14 |
15 | /// Creates the given data into this repository. It throws an [Exception]
16 | /// in case of any error
17 | Future create(T data);
18 |
19 | /// Returns the data specified by the given id if any, null otherwise. It
20 | /// throws an [Exception] in case of any error
21 | Future read(int id);
22 |
23 | /// Returns all the data if any, an empty list otherwise. It throws an
24 | /// [Exception] in case of any error
25 | Future> readAll();
26 |
27 | /// Updates the given data into this repository. It throws an [Exception]
28 | /// in case of any error
29 | Future update(T data);
30 |
31 | /// Deletes the data specified by the given id if any. It throws an
32 | /// [Exception] in case of any error
33 | Future delete(int id);
34 |
35 | /// Refreshes the actual data with the most up to date data. It throws an
36 | /// [Exception] in case of any error
37 | Future refresh();
38 |
39 | /// Disposes this repository
40 | void dispose();
41 | }
42 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | flutter_arch_comp
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 | CADisableMinimumFrameDurationOnPhone
45 |
46 | UIApplicationSupportsIndirectInputEvents
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/widgets/pokemon_card.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../../core/views/widgets/circular_image.dart';
4 | import '../pages/pokemon_details_page.dart';
5 | import '../ui_states/pokemon_ui_state.dart';
6 |
7 | /// PokemonCard represents a card to show some pokemon information, like an
8 | /// icon, the name, its weight and height
9 | class PokemonCard extends StatelessWidget {
10 | const PokemonCard(this.pokemon, {super.key});
11 |
12 | final PokemonItemUiState pokemon;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return Card(
17 | child: Padding(
18 | padding: const EdgeInsets.all(2),
19 | child: ListTile(
20 | leading: CircularImage(imageUrl: pokemon.image, size: 55),
21 | title: Text(
22 | pokemon.name,
23 | style: const TextStyle(fontWeight: FontWeight.bold),
24 | ),
25 | subtitle: Padding(
26 | padding: const EdgeInsets.only(top: 8),
27 | child: Row(
28 | children: [
29 | Text('ID: ${pokemon.id}'),
30 | const SizedBox(
31 | width: 16,
32 | ),
33 | Text('Order: ${pokemon.order}'),
34 | ],
35 | ),
36 | ),
37 | onTap: () {
38 | // Navigate to the details page. If the user leaves and returns to
39 | // the app after it has been killed while running in the
40 | // background, the navigation stack is restored.
41 | Navigator.pushNamed(
42 | context,
43 | PokemonDetailsPage.routeName,
44 | arguments: PokemonDetailsViewArgs(pokemon.id),
45 | );
46 | }),
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "dev.flutter.flutter-gradle-plugin"
5 | }
6 |
7 | def localProperties = new Properties()
8 | def localPropertiesFile = rootProject.file('local.properties')
9 | if (localPropertiesFile.exists()) {
10 | localPropertiesFile.withReader('UTF-8') { reader ->
11 | localProperties.load(reader)
12 | }
13 | }
14 |
15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
16 | if (flutterVersionCode == null) {
17 | flutterVersionCode = '1'
18 | }
19 |
20 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
21 | if (flutterVersionName == null) {
22 | flutterVersionName = '1.0'
23 | }
24 |
25 | android {
26 | namespace "com.example.flutter_arch_comp"
27 | compileSdkVersion 34
28 | ndkVersion "27.0.12077973"
29 |
30 | compileOptions {
31 | // Sets Java compatibility to Java 21
32 | sourceCompatibility JavaVersion.VERSION_21
33 | targetCompatibility JavaVersion.VERSION_21
34 | }
35 |
36 | kotlinOptions {
37 | jvmTarget = '21'
38 | }
39 |
40 | sourceSets {
41 | main.java.srcDirs += 'src/main/kotlin'
42 | }
43 |
44 | defaultConfig {
45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
46 | applicationId "com.example.flutter_arch_comp"
47 | minSdkVersion flutter.minSdkVersion
48 | targetSdkVersion 32
49 | versionCode flutterVersionCode.toInteger()
50 | versionName flutterVersionName
51 | }
52 |
53 | buildTypes {
54 | release {
55 | // TODO: Add your own signing config for the release build.
56 | // Signing with the debug keys for now, so `flutter run --release` works.
57 | signingConfig signingConfigs.debug
58 | }
59 | }
60 | }
61 |
62 | flutter {
63 | source '../..'
64 | }
65 |
66 | dependencies {
67 | // note: version after : must match kotlin's version defined in android/settings.gradle
68 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.20"
69 | }
70 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/widgets/actions_menu_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_arch_comp/src/core/utils/demo_hacks_helper.dart';
3 | import 'package:flutter_arch_comp/src/pokemon/models/repositories/pokemon_repository.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 |
6 | class ActionsMenuButton extends ConsumerWidget {
7 | const ActionsMenuButton({super.key});
8 |
9 | @override
10 | Widget build(BuildContext context, WidgetRef ref) {
11 | return PopupMenuButton<_MenuActions>(
12 | onSelected: (_MenuActions item) async {
13 | // this mocks an event which could happen via firebase, or from
14 | // a background process, or even from another UI, so the
15 | // refresh call is made directly via the repository and not via
16 | // the controller
17 | switch (item) {
18 | case _MenuActions.create:
19 | final next =
20 | await DemoHacksHelper.instance.nextPokemonFromRemote();
21 | if (next != null) {
22 | ref.read(pokemonRepositoryProvider).create(next);
23 | }
24 | break;
25 | case _MenuActions.delete:
26 | final first =
27 | await DemoHacksHelper.instance.firstPokemonFromLocal();
28 | if (first != null) {
29 | ref.read(pokemonRepositoryProvider).delete(first.id);
30 | }
31 | break;
32 | case _MenuActions.refresh:
33 | ref.read(pokemonRepositoryProvider).refresh();
34 | break;
35 | }
36 | },
37 | itemBuilder: (BuildContext context) => >[
38 | const PopupMenuItem<_MenuActions>(
39 | value: _MenuActions.create,
40 | child: Text('Create'),
41 | ),
42 | const PopupMenuItem<_MenuActions>(
43 | value: _MenuActions.delete,
44 | child: Text('Delete'),
45 | ),
46 | const PopupMenuItem<_MenuActions>(
47 | value: _MenuActions.refresh,
48 | child: Text('Refresh'),
49 | ),
50 | ]);
51 | }
52 | }
53 |
54 | enum _MenuActions { create, delete, refresh }
55 |
--------------------------------------------------------------------------------
/lib/src/settings/controllers/settings_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | import '../services/settings_service.dart';
5 |
6 | /// A class that many Widgets can interact with to read user settings, update
7 | /// user settings, or listen to user settings changes.
8 | ///
9 | /// Controllers glue Data Services to Flutter Widgets. The SettingsController
10 | /// uses the SettingsService to store and retrieve user settings.
11 | class SettingsController with ChangeNotifier {
12 | SettingsController(this._settingsService);
13 |
14 | // Make SettingsService a private variable so it is not used directly.
15 | final SettingsService _settingsService;
16 |
17 | // Make ThemeMode a private variable so it is not updated directly without
18 | // also persisting the changes with the SettingsService.
19 | ThemeMode _themeMode = ThemeMode.system;
20 |
21 | // Allow Widgets to read the user's preferred ThemeMode.
22 | ThemeMode get themeMode => _themeMode;
23 |
24 | /// Load the user's settings from the SettingsService. It may load from a
25 | /// local database or the internet. The controller only knows it can load the
26 | /// settings from the service.
27 | Future loadSettings() async {
28 | _themeMode = await _settingsService.themeMode();
29 |
30 | // Important! Inform listeners a change has occurred.
31 | notifyListeners();
32 | }
33 |
34 | /// Update and persist the ThemeMode based on the user's selection.
35 | Future updateThemeMode(ThemeMode? newThemeMode) async {
36 | if (newThemeMode == null) return;
37 |
38 | // Dot not perform any work if new and old ThemeMode are identical
39 | if (newThemeMode == _themeMode) return;
40 |
41 | // Otherwise, store the new theme mode in memory
42 | _themeMode = newThemeMode;
43 |
44 | // Important! Inform listeners a change has occurred.
45 | notifyListeners();
46 |
47 | // Persist the changes to a local database or the internet using the
48 | // SettingService.
49 | await _settingsService.updateThemeMode(newThemeMode);
50 | }
51 | }
52 |
53 | /// settingsControllerProvider provides the settings controller
54 | final settingsControllerProvider = ChangeNotifierProvider((ref) {
55 | final settingsService = ref.read(settingsServiceProvider);
56 | return SettingsController(settingsService);
57 | });
58 |
--------------------------------------------------------------------------------
/test/pokemon/views/ui_states/pokemon_ui_state_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_arch_comp/src/pokemon/models/data/fake_pokemon.dart';
2 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon.dart';
3 | import 'package:flutter_arch_comp/src/pokemon/views/ui_states/pokemon_ui_state.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 |
6 | void main() {
7 | group('PokemonUiState immutability', () {
8 | test('should not allow to remove pokemon directly', () {
9 | final mutableList = _getFakePokemonList();
10 |
11 | final pokemonUiState = PokemonUiState(pokemon: mutableList);
12 |
13 | expect(mutableList.length, 2);
14 | expect(pokemonUiState.pokemon.length, 2);
15 |
16 | expect(() => pokemonUiState.pokemon.removeAt(0), throwsUnsupportedError);
17 | });
18 |
19 | test('should not change when mutable pokemon list is changed', () {
20 | final mutableList = _getFakePokemonList();
21 |
22 | final pokemonUiState = PokemonUiState(pokemon: mutableList);
23 |
24 | expect(mutableList.length, 2);
25 | expect(pokemonUiState.pokemon.length, 2);
26 |
27 | mutableList.removeAt(0);
28 |
29 | expect(mutableList.length, 1);
30 | expect(pokemonUiState.pokemon.length, 2);
31 | });
32 |
33 | test('should not allow to add pokemon directly', () {
34 | final pokemonUiState = PokemonUiState(pokemon: const []);
35 |
36 | expect(pokemonUiState.pokemon.length, 0);
37 | expect(() => pokemonUiState.pokemon.add(const PokemonItemUiState()),
38 | throwsUnsupportedError);
39 | });
40 |
41 | test('should not allow to modify isFetchingPokemon directly', () {
42 | final pokemonUiState = PokemonUiState(isFetchingPokemon: true);
43 |
44 | expect(pokemonUiState.isFetchingPokemon, true);
45 | // This is not even a proper test, but it's left here to show that
46 | // commenting out the following line doesn't compile, so it's not
47 | // possible to mutate any primitive in any class marked as @immutable
48 | // pokemonUiState.isFetchingPokemon = false;
49 | expect(true, true);
50 | });
51 | });
52 | }
53 |
54 | _getFakePokemonList() {
55 | return [
56 | PokemonItemUiState.fromPokemon(
57 | Pokemon.fromPokemonApiModel(kFakePokemon[0])),
58 | PokemonItemUiState.fromPokemon(
59 | Pokemon.fromPokemonApiModel(kFakePokemon[1])),
60 | ];
61 | }
62 |
--------------------------------------------------------------------------------
/lib/src/settings/views/pages/settings_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | import '../../../pokemon/controllers/pokemon_controller.dart';
5 | import '../../controllers/settings_controller.dart';
6 |
7 | /// Displays the various settings that can be customized by the user.
8 | ///
9 | /// When a user changes a setting, the SettingsController is updated and
10 | /// Widgets that listen to the SettingsController are rebuilt.
11 | class SettingsPage extends ConsumerWidget {
12 | const SettingsPage({super.key});
13 |
14 | static const routeName = 'settings';
15 |
16 | @override
17 | Widget build(BuildContext context, WidgetRef ref) {
18 | final controller = ref.watch(settingsControllerProvider);
19 |
20 | return Scaffold(
21 | appBar: AppBar(
22 | title: const Text('Settings'),
23 | ),
24 | body: Padding(
25 | padding: const EdgeInsets.all(16),
26 | // Glue the SettingsController to the theme selection DropdownButton.
27 | //
28 | // When a user selects a theme from the dropdown list, the
29 | // SettingsController is updated, which rebuilds the MaterialApp.
30 | child: Column(
31 | children: [
32 | DropdownButton(
33 | // Read the selected themeMode from the controller
34 | value: controller.themeMode,
35 | // Call the updateThemeMode method any time the user selects a theme.
36 | onChanged: controller.updateThemeMode,
37 | items: const [
38 | DropdownMenuItem(
39 | value: ThemeMode.system,
40 | child: Text('System Theme'),
41 | ),
42 | DropdownMenuItem(
43 | value: ThemeMode.light,
44 | child: Text('Light Theme'),
45 | ),
46 | DropdownMenuItem(
47 | value: ThemeMode.dark,
48 | child: Text('Dark Theme'),
49 | )
50 | ],
51 | ),
52 | ElevatedButton(
53 | onPressed: () => _resetLocal(ref), child: const Text('RESET')),
54 | ],
55 | ),
56 | ),
57 | );
58 | }
59 |
60 | void _resetLocal(WidgetRef ref) {
61 | ref.read(pokemonControllerProvider.notifier).resetLocal();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
14 |
18 |
22 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/ui_states/pokemon_ui_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | import '../../models/data/pokemon.dart';
4 |
5 | /// PokemonUiState represents the UI state for the pokemon page
6 | @immutable
7 | class PokemonUiState {
8 | PokemonUiState({
9 | pokemon,
10 | this.isFetchingPokemon = false,
11 | this.errorMsg = '',
12 | }) : pokemon = (pokemon == null) ? const [] : List.unmodifiable(pokemon);
13 | final List pokemon;
14 | final bool isFetchingPokemon;
15 | final String errorMsg;
16 |
17 | PokemonUiState copy({
18 | List? pokemon,
19 | bool? isFetchingPokemon,
20 | String? errorMsg,
21 | }) {
22 | return PokemonUiState(
23 | pokemon: pokemon ?? this.pokemon,
24 | isFetchingPokemon: isFetchingPokemon ?? this.isFetchingPokemon,
25 | errorMsg: errorMsg ?? this.errorMsg,
26 | );
27 | }
28 |
29 | @override
30 | bool operator ==(Object other) =>
31 | identical(this, other) ||
32 | other is PokemonUiState &&
33 | runtimeType == other.runtimeType &&
34 | pokemon == other.pokemon &&
35 | isFetchingPokemon == other.isFetchingPokemon &&
36 | errorMsg == other.errorMsg;
37 |
38 | @override
39 | int get hashCode =>
40 | pokemon.hashCode ^ isFetchingPokemon.hashCode ^ errorMsg.hashCode;
41 | }
42 |
43 | /// PokemonItemUiState represents the UI state for an item of the pokemon page
44 | @immutable
45 | class PokemonItemUiState {
46 | const PokemonItemUiState({
47 | this.id = '',
48 | this.name = '',
49 | this.image = '',
50 | this.order = '',
51 | });
52 |
53 | final String id;
54 | final String name;
55 | final String order;
56 | final String image;
57 |
58 | factory PokemonItemUiState.fromPokemon(Pokemon pokemon) => PokemonItemUiState(
59 | id: pokemon.id.toString(),
60 | name: pokemon.name,
61 | image: pokemon.image,
62 | order: pokemon.order.toString(),
63 | );
64 |
65 | @override
66 | bool operator ==(Object other) =>
67 | identical(this, other) ||
68 | other is PokemonItemUiState &&
69 | runtimeType == other.runtimeType &&
70 | id == other.id &&
71 | name == other.name &&
72 | order == other.order &&
73 | image == other.image;
74 |
75 | @override
76 | int get hashCode =>
77 | id.hashCode ^ name.hashCode ^ order.hashCode ^ image.hashCode;
78 | }
79 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/src/core/utils/demo_hacks_helper.dart:
--------------------------------------------------------------------------------
1 | import '../../pokemon/models/data/pokemon.dart';
2 | import '../../pokemon/models/data/pokemon_api_model.dart';
3 | import '../models/data_sources/data_source.dart';
4 |
5 | /// DemoHacksHelper represents a singleton helper for adding few demo hacks to
6 | /// the code, so that for instance every time I refresh I'm able to retrieve 1
7 | /// pokemon more than the ones I have locally; this mocks the behaviour of
8 | /// the data model being shared between two instances of the app on two
9 | /// devices, or alternatively the app being a pokedex shared between two
10 | /// players. Everything inside this class can be considered a demo hack
11 | class DemoHacksHelper {
12 | static final instance = DemoHacksHelper._internal();
13 |
14 | // internal constructor
15 | DemoHacksHelper._internal();
16 |
17 | static const _defaultLowestId = 1;
18 | static const _defaultHighestId = 0;
19 |
20 | int _lowestId = _defaultLowestId;
21 | int get lowestId => _lowestId;
22 | int _highestId = _defaultHighestId;
23 | int get highestId => _highestId;
24 |
25 | late final DataSource _local;
26 | late final DataSource _remote;
27 |
28 | final bool _showError = false;
29 | final bool _artificialDelay = true;
30 |
31 | Future artificialDelay(int ms) async {
32 | if (_artificialDelay) {
33 | await Future.delayed(Duration(milliseconds: ms));
34 | }
35 | }
36 |
37 | void error() {
38 | if (_showError) {
39 | throw Exception('Flutter Vikings poor internet connection');
40 | }
41 | }
42 |
43 | void setDataSources(
44 | DataSource local, DataSource remote) {
45 | _local = local;
46 | _remote = remote;
47 | }
48 |
49 | void resetLocal() async {
50 | await _local.createAll([]);
51 | }
52 |
53 | void updateIds(List pokemon) {
54 | _lowestId = pokemon.isEmpty ? 1 : pokemon.first.id;
55 | _highestId = pokemon.isEmpty ? 0 : pokemon.last.id;
56 | }
57 |
58 | Future nextPokemonFromRemote() async {
59 | try {
60 | final model = await _remote.read(_highestId + 1);
61 | return model == null ? null : Pokemon.fromPokemonApiModel(model);
62 | } on Exception catch (_) {
63 | return null;
64 | }
65 | }
66 |
67 | Future firstPokemonFromLocal() async {
68 | try {
69 | final models = await _local.readAll();
70 | return models.isEmpty ? null : Pokemon.fromPokemonApiModel(models.first);
71 | } on Exception catch (_) {
72 | return null;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data/pokemon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon_api_model.dart';
3 |
4 | /// Pokemon represents the data model for a pokemon as it is represented on
5 | /// the UI. This differs from [PokemonApiModel] because lacking 'isDefault'
6 | /// as an example of reduction of model classes
7 | @immutable
8 | class Pokemon {
9 | const Pokemon({
10 | this.abilities = const [],
11 | this.baseExperience = 0,
12 | this.height = 0,
13 | this.id = 0,
14 | this.moves = const [],
15 | this.name = '',
16 | this.order = 0,
17 | this.image = '',
18 | this.types = const [],
19 | this.weight = 0,
20 | });
21 |
22 | final List abilities;
23 | final int baseExperience;
24 | final int height;
25 | final int id;
26 | final List moves;
27 | final String name;
28 | final int order;
29 | final String image;
30 | final List types;
31 | final int weight;
32 |
33 | factory Pokemon.fromPokemonApiModel(PokemonApiModel model) => Pokemon(
34 | abilities: model.abilities,
35 | baseExperience: model.baseExperience,
36 | height: model.height,
37 | id: model.id,
38 | moves: model.moves,
39 | name: model.name,
40 | order: model.order,
41 | image: model.image,
42 | types: model.types,
43 | weight: model.weight,
44 | );
45 |
46 | PokemonApiModel toPokemonApiModel() => PokemonApiModel(
47 | abilities: abilities,
48 | baseExperience: baseExperience,
49 | height: height,
50 | id: id,
51 | moves: moves,
52 | name: name,
53 | order: order,
54 | image: image,
55 | types: types,
56 | weight: weight,
57 | );
58 |
59 | @override
60 | bool operator ==(Object other) =>
61 | identical(this, other) ||
62 | other is Pokemon &&
63 | runtimeType == other.runtimeType &&
64 | abilities == other.abilities &&
65 | baseExperience == other.baseExperience &&
66 | height == other.height &&
67 | id == other.id &&
68 | moves == other.moves &&
69 | name == other.name &&
70 | order == other.order &&
71 | image == other.image &&
72 | types == other.types &&
73 | weight == other.weight;
74 |
75 | @override
76 | int get hashCode =>
77 | abilities.hashCode ^
78 | baseExperience.hashCode ^
79 | height.hashCode ^
80 | id.hashCode ^
81 | moves.hashCode ^
82 | name.hashCode ^
83 | order.hashCode ^
84 | image.hashCode ^
85 | types.hashCode ^
86 | weight.hashCode;
87 |
88 | get isEmpty => id == 0;
89 | List get abilitiesList =>
90 | abilities.map((i) => i.ability.name).toList();
91 | List get movesList => moves.map((i) => i.move.name).toList();
92 | List get typesList => types.map((i) => i.type.name).toList();
93 | }
94 |
--------------------------------------------------------------------------------
/lib/src/pokemon/controllers/pokemon_details_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/widgets.dart';
4 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon.dart';
5 | import 'package:flutter_riverpod/flutter_riverpod.dart';
6 |
7 | import '../../core/models/repositories/repository.dart';
8 | import '../models/repositories/pokemon_repository.dart';
9 | import '../views/ui_states/pokemon_details_ui_state.dart';
10 |
11 | /// PokemonDetailsController represents the controller of the pokemon
12 | /// details page
13 | class PokemonDetailsController extends ChangeNotifier {
14 | PokemonDetailsController(this.pokemonRepository, this.id) {
15 | _pokemonSubscription =
16 | pokemonRepository.watch(int.parse(id)).listen((pokemon) async {
17 | final changed = pokemon == null
18 | ? const PokemonDetailsItemUiState()
19 | : PokemonDetailsItemUiState.fromPokemon(pokemon);
20 | if (state.pokemon != changed) {
21 | _onLoading();
22 | // artificial delay
23 | await Future.delayed(const Duration(milliseconds: 500));
24 | _onData(pokemon);
25 | }
26 | });
27 | _uploadOnePokemon(id);
28 | }
29 |
30 | final Repository pokemonRepository;
31 | final String id;
32 | late final StreamSubscription _pokemonSubscription;
33 | // initial state is loading
34 | PokemonDetailsUiState _state =
35 | const PokemonDetailsUiState(isFetchingPokemon: true);
36 | PokemonDetailsUiState get state => _state;
37 |
38 | @override
39 | void dispose() {
40 | _pokemonSubscription.cancel();
41 | pokemonRepository.dispose();
42 | super.dispose();
43 | }
44 |
45 | void consumeError() {
46 | _state = _state.copy(errorMsg: '');
47 | notifyListeners();
48 | }
49 |
50 | void _uploadOnePokemon(String id) async {
51 | _onLoading();
52 | try {
53 | final pokemon = await pokemonRepository.read(int.parse(id));
54 | _onData(pokemon);
55 | } on Exception catch (e) {
56 | _onError('Unable to read pokemon with id $id, $e');
57 | }
58 | }
59 |
60 | void _onLoading() {
61 | // loading case
62 | _state = _state.copy(
63 | pokemon: null,
64 | isFetchingPokemon: true,
65 | errorMsg: '',
66 | );
67 | notifyListeners();
68 | }
69 |
70 | void _onData(Pokemon? data) {
71 | _state = _state.copy(
72 | pokemon: data == null
73 | ? const PokemonDetailsItemUiState()
74 | : PokemonDetailsItemUiState.fromPokemon(data),
75 | isFetchingPokemon: false,
76 | errorMsg: '',
77 | );
78 | notifyListeners();
79 | }
80 |
81 | void _onError(String msg) {
82 | // unsuccessful case
83 | _state = _state.copy(
84 | pokemon: null,
85 | isFetchingPokemon: false,
86 | errorMsg: msg,
87 | );
88 | notifyListeners();
89 | }
90 | }
91 |
92 | /// pokemonControllerProvider provides the pokemon controller
93 | final pokemonDetailsControllerProvider =
94 | ChangeNotifierProvider.family((ref, id) {
95 | final pokemonRepository = ref.read(pokemonRepositoryProvider);
96 | return PokemonDetailsController(pokemonRepository, id);
97 | });
98 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data_sources/pokemon_local_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/widgets.dart';
5 | import 'package:flutter_arch_comp/src/core/models/data_sources/data_source.dart';
6 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon_api_model.dart';
7 | import 'package:shared_preferences/shared_preferences.dart';
8 |
9 | import '../../../core/utils/demo_hacks_helper.dart';
10 |
11 | /// PokemonLocalDataSource represents a local [DataSource] for
12 | /// [PokemonApiModel]. Local data sources are backed up by the database, but
13 | /// in this case I use shared prefs to keep it simple
14 | class PokemonLocalDataSource implements DataSource {
15 | static const _pokemonKey = '_pokemonKey';
16 |
17 | @override
18 | Future create(PokemonApiModel pokemon) async {
19 | final all = await readAll();
20 | final first = all.firstWhere((p) => p.id == pokemon.id,
21 | orElse: () => const PokemonApiModel());
22 | if (first.isEmpty) {
23 | all.add(pokemon);
24 | await createAll(all);
25 | } else {
26 | update(pokemon);
27 | }
28 | }
29 |
30 | @override
31 | Future createAll(List pokemon) async {
32 | // TODO(alesalv): introduce failures
33 | try {
34 | final prefs = await SharedPreferences.getInstance();
35 | final jsonObj = pokemon.map((p) => p.toJson()).toList();
36 | final jsonStr = json.encode(jsonObj);
37 | prefs.setString(_pokemonKey, jsonStr);
38 | // this is a demo hack
39 | DemoHacksHelper.instance.updateIds(pokemon);
40 | } on Exception catch (e) {
41 | debugPrint('Failed to encode json, ${e.toString()}');
42 | throw Exception('Failed to encode json, ${e.toString()}');
43 | }
44 | }
45 |
46 | @override
47 | Future read(int id) async {
48 | final pokemonList = await readAll();
49 | final pokemon = pokemonList.firstWhere((p) => p.id == id,
50 | orElse: () => const PokemonApiModel());
51 | return pokemon.isEmpty ? null : pokemon;
52 | }
53 |
54 | @override
55 | Future> readAll() async {
56 | // TODO(alesalv): introduce failures
57 | final prefs = await SharedPreferences.getInstance();
58 | final jsonStr = prefs.getString(_pokemonKey) ?? '[]';
59 | try {
60 | final jsonObj = json.decode(jsonStr) as List;
61 | final decoded = jsonObj.map((j) => PokemonApiModel.fromJson(j)).toList();
62 | // this is a demo hack
63 | DemoHacksHelper.instance.updateIds(decoded);
64 | return decoded;
65 | } on Exception catch (e) {
66 | debugPrint('Failed to decode json, ${e.toString()}');
67 | throw Exception('Failed to decode json, ${e.toString()}');
68 | }
69 | }
70 |
71 | @override
72 | Future update(PokemonApiModel pokemon) async {
73 | final all = await readAll();
74 | final index = all.indexWhere((p) => p.id == pokemon.id);
75 | if (index != -1) {
76 | all[index] = pokemon;
77 | await createAll(all);
78 | }
79 | }
80 |
81 | @override
82 | Future delete(int id) async {
83 | final all = await readAll();
84 | final index = all.indexWhere((p) => p.id == id);
85 | if (index != -1) {
86 | all.removeAt(index);
87 | await createAll(all);
88 | }
89 | }
90 |
91 | // demo only
92 | Future reset() async {
93 | final prefs = await SharedPreferences.getInstance();
94 | await prefs.setString(_pokemonKey, '[]');
95 | // this is a demo hack
96 | DemoHacksHelper.instance.updateIds([]);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data/pokemon_api_model.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'pokemon_api_model.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | PokemonApiModel _$PokemonApiModelFromJson(Map json) =>
10 | PokemonApiModel(
11 | abilities: (json['abilities'] as List?)
12 | ?.map((e) => AbilityInfo.fromJson(e as Map))
13 | .toList() ??
14 | const [],
15 | baseExperience: json['base_experience'] as int? ?? 0,
16 | height: json['height'] as int? ?? 0,
17 | id: json['id'] as int? ?? 0,
18 | isDefault: json['is_default'] as bool? ?? false,
19 | moves: (json['moves'] as List?)
20 | ?.map((e) => MoveInfo.fromJson(e as Map))
21 | .toList() ??
22 | const [],
23 | name: json['name'] as String? ?? '',
24 | order: json['order'] as int? ?? 0,
25 | image: json['sprites'] == null
26 | ? ''
27 | : _extractImage(json['sprites'] as Object),
28 | types: (json['types'] as List?)
29 | ?.map((e) => TypeInfo.fromJson(e as Map))
30 | .toList() ??
31 | const [],
32 | weight: json['weight'] as int? ?? 0,
33 | );
34 |
35 | Map _$PokemonApiModelToJson(PokemonApiModel instance) =>
36 | {
37 | 'abilities': instance.abilities.map((e) => e.toJson()).toList(),
38 | 'base_experience': instance.baseExperience,
39 | 'height': instance.height,
40 | 'id': instance.id,
41 | 'is_default': instance.isDefault,
42 | 'moves': instance.moves.map((e) => e.toJson()).toList(),
43 | 'name': instance.name,
44 | 'order': instance.order,
45 | 'sprites': instance.image,
46 | 'types': instance.types.map((e) => e.toJson()).toList(),
47 | 'weight': instance.weight,
48 | };
49 |
50 | AbilityInfo _$AbilityInfoFromJson(Map json) => AbilityInfo(
51 | Ability.fromJson(json['ability'] as Map),
52 | );
53 |
54 | Map _$AbilityInfoToJson(AbilityInfo instance) =>
55 | {
56 | 'ability': instance.ability.toJson(),
57 | };
58 |
59 | Ability _$AbilityFromJson(Map json) => Ability(
60 | json['name'] as String,
61 | );
62 |
63 | Map _$AbilityToJson(Ability instance) => {
64 | 'name': instance.name,
65 | };
66 |
67 | MoveInfo _$MoveInfoFromJson(Map json) => MoveInfo(
68 | Move.fromJson(json['move'] as Map),
69 | );
70 |
71 | Map _$MoveInfoToJson(MoveInfo instance) => {
72 | 'move': instance.move.toJson(),
73 | };
74 |
75 | Move _$MoveFromJson(Map json) => Move(
76 | json['name'] as String,
77 | );
78 |
79 | Map _$MoveToJson(Move instance) => {
80 | 'name': instance.name,
81 | };
82 |
83 | TypeInfo _$TypeInfoFromJson(Map json) => TypeInfo(
84 | Type.fromJson(json['type'] as Map),
85 | );
86 |
87 | Map _$TypeInfoToJson(TypeInfo instance) => {
88 | 'type': instance.type.toJson(),
89 | };
90 |
91 | Type _$TypeFromJson(Map json) => Type(
92 | json['name'] as String,
93 | );
94 |
95 | Map _$TypeToJson(Type instance) => {
96 | 'name': instance.name,
97 | };
98 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/ui_states/pokemon_details_ui_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter_arch_comp/src/core/utils/extensions.dart';
3 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon.dart';
4 |
5 | /// PokemonDetailsUiState represents the UI state for the pokemon details page
6 | @immutable
7 | class PokemonDetailsUiState {
8 | const PokemonDetailsUiState({
9 | this.pokemon = const PokemonDetailsItemUiState(),
10 | this.isFetchingPokemon = false,
11 | this.errorMsg = '',
12 | });
13 |
14 | final PokemonDetailsItemUiState pokemon;
15 | final bool isFetchingPokemon;
16 | final String errorMsg;
17 |
18 | PokemonDetailsUiState copy({
19 | PokemonDetailsItemUiState? pokemon,
20 | bool? isFetchingPokemon,
21 | String? errorMsg,
22 | }) {
23 | return PokemonDetailsUiState(
24 | pokemon: pokemon ?? this.pokemon,
25 | isFetchingPokemon: isFetchingPokemon ?? this.isFetchingPokemon,
26 | errorMsg: errorMsg ?? this.errorMsg,
27 | );
28 | }
29 |
30 | @override
31 | bool operator ==(Object other) =>
32 | identical(this, other) ||
33 | other is PokemonDetailsUiState &&
34 | runtimeType == other.runtimeType &&
35 | pokemon == other.pokemon &&
36 | isFetchingPokemon == other.isFetchingPokemon &&
37 | errorMsg == other.errorMsg;
38 |
39 | @override
40 | int get hashCode =>
41 | pokemon.hashCode ^ isFetchingPokemon.hashCode ^ errorMsg.hashCode;
42 | }
43 |
44 | /// PokemonDetailsItemUiState represents the UI state for an item of the
45 | /// pokemon details page
46 | @immutable
47 | class PokemonDetailsItemUiState {
48 | const PokemonDetailsItemUiState({
49 | this.abilities = '',
50 | this.baseExperience = '',
51 | this.height = '',
52 | this.id = '',
53 | this.moves = '',
54 | this.name = '',
55 | this.image = '',
56 | this.types = '',
57 | this.weight = '',
58 | });
59 |
60 | final String abilities;
61 | final String baseExperience;
62 | final String height;
63 | final String id;
64 | final String moves;
65 | final String name;
66 | final String image;
67 | final String types;
68 | final String weight;
69 |
70 | factory PokemonDetailsItemUiState.fromPokemon(Pokemon pokemon) =>
71 | PokemonDetailsItemUiState(
72 | abilities: pokemon.abilitiesList.toPlainString(),
73 | baseExperience: pokemon.baseExperience.toString(),
74 | height: pokemon.height.toString(),
75 | id: pokemon.id.toString(),
76 | moves: pokemon.movesList.toPlainString(),
77 | name: pokemon.name,
78 | image: pokemon.image,
79 | types: pokemon.typesList.toPlainString(),
80 | weight: pokemon.weight.toString(),
81 | );
82 |
83 | @override
84 | bool operator ==(Object other) =>
85 | identical(this, other) ||
86 | other is PokemonDetailsItemUiState &&
87 | runtimeType == other.runtimeType &&
88 | abilities == other.abilities &&
89 | baseExperience == other.baseExperience &&
90 | height == other.height &&
91 | id == other.id &&
92 | moves == other.moves &&
93 | name == other.name &&
94 | image == other.image &&
95 | types == other.types &&
96 | weight == other.weight;
97 |
98 | @override
99 | int get hashCode =>
100 | abilities.hashCode ^
101 | baseExperience.hashCode ^
102 | height.hashCode ^
103 | id.hashCode ^
104 | moves.hashCode ^
105 | name.hashCode ^
106 | image.hashCode ^
107 | types.hashCode ^
108 | weight.hashCode;
109 |
110 | get isEmpty => this == const PokemonDetailsItemUiState();
111 | }
112 |
--------------------------------------------------------------------------------
/lib/src/core/app.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_arch_comp/src/core/views/pages/home_page.dart';
3 | import 'package:flutter_arch_comp/src/core/views/pages/splash_page.dart';
4 | import 'package:flutter_arch_comp/src/pokemon/views/pages/pokemon_details_page.dart';
5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
6 | import 'package:flutter_localizations/flutter_localizations.dart';
7 | import 'package:flutter_riverpod/flutter_riverpod.dart';
8 |
9 | import '../pokemon/views/pages/pokemon_page.dart';
10 | import '../settings/controllers/settings_controller.dart';
11 | import '../settings/views/pages/settings_page.dart';
12 |
13 | /// The Widget that configures your application.
14 | class MyApp extends ConsumerWidget {
15 | const MyApp({super.key});
16 |
17 | @override
18 | Widget build(BuildContext context, WidgetRef ref) {
19 | final settingsController = ref.watch(settingsControllerProvider);
20 |
21 | // Glue the SettingsController to the MaterialApp.
22 | //
23 | // The AnimatedBuilder Widget listens to the SettingsController for changes.
24 | // Whenever the user updates their settings, the MaterialApp is rebuilt.
25 | return AnimatedBuilder(
26 | animation: settingsController,
27 | builder: (BuildContext context, Widget? child) {
28 | return MaterialApp(
29 | // Providing a restorationScopeId allows the Navigator built by the
30 | // MaterialApp to restore the navigation stack when a user leaves and
31 | // returns to the app after it has been killed while running in the
32 | // background.
33 | restorationScopeId: 'app',
34 |
35 | // Provide the generated AppLocalizations to the MaterialApp. This
36 | // allows descendant Widgets to display the correct translations
37 | // depending on the user's locale.
38 | localizationsDelegates: const [
39 | AppLocalizations.delegate,
40 | GlobalMaterialLocalizations.delegate,
41 | GlobalWidgetsLocalizations.delegate,
42 | GlobalCupertinoLocalizations.delegate,
43 | ],
44 | supportedLocales: const [
45 | Locale('en', ''), // English, no country code
46 | ],
47 |
48 | // Use AppLocalizations to configure the correct application title
49 | // depending on the user's locale.
50 | //
51 | // The appTitle is defined in .arb files found in the localization
52 | // directory.
53 | onGenerateTitle: (BuildContext context) =>
54 | AppLocalizations.of(context)!.appTitle,
55 |
56 | // Define a light and dark color theme. Then, read the user's
57 | // preferred ThemeMode (light, dark, or system default) from the
58 | // SettingsController to display the correct theme.
59 | theme: ThemeData(),
60 | darkTheme: ThemeData.dark(),
61 | themeMode: settingsController.themeMode,
62 |
63 | // Define a function to handle named routes in order to support
64 | // Flutter web url navigation and deep linking.
65 | onGenerateRoute: (RouteSettings routeSettings) {
66 | return MaterialPageRoute(
67 | settings: routeSettings,
68 | builder: (BuildContext context) {
69 | switch (routeSettings.name) {
70 | case HomePage.routeName:
71 | return const HomePage();
72 | case SettingsPage.routeName:
73 | return const SettingsPage();
74 | case PokemonPage.routeName:
75 | return const PokemonPage();
76 | case PokemonDetailsPage.routeName:
77 | return const PokemonDetailsPage();
78 | default:
79 | return const SplashPage();
80 | }
81 | },
82 | );
83 | },
84 | );
85 | },
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data_sources/pokemon_remote_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_arch_comp/src/core/utils/demo_hacks_helper.dart';
4 | import 'package:http/http.dart' as http;
5 |
6 | import '../../../core/models/data_sources/data_source.dart';
7 | import '../data/pokemon_api_model.dart';
8 |
9 | PokemonApiModel parsePokemon(String responseBody) {
10 | final parsed = json.decode(responseBody) as Map;
11 | return PokemonApiModel.fromJson(parsed);
12 | }
13 |
14 | /// PokemonRemoteDataSource represents a remote [DataSource] for
15 | /// [PokemonApiModel]. Remote data sources are backed up by the server
16 | class PokemonRemoteDataSource implements DataSource {
17 | @override
18 | Future create(PokemonApiModel pokemon) async {
19 | try {
20 | // mocks the pokemon as created if I can poke the server
21 | await http
22 | .get(Uri.parse('https://pokeapi.co/api/v2/pokemon/1/'))
23 | .timeout(const Duration(seconds: 4));
24 | } on Exception catch (e) {
25 | throw Exception(
26 | 'Unable to create pokemon with id ${pokemon.id} from API, ${e.toString()}');
27 | }
28 | }
29 |
30 | @override
31 | Future createAll(List data) async {
32 | try {
33 | // mocks the pokemon as created if I can poke the server
34 | await http
35 | .get(Uri.parse('https://pokeapi.co/api/v2/pokemon/1/'))
36 | .timeout(const Duration(seconds: 4));
37 | } on Exception catch (e) {
38 | throw Exception('Unable to create all pokemon from API, ${e.toString()}');
39 | }
40 | }
41 |
42 | @override
43 | Future delete(int id) async {
44 | try {
45 | // mocks the pokemon as deleted if I can poke the server
46 | await http
47 | .get(Uri.parse('https://pokeapi.co/api/v2/pokemon/1/'))
48 | .timeout(const Duration(seconds: 4));
49 | } on Exception catch (e) {
50 | throw Exception(
51 | 'Unable to delete pokemon with id $id from API, ${e.toString()}');
52 | }
53 | }
54 |
55 | @override
56 | Future read(int id) async {
57 | try {
58 | final res = await http
59 | .get(Uri.parse('https://pokeapi.co/api/v2/pokemon/$id/'))
60 | .timeout(const Duration(seconds: 4));
61 | if (res.statusCode == 200) {
62 | return parsePokemon(res.body);
63 | } else {
64 | throw Exception();
65 | }
66 | } on Exception catch (e) {
67 | throw Exception(
68 | 'Unable to read pokemon with id $id from API, ${e.toString()}');
69 | }
70 | }
71 |
72 | @override
73 | Future> readAll() async {
74 | /// create pokemon urls
75 | final urls = [];
76 | for (int i = DemoHacksHelper.instance.lowestId;
77 | i <= DemoHacksHelper.instance.highestId + 1;
78 | i++) {
79 | urls.add('https://pokeapi.co/api/v2/pokemon/$i/');
80 | }
81 |
82 | try {
83 | final pokemon = await Future.wait(urls.map((url) async {
84 | final res = await http.get(Uri.parse(url));
85 | if (res.statusCode == 200) {
86 | return parsePokemon(res.body);
87 | } else {
88 | throw Exception();
89 | }
90 | }));
91 |
92 | return pokemon;
93 | } on Exception catch (e) {
94 | throw Exception('Unable to read all pokemon from API, ${e.toString()}');
95 | }
96 | }
97 |
98 | @override
99 | Future update(PokemonApiModel pokemon) async {
100 | try {
101 | // mocks the pokemon as updated if I can poke the server
102 | await http
103 | .get(Uri.parse('https://pokeapi.co/api/v2/pokemon/1/'))
104 | .timeout(const Duration(seconds: 4));
105 | } on Exception catch (e) {
106 | throw Exception(
107 | 'Unable to update pokemon with id ${pokemon.id} from API, ${e.toString()}');
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/assets/images/void.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | flutter_arch_comp
30 |
31 |
32 |
33 |
36 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/pages/pokemon_details_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 |
4 | import '../../../core/views/widgets/circular_image.dart';
5 | import '../../controllers/pokemon_details_controller.dart';
6 | import '../widgets/loading_indicator.dart';
7 |
8 | /// Displays detailed information about a pokemon
9 | class PokemonDetailsPage extends StatelessWidget {
10 | const PokemonDetailsPage({super.key});
11 |
12 | static const routeName = 'pokemon_details';
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final pokemonId =
17 | (ModalRoute.of(context)!.settings.arguments as PokemonDetailsViewArgs)
18 | .id;
19 |
20 | return Scaffold(
21 | appBar: AppBar(
22 | title: const Text('Pokemon Details'),
23 | ),
24 | body: Stack(children: [
25 | _PokemonDetailsPanel(pokemonId),
26 | _LoadingIndicator(pokemonId),
27 | _ErrorIndicator(pokemonId),
28 | ]),
29 | );
30 | }
31 | }
32 |
33 | class PokemonDetailsViewArgs {
34 | final String id;
35 | const PokemonDetailsViewArgs(this.id);
36 | }
37 |
38 | class _LoadingIndicator extends ConsumerWidget {
39 | const _LoadingIndicator(this.id);
40 | final String id;
41 | @override
42 | Widget build(BuildContext context, WidgetRef ref) {
43 | final isFetchingPokemon = ref.watch(pokemonDetailsControllerProvider(id)
44 | .select((c) => c.state.isFetchingPokemon));
45 | return isFetchingPokemon
46 | ? const LoadingIndicator()
47 | : const SizedBox.shrink();
48 | }
49 | }
50 |
51 | class _PokemonDetailsPanel extends ConsumerWidget {
52 | const _PokemonDetailsPanel(this.id);
53 | final String id;
54 | @override
55 | Widget build(BuildContext context, WidgetRef ref) {
56 | final pokemon = ref.watch(
57 | pokemonDetailsControllerProvider(id).select((c) => c.state.pokemon));
58 | return pokemon.isEmpty
59 | ? Center(
60 | child: Text(
61 | 'No information for pokemon with id $id',
62 | style: const TextStyle(fontSize: 20),
63 | ),
64 | )
65 | : SingleChildScrollView(
66 | child: Column(
67 | children: [
68 | Padding(
69 | padding: const EdgeInsets.all(32.0),
70 | child: CircularImage(imageUrl: pokemon.image, size: 200),
71 | ),
72 | _Tile(title: 'Name', content: pokemon.name),
73 | _Tile(title: 'Experience', content: pokemon.baseExperience),
74 | _Tile(title: 'Height (dm)', content: pokemon.height),
75 | _Tile(title: 'Weight (hg)', content: pokemon.weight),
76 | _Tile(title: 'Types', content: pokemon.types.toString()),
77 | _Tile(title: 'Abilities', content: pokemon.abilities),
78 | _Tile(title: 'Moves', content: pokemon.moves.toString()),
79 | ],
80 | ),
81 | );
82 | }
83 | }
84 |
85 | class _ErrorIndicator extends ConsumerWidget {
86 | const _ErrorIndicator(this.id);
87 | final String id;
88 |
89 | @override
90 | Widget build(BuildContext context, WidgetRef ref) {
91 | final msg = ref.watch(
92 | pokemonDetailsControllerProvider(id).select((c) => c.state.errorMsg));
93 |
94 | if (msg.isNotEmpty) {
95 | WidgetsBinding.instance
96 | .addPostFrameCallback((final _) => _showSnackbar(context, ref, msg));
97 | }
98 | return const SizedBox.shrink();
99 | }
100 |
101 | void _showSnackbar(BuildContext context, WidgetRef ref, String msg) {
102 | final snackBar = SnackBar(content: Text('Oops something went wrong: $msg'));
103 | ScaffoldMessenger.of(context).showSnackBar(snackBar);
104 | ref.read(pokemonDetailsControllerProvider(id)).consumeError();
105 | }
106 | }
107 |
108 | class _Tile extends StatelessWidget {
109 | const _Tile({required this.title, required this.content});
110 | final String title;
111 | final String content;
112 | @override
113 | Widget build(BuildContext context) {
114 | return Padding(
115 | padding: const EdgeInsets.all(8.0),
116 | child: Text('${title.toUpperCase()}: $content'),
117 | );
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/lib/src/pokemon/views/pages/pokemon_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_arch_comp/src/pokemon/views/widgets/actions_fabs_row.dart';
3 | import 'package:flutter_arch_comp/src/pokemon/views/widgets/actions_menu_button.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 | import 'package:flutter_svg/svg.dart';
6 |
7 | import '../../controllers/pokemon_controller.dart';
8 | import '../widgets/loading_indicator.dart';
9 | import '../widgets/pokemon_card.dart';
10 |
11 | /// PokemonPage represents a page to displays a list of pokemon, showing a
12 | /// loading indicator for fetching operations and an error indicator for errors
13 | class PokemonPage extends ConsumerStatefulWidget {
14 | const PokemonPage({super.key});
15 |
16 | static const routeName = 'pokemon_page';
17 |
18 | @override
19 | ConsumerState createState() => _PokemonPageState();
20 | }
21 |
22 | class _PokemonPageState extends ConsumerState {
23 | @override
24 | void initState() {
25 | super.initState();
26 | WidgetsBinding.instance.addPostFrameCallback((final _) => _upload());
27 | }
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | return Scaffold(
32 | appBar: AppBar(
33 | title: const Text('Pokemon'),
34 | actions: const [ActionsMenuButton()],
35 | ),
36 | body: Stack(children: [
37 | _PokemonList(),
38 | _LoadingIndicator(),
39 | _ErrorIndicator(),
40 | ]),
41 | floatingActionButton: const ActionsFabsRow(),
42 | );
43 | }
44 |
45 | _upload() {
46 | ref.read(pokemonControllerProvider).uploadPokemon();
47 | }
48 | }
49 |
50 | class _LoadingIndicator extends ConsumerWidget {
51 | @override
52 | Widget build(BuildContext context, WidgetRef ref) {
53 | final isFetchingPokemon = ref.watch(
54 | pokemonControllerProvider.select((c) => c.state.isFetchingPokemon));
55 |
56 | return isFetchingPokemon
57 | ? const LoadingIndicator()
58 | : const SizedBox.shrink();
59 | }
60 | }
61 |
62 | class _PokemonList extends ConsumerWidget {
63 | @override
64 | Widget build(BuildContext context, WidgetRef ref) {
65 | final items =
66 | ref.watch(pokemonControllerProvider.select((c) => c.state.pokemon));
67 |
68 | return items.isEmpty
69 | ? Padding(
70 | padding: const EdgeInsets.only(top: 200),
71 | child: Center(
72 | child: Column(
73 | children: [
74 | SvgPicture.asset(
75 | 'assets/images/void.svg',
76 | width: 150,
77 | height: 150, //asset location
78 | ),
79 | const SizedBox(
80 | height: 24,
81 | ),
82 | const Text(
83 | 'No pokemon yet',
84 | style: TextStyle(fontSize: 20),
85 | ),
86 | ],
87 | ),
88 | ),
89 | )
90 | : ListView.builder(
91 | // Providing a restorationId allows the ListView to restore the
92 | // scroll position when a user leaves and returns to the app after it
93 | // has been killed while running in the background.
94 | restorationId: 'pokemonListView',
95 | itemCount: items.length,
96 | itemBuilder: (BuildContext context, int index) {
97 | return PokemonCard(items[index]);
98 | },
99 | );
100 | }
101 | }
102 |
103 | class _ErrorIndicator extends ConsumerWidget {
104 | @override
105 | Widget build(BuildContext context, WidgetRef ref) {
106 | final msg =
107 | ref.watch(pokemonControllerProvider.select((c) => c.state.errorMsg));
108 |
109 | if (msg.isNotEmpty) {
110 | WidgetsBinding.instance
111 | .addPostFrameCallback((final _) => _showSnackbar(context, ref, msg));
112 | }
113 | return const SizedBox.shrink();
114 | }
115 |
116 | void _showSnackbar(BuildContext context, WidgetRef ref, String msg) {
117 | final snackBar = SnackBar(content: Text('Oops something went wrong: $msg'));
118 | ScaffoldMessenger.of(context).showSnackBar(snackBar);
119 | ref.read(pokemonControllerProvider).consumeError();
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/lib/src/pokemon/controllers/pokemon_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/widgets.dart';
4 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon.dart';
5 | import 'package:flutter_arch_comp/src/pokemon/models/repositories/pokemon_repository.dart';
6 | import 'package:flutter_arch_comp/src/pokemon/views/ui_states/pokemon_ui_state.dart';
7 | import 'package:flutter_riverpod/flutter_riverpod.dart';
8 |
9 | import '../../core/models/repositories/repository.dart';
10 | import '../../core/utils/demo_hacks_helper.dart';
11 |
12 | /// PokemonController represents the controller of the pokemon page
13 | class PokemonController extends ChangeNotifier {
14 | PokemonController(this.pokemonRepository) {
15 | _pokemonSubscription = pokemonRepository.watchAll().listen((pokemon) async {
16 | _onData(pokemon);
17 | });
18 | }
19 |
20 | final Repository pokemonRepository;
21 | late final StreamSubscription _pokemonSubscription;
22 | PokemonUiState _state = PokemonUiState();
23 | PokemonUiState get state => _state;
24 |
25 | Future create(Pokemon pokemon) async {
26 | /// demo only, the param 'pokemon' passed to create() is not really used;
27 | /// usually it would come from the user adding a new entry on UI, or from
28 | /// a push notification or firebase, but in this demo we use always the
29 | /// first next from remote
30 | final next = await DemoHacksHelper.instance.nextPokemonFromRemote();
31 | if (next != null) {
32 | _onLoading();
33 | try {
34 | await pokemonRepository.create(next);
35 | // _onData is handled through watchPokemon
36 | } on Exception catch (e) {
37 | _onError('Unable to create pokemon with id ${next.id}, $e');
38 | }
39 | }
40 | }
41 |
42 | void update(Pokemon pokemon) {}
43 |
44 | void delete(int id) async {
45 | /// demo only, the param 'id' passed to delete() is not really used;
46 | /// usually it would come from the user swiping a given pokemon away from
47 | /// the list on UI, but in this demo we use always the first pokemon from
48 | /// local
49 | final first = await DemoHacksHelper.instance.firstPokemonFromLocal();
50 | if (first != null) {
51 | _onLoading();
52 | try {
53 | await pokemonRepository.delete(first.id);
54 | // _onData is handled through watchPokemon
55 | } on Exception catch (e) {
56 | _onError('Unable to delete pokemon with id ${first.id}, $e');
57 | }
58 | }
59 | }
60 |
61 | void refresh() async {
62 | _onLoading();
63 | try {
64 | await pokemonRepository.refresh();
65 | // _onData is handled through watchPokemon
66 | } on Exception catch (e) {
67 | _onError('Unable to refresh pokemon, $e');
68 | }
69 | }
70 |
71 | void uploadPokemon() async {
72 | _onLoading();
73 | try {
74 | final pokemon = await pokemonRepository.readAll();
75 | _onData(pokemon);
76 | } on Exception catch (e) {
77 | _onError('Unable to upload pokemon, $e');
78 | } finally {
79 | // async call
80 | refresh();
81 | }
82 | }
83 |
84 | @override
85 | void dispose() {
86 | _pokemonSubscription.cancel();
87 | pokemonRepository.dispose();
88 | super.dispose();
89 | }
90 |
91 | void consumeError() {
92 | _state = _state.copy(errorMsg: '');
93 | notifyListeners();
94 | }
95 |
96 | /// demo only
97 | void resetLocal() {
98 | DemoHacksHelper.instance.resetLocal();
99 | _state = _state.copy(pokemon: []);
100 | notifyListeners();
101 | }
102 |
103 | void _onLoading() {
104 | _state = _state.copy(
105 | pokemon: null,
106 | isFetchingPokemon: true,
107 | errorMsg: '',
108 | );
109 | notifyListeners();
110 | }
111 |
112 | void _onData(List data) {
113 | _state = _state.copy(
114 | pokemon: data.map((p) => PokemonItemUiState.fromPokemon(p)).toList(),
115 | isFetchingPokemon: false,
116 | errorMsg: '',
117 | );
118 | notifyListeners();
119 | }
120 |
121 | void _onError(String msg) {
122 | // unsuccessful case, keep previous data
123 | _state = _state.copy(
124 | pokemon: null,
125 | isFetchingPokemon: false,
126 | errorMsg: msg,
127 | );
128 | notifyListeners();
129 | }
130 | }
131 |
132 | /// pokemonControllerProvider provides the pokemon controller
133 | final pokemonControllerProvider = ChangeNotifierProvider((ref) {
134 | final pokemonRepository = ref.read(pokemonRepositoryProvider);
135 | return PokemonController(pokemonRepository);
136 | });
137 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/data/pokemon_api_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:json_annotation/json_annotation.dart';
3 |
4 | part 'pokemon_api_model.g.dart';
5 |
6 | String _extractImage(Object input) {
7 | if (input is Map) {
8 | return input['front_default'];
9 | }
10 | if (input is String && input.startsWith('https://')) {
11 | return input;
12 | }
13 | return '';
14 | }
15 |
16 | /// PokemonApiModel represents the data model for a pokemon as it comes from
17 | /// the server side
18 | @JsonSerializable(explicitToJson: true)
19 | @immutable
20 | class PokemonApiModel {
21 | const PokemonApiModel({
22 | this.abilities = const [],
23 | this.baseExperience = 0,
24 | this.height = 0,
25 | this.id = 0,
26 | this.isDefault = false,
27 | this.moves = const [],
28 | this.name = '',
29 | this.order = 0,
30 | this.image = '',
31 | this.types = const [],
32 | this.weight = 0,
33 | });
34 |
35 | final List abilities;
36 | @JsonKey(name: 'base_experience')
37 | final int baseExperience;
38 | final int height;
39 | final int id;
40 | @JsonKey(name: 'is_default')
41 | final bool isDefault; // isDefault is a property not used by the UI layer
42 | final List moves;
43 | final String name;
44 | final int order;
45 | @JsonKey(name: 'sprites', fromJson: _extractImage)
46 | final String image;
47 | final List types;
48 | final int weight;
49 |
50 | factory PokemonApiModel.fromJson(Map json) =>
51 | _$PokemonApiModelFromJson(json);
52 | Map toJson() => _$PokemonApiModelToJson(this);
53 |
54 | @override
55 | bool operator ==(Object other) =>
56 | identical(this, other) ||
57 | other is PokemonApiModel &&
58 | runtimeType == other.runtimeType &&
59 | listEquals(abilities, other.abilities) &&
60 | baseExperience == other.baseExperience &&
61 | height == other.height &&
62 | id == other.id &&
63 | isDefault == other.isDefault &&
64 | listEquals(moves, other.moves) &&
65 | name == other.name &&
66 | order == other.order &&
67 | image == other.image &&
68 | listEquals(types, other.types) &&
69 | weight == other.weight;
70 |
71 | @override
72 | int get hashCode =>
73 | abilities.hashCode ^
74 | baseExperience.hashCode ^
75 | height.hashCode ^
76 | id.hashCode ^
77 | isDefault.hashCode ^
78 | moves.hashCode ^
79 | name.hashCode ^
80 | order.hashCode ^
81 | image.hashCode ^
82 | types.hashCode ^
83 | weight.hashCode;
84 |
85 | get isEmpty => id == 0;
86 | List get abilitiesList =>
87 | abilities.map((i) => i.ability.name).toList();
88 | List get movesList => moves.map((i) => i.move.name).toList();
89 | List get typesList => types.map((i) => i.type.name).toList();
90 | }
91 |
92 | @JsonSerializable(explicitToJson: true)
93 | @immutable
94 | class AbilityInfo {
95 | const AbilityInfo(this.ability);
96 | final Ability ability;
97 | factory AbilityInfo.fromJson(Map json) =>
98 | _$AbilityInfoFromJson(json);
99 | Map toJson() => _$AbilityInfoToJson(this);
100 |
101 | @override
102 | bool operator ==(Object other) =>
103 | identical(this, other) ||
104 | other is AbilityInfo &&
105 | runtimeType == other.runtimeType &&
106 | ability == other.ability;
107 |
108 | @override
109 | int get hashCode => ability.hashCode;
110 | }
111 |
112 | @JsonSerializable()
113 | @immutable
114 | class Ability {
115 | const Ability(this.name);
116 | final String name;
117 | factory Ability.fromJson(Map json) =>
118 | _$AbilityFromJson(json);
119 | Map toJson() => _$AbilityToJson(this);
120 |
121 | @override
122 | bool operator ==(Object other) =>
123 | identical(this, other) ||
124 | other is Ability &&
125 | runtimeType == other.runtimeType &&
126 | name == other.name;
127 |
128 | @override
129 | int get hashCode => name.hashCode;
130 | }
131 |
132 | @JsonSerializable(explicitToJson: true)
133 | @immutable
134 | class MoveInfo {
135 | const MoveInfo(this.move);
136 | final Move move;
137 | factory MoveInfo.fromJson(Map json) =>
138 | _$MoveInfoFromJson(json);
139 | Map toJson() => _$MoveInfoToJson(this);
140 |
141 | @override
142 | bool operator ==(Object other) =>
143 | identical(this, other) ||
144 | other is MoveInfo &&
145 | runtimeType == other.runtimeType &&
146 | move == other.move;
147 |
148 | @override
149 | int get hashCode => move.hashCode;
150 | }
151 |
152 | @JsonSerializable()
153 | @immutable
154 | class Move {
155 | const Move(this.name);
156 | final String name;
157 | factory Move.fromJson(Map json) => _$MoveFromJson(json);
158 | Map toJson() => _$MoveToJson(this);
159 |
160 | @override
161 | bool operator ==(Object other) =>
162 | identical(this, other) ||
163 | other is Move && runtimeType == other.runtimeType && name == other.name;
164 |
165 | @override
166 | int get hashCode => name.hashCode;
167 | }
168 |
169 | @JsonSerializable(explicitToJson: true)
170 | @immutable
171 | class TypeInfo {
172 | const TypeInfo(this.type);
173 | final Type type;
174 | factory TypeInfo.fromJson(Map json) =>
175 | _$TypeInfoFromJson(json);
176 | Map toJson() => _$TypeInfoToJson(this);
177 |
178 | @override
179 | bool operator ==(Object other) =>
180 | identical(this, other) ||
181 | other is TypeInfo &&
182 | runtimeType == other.runtimeType &&
183 | type == other.type;
184 |
185 | @override
186 | int get hashCode => type.hashCode;
187 | }
188 |
189 | @JsonSerializable()
190 | @immutable
191 | class Type {
192 | const Type(this.name);
193 | final String name;
194 | factory Type.fromJson(Map json) => _$TypeFromJson(json);
195 | Map toJson() => _$TypeToJson(this);
196 |
197 | @override
198 | bool operator ==(Object other) =>
199 | identical(this, other) ||
200 | other is Type && runtimeType == other.runtimeType && name == other.name;
201 |
202 | @override
203 | int get hashCode => name.hashCode;
204 | }
205 |
--------------------------------------------------------------------------------
/lib/src/pokemon/models/repositories/pokemon_repository.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter_arch_comp/src/core/models/repositories/repository.dart';
4 | import 'package:flutter_riverpod/flutter_riverpod.dart';
5 |
6 | import '../../../core/models/data_sources/data_source.dart';
7 | import '../../../core/utils/demo_hacks_helper.dart';
8 | import '../../../network/utils/connectivity.dart';
9 | import '../data/pokemon.dart';
10 | import '../data/pokemon_api_model.dart';
11 | import '../data_sources/pokemon_local_data_source.dart';
12 | import '../data_sources/pokemon_remote_data_source.dart';
13 |
14 | /// PokemonRepository represents a [Repository] for pokemon. For more
15 | /// information on repositories, see
16 | /// https://developer.android.com/jetpack/guide/data-layer
17 | class PokemonRepository implements Repository {
18 | PokemonRepository(this._local, this._remote, this._connectivity) {
19 | // this is a demo hack
20 | DemoHacksHelper.instance.setDataSources(_local, _remote);
21 | }
22 | final DataSource _local;
23 | final DataSource _remote;
24 | final Connectivity _connectivity;
25 | final _streamController =
26 | StreamController>.broadcast(sync: true);
27 |
28 | @override
29 | Stream watch(int id) {
30 | return _streamController.stream.map((pokemon) {
31 | final one =
32 | pokemon.firstWhere((p) => p.id == id, orElse: () => const Pokemon());
33 | return one.isEmpty ? null : one;
34 | });
35 | }
36 |
37 | @override
38 | Stream> watchAll() {
39 | return _streamController.stream;
40 | }
41 |
42 | @override
43 | Future create(Pokemon pokemon) async {
44 | // TODO(alesalv): remove artificial delay
45 | await DemoHacksHelper.instance.artificialDelay(3000);
46 |
47 | // TODO(alesalv): remove error
48 | DemoHacksHelper.instance.error();
49 |
50 | if (await _connectivity.isConnected()) {
51 | try {
52 | final model = pokemon.toPokemonApiModel();
53 | await _remote.create(model);
54 | await _local.create(model);
55 | final models = await _local.readAll();
56 | _streamController
57 | .add(models.map((m) => Pokemon.fromPokemonApiModel(m)).toList());
58 | } on Exception catch (e) {
59 | throw Exception('Unable to create pokemon with id ${pokemon.id}, $e');
60 | }
61 | } else {
62 | // here an offline first approach should be implemented, out of the
63 | // scope for this demo
64 | throw UnimplementedError('Not supported for this demo');
65 | }
66 | }
67 |
68 | @override
69 | Future read(int id) async {
70 | // TODO(alesalv): remove artificial delay
71 | await DemoHacksHelper.instance.artificialDelay(3000);
72 |
73 | if (await _connectivity.isConnected()) {
74 | try {
75 | final model = await _local.read(id);
76 | return model == null ? null : Pokemon.fromPokemonApiModel(model);
77 | } on Exception catch (e) {
78 | throw Exception('Unable to read pokemon with id $id, $e');
79 | }
80 | } else {
81 | throw UnimplementedError('Not supported for this demo');
82 | }
83 | }
84 |
85 | @override
86 | Future> readAll() async {
87 | // TODO(alesalv): remove artificial delay
88 | await DemoHacksHelper.instance.artificialDelay(1);
89 |
90 | if (await _connectivity.isConnected()) {
91 | try {
92 | final models = await _local.readAll();
93 | return models.map((m) => Pokemon.fromPokemonApiModel(m)).toList();
94 | } on Exception catch (e) {
95 | throw Exception('Unable to read pokemon, $e');
96 | }
97 | } else {
98 | throw UnimplementedError('Not supported for this demo');
99 | }
100 | }
101 |
102 | @override
103 | Future update(Pokemon pokemon) async {
104 | // TODO(alesalv): remove artificial delay
105 | await DemoHacksHelper.instance.artificialDelay(3000);
106 |
107 | if (await _connectivity.isConnected()) {
108 | try {
109 | final model = pokemon.toPokemonApiModel();
110 | await _remote.update(model);
111 | await _local.update(model);
112 | final models = await _local.readAll();
113 | _streamController
114 | .add(models.map((m) => Pokemon.fromPokemonApiModel(m)).toList());
115 | } on Exception catch (e) {
116 | throw Exception('Unable to update pokemon with id ${pokemon.id}, $e');
117 | }
118 | } else {
119 | throw UnimplementedError('Not supported for this demo');
120 | }
121 | }
122 |
123 | @override
124 | Future delete(int id) async {
125 | // TODO(alesalv): remove artificial delay
126 | await DemoHacksHelper.instance.artificialDelay(3000);
127 |
128 | if (await _connectivity.isConnected()) {
129 | try {
130 | await _remote.delete(id);
131 | await _local.delete(id);
132 | final models = await _local.readAll();
133 | _streamController
134 | .add(models.map((m) => Pokemon.fromPokemonApiModel(m)).toList());
135 | } on Exception catch (e) {
136 | throw Exception('Unable to delete pokemon with id $id, $e');
137 | }
138 | } else {
139 | throw UnimplementedError('Not supported for this demo');
140 | }
141 | }
142 |
143 | @override
144 | Future refresh() async {
145 | // TODO(alesalv): remove artificial delay
146 | await DemoHacksHelper.instance.artificialDelay(3000);
147 |
148 | if (await _connectivity.isConnected()) {
149 | try {
150 | final remotePokemon = await _remote.readAll();
151 | // persist into local db
152 | await _local.createAll(remotePokemon);
153 | final models = await _local.readAll();
154 | _streamController
155 | .add(models.map((m) => Pokemon.fromPokemonApiModel(m)).toList());
156 | } on Exception catch (e) {
157 | throw Exception('Unable to refresh pokemon, $e');
158 | }
159 | } else {
160 | throw UnimplementedError('Not supported for this demo');
161 | }
162 | }
163 |
164 | @override
165 | void dispose() {
166 | if (!_streamController.hasListener) {
167 | _streamController.close();
168 | }
169 | }
170 | }
171 |
172 | /// pokemonRepositoryProvider provides the pokemon repository (singleton)
173 | final pokemonRepositoryProvider = Provider>((ref) {
174 | return PokemonRepository(PokemonLocalDataSource(), PokemonRemoteDataSource(),
175 | Connectivity.instance);
176 | });
177 |
--------------------------------------------------------------------------------
/test/pokemon/models/data_sources/pokemon_local_data_source_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_arch_comp/src/pokemon/models/data/fake_pokemon.dart';
2 | import 'package:flutter_arch_comp/src/pokemon/models/data/pokemon_api_model.dart';
3 | import 'package:flutter_arch_comp/src/pokemon/models/data_sources/pokemon_local_data_source.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 | import 'package:shared_preferences/shared_preferences.dart';
6 |
7 | void main() {
8 | group('Create', () {
9 | test('should create one pokemon', () async {
10 | SharedPreferences.setMockInitialValues({});
11 |
12 | final ds = PokemonLocalDataSource();
13 | await ds.create(kFakePokemon[0]);
14 | final pokemon = await ds.readAll();
15 | expect(pokemon.length, 1);
16 | expect(pokemon[0], kFakePokemon[0]);
17 | });
18 |
19 | test('should overwrite one existing pokemon', () async {
20 | SharedPreferences.setMockInitialValues({});
21 |
22 | final ds = PokemonLocalDataSource();
23 | await ds.createAll(kFakePokemon);
24 |
25 | final updatedHeight = kFakePokemon[0].height + 70;
26 | final updated = PokemonApiModel(
27 | id: kFakePokemon[0].id,
28 | name: kFakePokemon[0].name,
29 | height: updatedHeight,
30 | weight: kFakePokemon[0].weight,
31 | image: kFakePokemon[0].image,
32 | );
33 | await ds.create(updated);
34 |
35 | expect((await ds.readAll()).length, 2);
36 | final pokemon = await ds.read(updated.id);
37 | expect(pokemon, updated);
38 | expect(pokemon!.height, updatedHeight);
39 | });
40 | });
41 |
42 | group('Create all', () {
43 | test('should create two pokemon', () async {
44 | SharedPreferences.setMockInitialValues({});
45 |
46 | final ds = PokemonLocalDataSource();
47 | await ds.createAll(kFakePokemon);
48 | final pokemon = await ds.readAll();
49 | expect(pokemon.length, 2);
50 | expect(pokemon[0], kFakePokemon[0]);
51 | expect(pokemon[1], kFakePokemon[1]);
52 | });
53 |
54 | test('should create no pokemon for empty list', () async {
55 | SharedPreferences.setMockInitialValues({});
56 |
57 | final ds = PokemonLocalDataSource();
58 | await ds.createAll([]);
59 | final pokemon = await ds.readAll();
60 | expect(pokemon.length, 0);
61 | });
62 | });
63 |
64 | group('Read', () {
65 | test('should read one pokemon', () async {
66 | SharedPreferences.setMockInitialValues({});
67 |
68 | final ds = PokemonLocalDataSource();
69 | await ds.createAll(kFakePokemon);
70 | final pokemon = await ds.read(kFakePokemon[0].id);
71 | expect(pokemon, kFakePokemon[0]);
72 | });
73 |
74 | test('should read no pokemon for non existing id', () async {
75 | SharedPreferences.setMockInitialValues({});
76 |
77 | final ds = PokemonLocalDataSource();
78 | await ds.createAll(kFakePokemon);
79 | final nonExistingId = kFakePokemon.last.id + 1;
80 | final pokemon = await ds.read(nonExistingId);
81 | expect(pokemon, null);
82 | });
83 | });
84 |
85 | group('Read all', () {
86 | test('should read two pokemon', () async {
87 | SharedPreferences.setMockInitialValues({});
88 |
89 | final ds = PokemonLocalDataSource();
90 | await ds.createAll(kFakePokemon);
91 | final pokemon = await ds.readAll();
92 | expect(pokemon.length, 2);
93 | expect(pokemon[0], kFakePokemon[0]);
94 | expect(pokemon[1], kFakePokemon[1]);
95 | });
96 |
97 | test('should read no pokemon for empty list', () async {
98 | SharedPreferences.setMockInitialValues({});
99 |
100 | final ds = PokemonLocalDataSource();
101 | await ds.createAll([]);
102 | final pokemon = await ds.readAll();
103 | expect(pokemon.length, 0);
104 | });
105 | });
106 |
107 | group('Update', () {
108 | test('should update one pokemon', () async {
109 | SharedPreferences.setMockInitialValues({});
110 |
111 | final ds = PokemonLocalDataSource();
112 | await ds.createAll(kFakePokemon);
113 |
114 | final updatedHeight = kFakePokemon[0].height + 70;
115 | final updated = PokemonApiModel(
116 | id: kFakePokemon[0].id,
117 | name: kFakePokemon[0].name,
118 | height: updatedHeight,
119 | weight: kFakePokemon[0].weight,
120 | image: kFakePokemon[0].image,
121 | );
122 | await ds.update(updated);
123 |
124 | expect((await ds.readAll()).length, 2);
125 | final pokemon = await ds.read(updated.id);
126 | expect(pokemon, updated);
127 | expect(pokemon!.height, updatedHeight);
128 | });
129 |
130 | test('should update no pokemon for non existing id', () async {
131 | SharedPreferences.setMockInitialValues({});
132 |
133 | final ds = PokemonLocalDataSource();
134 | await ds.createAll(kFakePokemon);
135 |
136 | final nonExistingId = kFakePokemon.last.id + 1;
137 | final updatedHeight = kFakePokemon[0].height + 70;
138 | final updated = PokemonApiModel(
139 | id: nonExistingId,
140 | name: kFakePokemon[0].name,
141 | height: updatedHeight,
142 | weight: kFakePokemon[0].weight,
143 | image: kFakePokemon[0].image,
144 | );
145 | await ds.update(updated);
146 |
147 | expect((await ds.readAll()).length, 2);
148 | final pokemon = await ds.read(updated.id);
149 | expect(pokemon, null);
150 | });
151 | });
152 |
153 | group('Delete', () {
154 | test('should delete one pokemon', () async {
155 | SharedPreferences.setMockInitialValues({});
156 |
157 | final ds = PokemonLocalDataSource();
158 | await ds.createAll(kFakePokemon);
159 |
160 | await ds.delete(kFakePokemon[1].id);
161 |
162 | final pokemon = await ds.readAll();
163 | expect(pokemon.length, 1);
164 | expect(pokemon[0], kFakePokemon[0]);
165 | });
166 |
167 | test('should delete no pokemon for non existing id', () async {
168 | SharedPreferences.setMockInitialValues({});
169 |
170 | final ds = PokemonLocalDataSource();
171 | await ds.createAll(kFakePokemon);
172 |
173 | final nonExistingId = kFakePokemon.last.id + 1;
174 | await ds.delete(nonExistingId);
175 |
176 | final pokemon = await ds.readAll();
177 | expect(pokemon.length, 2);
178 | expect(pokemon[0], kFakePokemon[0]);
179 | expect(pokemon[1], kFakePokemon[1]);
180 | });
181 |
182 | test('should delete no pokemon for multiple delete', () async {
183 | SharedPreferences.setMockInitialValues({});
184 |
185 | final ds = PokemonLocalDataSource();
186 | await ds.createAll(kFakePokemon);
187 |
188 | await ds.delete(kFakePokemon[1].id);
189 |
190 | final pokemonAfterOne = await ds.readAll();
191 | expect(pokemonAfterOne.length, 1);
192 | expect(pokemonAfterOne[0], kFakePokemon[0]);
193 |
194 | await ds.delete(kFakePokemon[1].id);
195 | await ds.delete(kFakePokemon[1].id);
196 | await ds.delete(kFakePokemon[1].id);
197 |
198 | final pokemonAfterMultiple = await ds.readAll();
199 | expect(pokemonAfterMultiple.length, 1);
200 | expect(pokemonAfterMultiple[0], kFakePokemon[0]);
201 | });
202 | });
203 | }
204 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
16 | B7D67E289FD0A4FF22D6B5CB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0821D9DB95CFBC55730949D0 /* Pods_Runner.framework */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXCopyFilesBuildPhase section */
20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
21 | isa = PBXCopyFilesBuildPhase;
22 | buildActionMask = 2147483647;
23 | dstPath = "";
24 | dstSubfolderSpec = 10;
25 | files = (
26 | );
27 | name = "Embed Frameworks";
28 | runOnlyForDeploymentPostprocessing = 0;
29 | };
30 | /* End PBXCopyFilesBuildPhase section */
31 |
32 | /* Begin PBXFileReference section */
33 | 0821D9DB95CFBC55730949D0 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
36 | 376DC2AD7E8D2628F939A2BF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
37 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
38 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
39 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
41 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
42 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
43 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
44 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
45 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
46 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
47 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
48 | D22A63499104BD5C4248BF71 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
49 | D3CB2177524ECECF5DBCC511 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | 97C146EB1CF9000F007C117D /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | B7D67E289FD0A4FF22D6B5CB /* Pods_Runner.framework in Frameworks */,
58 | );
59 | runOnlyForDeploymentPostprocessing = 0;
60 | };
61 | /* End PBXFrameworksBuildPhase section */
62 |
63 | /* Begin PBXGroup section */
64 | 1ABE9800A2953A8DC063BFEE /* Frameworks */ = {
65 | isa = PBXGroup;
66 | children = (
67 | 0821D9DB95CFBC55730949D0 /* Pods_Runner.framework */,
68 | );
69 | name = Frameworks;
70 | sourceTree = "";
71 | };
72 | 9740EEB11CF90186004384FC /* Flutter */ = {
73 | isa = PBXGroup;
74 | children = (
75 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
76 | 9740EEB21CF90195004384FC /* Debug.xcconfig */,
77 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
78 | 9740EEB31CF90195004384FC /* Generated.xcconfig */,
79 | );
80 | name = Flutter;
81 | sourceTree = "";
82 | };
83 | 97C146E51CF9000F007C117D = {
84 | isa = PBXGroup;
85 | children = (
86 | 9740EEB11CF90186004384FC /* Flutter */,
87 | 97C146F01CF9000F007C117D /* Runner */,
88 | 97C146EF1CF9000F007C117D /* Products */,
89 | A0174C8C79A555EEE12CCEA7 /* Pods */,
90 | 1ABE9800A2953A8DC063BFEE /* Frameworks */,
91 | );
92 | sourceTree = "";
93 | };
94 | 97C146EF1CF9000F007C117D /* Products */ = {
95 | isa = PBXGroup;
96 | children = (
97 | 97C146EE1CF9000F007C117D /* Runner.app */,
98 | );
99 | name = Products;
100 | sourceTree = "";
101 | };
102 | 97C146F01CF9000F007C117D /* Runner */ = {
103 | isa = PBXGroup;
104 | children = (
105 | 97C146FA1CF9000F007C117D /* Main.storyboard */,
106 | 97C146FD1CF9000F007C117D /* Assets.xcassets */,
107 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
108 | 97C147021CF9000F007C117D /* Info.plist */,
109 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
110 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
111 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
112 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
113 | );
114 | path = Runner;
115 | sourceTree = "";
116 | };
117 | A0174C8C79A555EEE12CCEA7 /* Pods */ = {
118 | isa = PBXGroup;
119 | children = (
120 | D22A63499104BD5C4248BF71 /* Pods-Runner.debug.xcconfig */,
121 | 376DC2AD7E8D2628F939A2BF /* Pods-Runner.release.xcconfig */,
122 | D3CB2177524ECECF5DBCC511 /* Pods-Runner.profile.xcconfig */,
123 | );
124 | name = Pods;
125 | path = Pods;
126 | sourceTree = "";
127 | };
128 | /* End PBXGroup section */
129 |
130 | /* Begin PBXNativeTarget section */
131 | 97C146ED1CF9000F007C117D /* Runner */ = {
132 | isa = PBXNativeTarget;
133 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
134 | buildPhases = (
135 | 76512DF0DE85C06B0DA02902 /* [CP] Check Pods Manifest.lock */,
136 | 9740EEB61CF901F6004384FC /* Run Script */,
137 | 97C146EA1CF9000F007C117D /* Sources */,
138 | 97C146EB1CF9000F007C117D /* Frameworks */,
139 | 97C146EC1CF9000F007C117D /* Resources */,
140 | 9705A1C41CF9048500538489 /* Embed Frameworks */,
141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
142 | A7C125D7D793946C7C53C67D /* [CP] Embed Pods Frameworks */,
143 | );
144 | buildRules = (
145 | );
146 | dependencies = (
147 | );
148 | name = Runner;
149 | productName = Runner;
150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
151 | productType = "com.apple.product-type.application";
152 | };
153 | /* End PBXNativeTarget section */
154 |
155 | /* Begin PBXProject section */
156 | 97C146E61CF9000F007C117D /* Project object */ = {
157 | isa = PBXProject;
158 | attributes = {
159 | LastUpgradeCheck = 1510;
160 | ORGANIZATIONNAME = "";
161 | TargetAttributes = {
162 | 97C146ED1CF9000F007C117D = {
163 | CreatedOnToolsVersion = 7.3.1;
164 | LastSwiftMigration = 1100;
165 | };
166 | };
167 | };
168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
169 | compatibilityVersion = "Xcode 9.3";
170 | developmentRegion = en;
171 | hasScannedForEncodings = 0;
172 | knownRegions = (
173 | en,
174 | Base,
175 | );
176 | mainGroup = 97C146E51CF9000F007C117D;
177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
178 | projectDirPath = "";
179 | projectRoot = "";
180 | targets = (
181 | 97C146ED1CF9000F007C117D /* Runner */,
182 | );
183 | };
184 | /* End PBXProject section */
185 |
186 | /* Begin PBXResourcesBuildPhase section */
187 | 97C146EC1CF9000F007C117D /* Resources */ = {
188 | isa = PBXResourcesBuildPhase;
189 | buildActionMask = 2147483647;
190 | files = (
191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
195 | );
196 | runOnlyForDeploymentPostprocessing = 0;
197 | };
198 | /* End PBXResourcesBuildPhase section */
199 |
200 | /* Begin PBXShellScriptBuildPhase section */
201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
202 | isa = PBXShellScriptBuildPhase;
203 | alwaysOutOfDate = 1;
204 | buildActionMask = 2147483647;
205 | files = (
206 | );
207 | inputPaths = (
208 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
209 | );
210 | name = "Thin Binary";
211 | outputPaths = (
212 | );
213 | runOnlyForDeploymentPostprocessing = 0;
214 | shellPath = /bin/sh;
215 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
216 | };
217 | 76512DF0DE85C06B0DA02902 /* [CP] Check Pods Manifest.lock */ = {
218 | isa = PBXShellScriptBuildPhase;
219 | buildActionMask = 2147483647;
220 | files = (
221 | );
222 | inputFileListPaths = (
223 | );
224 | inputPaths = (
225 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
226 | "${PODS_ROOT}/Manifest.lock",
227 | );
228 | name = "[CP] Check Pods Manifest.lock";
229 | outputFileListPaths = (
230 | );
231 | outputPaths = (
232 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
233 | );
234 | runOnlyForDeploymentPostprocessing = 0;
235 | shellPath = /bin/sh;
236 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
237 | showEnvVarsInLog = 0;
238 | };
239 | 9740EEB61CF901F6004384FC /* Run Script */ = {
240 | isa = PBXShellScriptBuildPhase;
241 | alwaysOutOfDate = 1;
242 | buildActionMask = 2147483647;
243 | files = (
244 | );
245 | inputPaths = (
246 | );
247 | name = "Run Script";
248 | outputPaths = (
249 | );
250 | runOnlyForDeploymentPostprocessing = 0;
251 | shellPath = /bin/sh;
252 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
253 | };
254 | A7C125D7D793946C7C53C67D /* [CP] Embed Pods Frameworks */ = {
255 | isa = PBXShellScriptBuildPhase;
256 | buildActionMask = 2147483647;
257 | files = (
258 | );
259 | inputFileListPaths = (
260 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
261 | );
262 | name = "[CP] Embed Pods Frameworks";
263 | outputFileListPaths = (
264 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
265 | );
266 | runOnlyForDeploymentPostprocessing = 0;
267 | shellPath = /bin/sh;
268 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
269 | showEnvVarsInLog = 0;
270 | };
271 | /* End PBXShellScriptBuildPhase section */
272 |
273 | /* Begin PBXSourcesBuildPhase section */
274 | 97C146EA1CF9000F007C117D /* Sources */ = {
275 | isa = PBXSourcesBuildPhase;
276 | buildActionMask = 2147483647;
277 | files = (
278 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
279 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
280 | );
281 | runOnlyForDeploymentPostprocessing = 0;
282 | };
283 | /* End PBXSourcesBuildPhase section */
284 |
285 | /* Begin PBXVariantGroup section */
286 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
287 | isa = PBXVariantGroup;
288 | children = (
289 | 97C146FB1CF9000F007C117D /* Base */,
290 | );
291 | name = Main.storyboard;
292 | sourceTree = "";
293 | };
294 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
295 | isa = PBXVariantGroup;
296 | children = (
297 | 97C147001CF9000F007C117D /* Base */,
298 | );
299 | name = LaunchScreen.storyboard;
300 | sourceTree = "";
301 | };
302 | /* End PBXVariantGroup section */
303 |
304 | /* Begin XCBuildConfiguration section */
305 | 249021D3217E4FDB00AE95B9 /* Profile */ = {
306 | isa = XCBuildConfiguration;
307 | buildSettings = {
308 | ALWAYS_SEARCH_USER_PATHS = NO;
309 | CLANG_ANALYZER_NONNULL = YES;
310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
311 | CLANG_CXX_LIBRARY = "libc++";
312 | CLANG_ENABLE_MODULES = YES;
313 | CLANG_ENABLE_OBJC_ARC = YES;
314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
315 | CLANG_WARN_BOOL_CONVERSION = YES;
316 | CLANG_WARN_COMMA = YES;
317 | CLANG_WARN_CONSTANT_CONVERSION = YES;
318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
320 | CLANG_WARN_EMPTY_BODY = YES;
321 | CLANG_WARN_ENUM_CONVERSION = YES;
322 | CLANG_WARN_INFINITE_RECURSION = YES;
323 | CLANG_WARN_INT_CONVERSION = YES;
324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
329 | CLANG_WARN_STRICT_PROTOTYPES = YES;
330 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
331 | CLANG_WARN_UNREACHABLE_CODE = YES;
332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
334 | COPY_PHASE_STRIP = NO;
335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
336 | ENABLE_NS_ASSERTIONS = NO;
337 | ENABLE_STRICT_OBJC_MSGSEND = YES;
338 | GCC_C_LANGUAGE_STANDARD = gnu99;
339 | GCC_NO_COMMON_BLOCKS = YES;
340 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
341 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
342 | GCC_WARN_UNDECLARED_SELECTOR = YES;
343 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
344 | GCC_WARN_UNUSED_FUNCTION = YES;
345 | GCC_WARN_UNUSED_VARIABLE = YES;
346 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
347 | MTL_ENABLE_DEBUG_INFO = NO;
348 | SDKROOT = iphoneos;
349 | SUPPORTED_PLATFORMS = iphoneos;
350 | TARGETED_DEVICE_FAMILY = "1,2";
351 | VALIDATE_PRODUCT = YES;
352 | };
353 | name = Profile;
354 | };
355 | 249021D4217E4FDB00AE95B9 /* Profile */ = {
356 | isa = XCBuildConfiguration;
357 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
358 | buildSettings = {
359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
360 | CLANG_ENABLE_MODULES = YES;
361 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
362 | ENABLE_BITCODE = NO;
363 | INFOPLIST_FILE = Runner/Info.plist;
364 | LD_RUNPATH_SEARCH_PATHS = (
365 | "$(inherited)",
366 | "@executable_path/Frameworks",
367 | );
368 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterArchComp;
369 | PRODUCT_NAME = "$(TARGET_NAME)";
370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
371 | SWIFT_VERSION = 5.0;
372 | VERSIONING_SYSTEM = "apple-generic";
373 | };
374 | name = Profile;
375 | };
376 | 97C147031CF9000F007C117D /* Debug */ = {
377 | isa = XCBuildConfiguration;
378 | buildSettings = {
379 | ALWAYS_SEARCH_USER_PATHS = NO;
380 | CLANG_ANALYZER_NONNULL = YES;
381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
382 | CLANG_CXX_LIBRARY = "libc++";
383 | CLANG_ENABLE_MODULES = YES;
384 | CLANG_ENABLE_OBJC_ARC = YES;
385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
386 | CLANG_WARN_BOOL_CONVERSION = YES;
387 | CLANG_WARN_COMMA = YES;
388 | CLANG_WARN_CONSTANT_CONVERSION = YES;
389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
391 | CLANG_WARN_EMPTY_BODY = YES;
392 | CLANG_WARN_ENUM_CONVERSION = YES;
393 | CLANG_WARN_INFINITE_RECURSION = YES;
394 | CLANG_WARN_INT_CONVERSION = YES;
395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
400 | CLANG_WARN_STRICT_PROTOTYPES = YES;
401 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
402 | CLANG_WARN_UNREACHABLE_CODE = YES;
403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
405 | COPY_PHASE_STRIP = NO;
406 | DEBUG_INFORMATION_FORMAT = dwarf;
407 | ENABLE_STRICT_OBJC_MSGSEND = YES;
408 | ENABLE_TESTABILITY = YES;
409 | GCC_C_LANGUAGE_STANDARD = gnu99;
410 | GCC_DYNAMIC_NO_PIC = NO;
411 | GCC_NO_COMMON_BLOCKS = YES;
412 | GCC_OPTIMIZATION_LEVEL = 0;
413 | GCC_PREPROCESSOR_DEFINITIONS = (
414 | "DEBUG=1",
415 | "$(inherited)",
416 | );
417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
419 | GCC_WARN_UNDECLARED_SELECTOR = YES;
420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
421 | GCC_WARN_UNUSED_FUNCTION = YES;
422 | GCC_WARN_UNUSED_VARIABLE = YES;
423 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
424 | MTL_ENABLE_DEBUG_INFO = YES;
425 | ONLY_ACTIVE_ARCH = YES;
426 | SDKROOT = iphoneos;
427 | TARGETED_DEVICE_FAMILY = "1,2";
428 | };
429 | name = Debug;
430 | };
431 | 97C147041CF9000F007C117D /* Release */ = {
432 | isa = XCBuildConfiguration;
433 | buildSettings = {
434 | ALWAYS_SEARCH_USER_PATHS = NO;
435 | CLANG_ANALYZER_NONNULL = YES;
436 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
437 | CLANG_CXX_LIBRARY = "libc++";
438 | CLANG_ENABLE_MODULES = YES;
439 | CLANG_ENABLE_OBJC_ARC = YES;
440 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
441 | CLANG_WARN_BOOL_CONVERSION = YES;
442 | CLANG_WARN_COMMA = YES;
443 | CLANG_WARN_CONSTANT_CONVERSION = YES;
444 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
445 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
446 | CLANG_WARN_EMPTY_BODY = YES;
447 | CLANG_WARN_ENUM_CONVERSION = YES;
448 | CLANG_WARN_INFINITE_RECURSION = YES;
449 | CLANG_WARN_INT_CONVERSION = YES;
450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
455 | CLANG_WARN_STRICT_PROTOTYPES = YES;
456 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
457 | CLANG_WARN_UNREACHABLE_CODE = YES;
458 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
459 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
460 | COPY_PHASE_STRIP = NO;
461 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
462 | ENABLE_NS_ASSERTIONS = NO;
463 | ENABLE_STRICT_OBJC_MSGSEND = YES;
464 | GCC_C_LANGUAGE_STANDARD = gnu99;
465 | GCC_NO_COMMON_BLOCKS = YES;
466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
467 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
468 | GCC_WARN_UNDECLARED_SELECTOR = YES;
469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
470 | GCC_WARN_UNUSED_FUNCTION = YES;
471 | GCC_WARN_UNUSED_VARIABLE = YES;
472 | IPHONEOS_DEPLOYMENT_TARGET = 12.0;
473 | MTL_ENABLE_DEBUG_INFO = NO;
474 | SDKROOT = iphoneos;
475 | SUPPORTED_PLATFORMS = iphoneos;
476 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
477 | TARGETED_DEVICE_FAMILY = "1,2";
478 | VALIDATE_PRODUCT = YES;
479 | };
480 | name = Release;
481 | };
482 | 97C147061CF9000F007C117D /* Debug */ = {
483 | isa = XCBuildConfiguration;
484 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
485 | buildSettings = {
486 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
487 | CLANG_ENABLE_MODULES = YES;
488 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
489 | ENABLE_BITCODE = NO;
490 | INFOPLIST_FILE = Runner/Info.plist;
491 | LD_RUNPATH_SEARCH_PATHS = (
492 | "$(inherited)",
493 | "@executable_path/Frameworks",
494 | );
495 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterArchComp;
496 | PRODUCT_NAME = "$(TARGET_NAME)";
497 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
498 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
499 | SWIFT_VERSION = 5.0;
500 | VERSIONING_SYSTEM = "apple-generic";
501 | };
502 | name = Debug;
503 | };
504 | 97C147071CF9000F007C117D /* Release */ = {
505 | isa = XCBuildConfiguration;
506 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
507 | buildSettings = {
508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
509 | CLANG_ENABLE_MODULES = YES;
510 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
511 | ENABLE_BITCODE = NO;
512 | INFOPLIST_FILE = Runner/Info.plist;
513 | LD_RUNPATH_SEARCH_PATHS = (
514 | "$(inherited)",
515 | "@executable_path/Frameworks",
516 | );
517 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterArchComp;
518 | PRODUCT_NAME = "$(TARGET_NAME)";
519 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
520 | SWIFT_VERSION = 5.0;
521 | VERSIONING_SYSTEM = "apple-generic";
522 | };
523 | name = Release;
524 | };
525 | /* End XCBuildConfiguration section */
526 |
527 | /* Begin XCConfigurationList section */
528 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
529 | isa = XCConfigurationList;
530 | buildConfigurations = (
531 | 97C147031CF9000F007C117D /* Debug */,
532 | 97C147041CF9000F007C117D /* Release */,
533 | 249021D3217E4FDB00AE95B9 /* Profile */,
534 | );
535 | defaultConfigurationIsVisible = 0;
536 | defaultConfigurationName = Release;
537 | };
538 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
539 | isa = XCConfigurationList;
540 | buildConfigurations = (
541 | 97C147061CF9000F007C117D /* Debug */,
542 | 97C147071CF9000F007C117D /* Release */,
543 | 249021D4217E4FDB00AE95B9 /* Profile */,
544 | );
545 | defaultConfigurationIsVisible = 0;
546 | defaultConfigurationName = Release;
547 | };
548 | /* End XCConfigurationList section */
549 | };
550 | rootObject = 97C146E61CF9000F007C117D /* Project object */;
551 | }
552 |
--------------------------------------------------------------------------------
/macos/Runner/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
--------------------------------------------------------------------------------