├── .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 | void -------------------------------------------------------------------------------- /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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | --------------------------------------------------------------------------------