├── .fvm └── fvm_config.json ├── .gitignore ├── .metadata ├── .run └── All Tests.run.xml ├── Makefile ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── appflate │ │ │ │ └── flutter_demo │ │ │ │ └── flutter_demo │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── images │ ├── 2.0x │ └── logo.webp │ ├── 3.0x │ └── logo.webp │ └── logo.webp ├── dart_test.yaml ├── docs ├── example.png ├── modules.png └── modules_details.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── l10n.yaml ├── lib ├── core │ ├── domain │ │ ├── model │ │ │ ├── app_init_failure.dart │ │ │ ├── displayable_failure.dart │ │ │ └── user.dart │ │ ├── stores │ │ │ └── user_store.dart │ │ └── use_cases │ │ │ └── app_init_use_case.dart │ ├── helpers.dart │ └── utils │ │ ├── bloc_extensions.dart │ │ ├── current_time_provider.dart │ │ ├── debouncer.dart │ │ ├── durations.dart │ │ ├── either_extensions.dart │ │ ├── logging.dart │ │ ├── mvp_extensions.dart │ │ └── periodic_task_executor.dart ├── dependency_injection │ └── app_component.dart ├── features │ ├── app_init │ │ ├── app_init_initial_params.dart │ │ ├── app_init_navigator.dart │ │ ├── app_init_page.dart │ │ ├── app_init_presentation_model.dart │ │ ├── app_init_presenter.dart │ │ └── dependency_injection │ │ │ └── feature_component.dart │ └── auth │ │ ├── dependency_injection │ │ └── feature_component.dart │ │ ├── domain │ │ ├── model │ │ │ └── log_in_failure.dart │ │ └── use_cases │ │ │ └── log_in_use_case.dart │ │ └── login │ │ ├── login_initial_params.dart │ │ ├── login_navigator.dart │ │ ├── login_page.dart │ │ ├── login_presentation_model.dart │ │ └── login_presenter.dart ├── flutter_demo_app.dart ├── l10n │ └── intl_en.arb ├── localization │ ├── app_en.arb │ └── app_localizations_utils.dart ├── main.dart ├── navigation │ ├── alert_dialog_route.dart │ ├── app_navigator.dart │ ├── close_route.dart │ ├── close_with_result_route.dart │ ├── error_dialog_route.dart │ ├── no_routes.dart │ └── transitions │ │ ├── fade_in_page_transition.dart │ │ └── slide_bottom_page_transition.dart ├── resources │ └── assets.gen.dart └── utils │ └── locale_resolution.dart ├── pubspec.lock ├── pubspec.yaml ├── templates ├── mason-lock.json ├── mason.yaml ├── page │ ├── README.md │ ├── __brick__ │ │ ├── {{{initial_params_absolute_path}}} │ │ ├── {{{navigator_absolute_path}}} │ │ ├── {{{page_absolute_path}}} │ │ ├── {{{page_test_absolute_path}}} │ │ ├── {{{presentation_model_absolute_path}}} │ │ ├── {{{presenter_absolute_path}}} │ │ └── {{{presenter_test_absolute_path}}} │ ├── brick.yaml │ └── hooks │ │ ├── .gitignore │ │ ├── post_gen.dart │ │ ├── pre_gen.dart │ │ └── pubspec.yaml ├── repository │ ├── README.md │ ├── __brick__ │ │ ├── {{{implementation_absolute_path}}} │ │ └── {{{interface_absolute_path}}} │ ├── brick.yaml │ └── hooks │ │ ├── .gitignore │ │ ├── post_gen.dart │ │ ├── pre_gen.dart │ │ └── pubspec.yaml ├── template_utils │ ├── .gitignore │ ├── README.md │ ├── analysis_options.yaml │ ├── lib │ │ ├── feature_templates.dart │ │ ├── file_utils.dart │ │ └── template_utils.dart │ ├── pubspec.lock │ └── pubspec.yaml └── use_case │ ├── README.md │ ├── __brick__ │ ├── {{{failure_absolute_path}}} │ ├── {{{use_case_absolute_path}}} │ └── {{{use_case_test_absolute_path}}} │ ├── brick.yaml │ └── hooks │ ├── .gitignore │ ├── analysis_options.yaml │ ├── post_gen.dart │ ├── pre_gen.dart │ └── pubspec.yaml ├── test ├── features │ ├── app_init │ │ ├── domain │ │ │ └── app_init_use_case_test.dart │ │ ├── mocks │ │ │ ├── app_init_mock_definitions.dart │ │ │ └── app_init_mocks.dart │ │ ├── pages │ │ │ ├── app_init_page_test.dart │ │ │ ├── flutter_test_config.dart │ │ │ └── goldens │ │ │ │ ├── ci │ │ │ │ └── app_init_page.png │ │ │ │ └── macos │ │ │ │ └── app_init_page.png │ │ └── presenters │ │ │ └── app_init_presenter_test.dart │ └── auth │ │ ├── domain │ │ └── log_in_use_case_test.dart │ │ ├── mocks │ │ ├── auth_mock_definitions.dart │ │ └── auth_mocks.dart │ │ ├── pages │ │ ├── flutter_test_config.dart │ │ ├── goldens │ │ │ ├── ci │ │ │ │ └── login_page.png │ │ │ └── macos │ │ │ │ └── login_page.png │ │ └── login_page_test.dart │ │ └── presenters │ │ └── login_presenter_test.dart ├── flutter_test_config.dart ├── mocks │ ├── mock_definitions.dart │ └── mocks.dart └── test_utils │ ├── golden_test_device_scenario.dart │ ├── golden_tests_utils.dart │ └── test_utils.dart └── tools ├── arb_files_validator └── bin │ └── arb_files_validator.dart └── custom_lints └── clean_architecture_lints ├── analysis_options.yaml ├── bin ├── custom_lint.dart ├── lints │ ├── domain_entity_missing_copy_with_method_lint.dart │ ├── domain_entity_missing_empty_constructor_lint.dart │ ├── domain_entity_missing_equatable_lint.dart │ ├── domain_entity_missing_props_items_lint.dart │ ├── domain_entity_non_final_fields_lint.dart │ ├── domain_entity_too_many_public_members_lint.dart │ ├── dont_use_datetime_now_lint.dart │ ├── forbidden_import_in_domain_lint.dart │ ├── forbidden_import_in_presentation_lint.dart │ ├── page_too_widgets.dart │ ├── presentation_model_non_final_field_lint.dart │ ├── presentation_model_structure_lint.dart │ └── use_case_multiple_accessors_lint.dart └── utils │ ├── lint_codes.dart │ └── lint_utils.dart ├── pubspec.lock └── pubspec.yaml /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.0.5", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .vscode/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | .fvm/flutter_sdk 20 | 21 | lib/generated/ 22 | 23 | **/*.g.dart 24 | **/*.freezed.dart 25 | **/*.gql.dart 26 | **/*.gr.dart 27 | 28 | **/__generated__/ 29 | 30 | # The .vscode folder contains launch configuration and tasks you configure in 31 | # VS Code which you may wish to be included in version control, so this line 32 | # is commented out by default. 33 | #.vscode/ 34 | 35 | # Flutter/Dart/Pub related 36 | **/doc/api/ 37 | **/ios/Flutter/.last_build_id 38 | .dart_tool/ 39 | .flutter-plugins 40 | .flutter-plugins-dependencies 41 | .packages 42 | .pub-cache/ 43 | .pub/ 44 | /build/ 45 | 46 | # Web related 47 | lib/generated_plugin_registrant.dart 48 | 49 | # Symbolication related 50 | app.*.symbols 51 | 52 | # Obfuscation related 53 | app.*.map.json 54 | 55 | # Android Studio will place build artifacts here 56 | /android/app/debug 57 | /android/app/profile 58 | /android/app/release 59 | 60 | # project-specific 61 | .mason 62 | /macos 63 | /test/**/goldens/windows 64 | /test/**/goldens/linux 65 | /test/**/failures/*.png 66 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f1875d570e39de09040c8f79aa13cc56baab8db1 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 17 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 18 | - platform: ios 19 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 20 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /.run/All Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | package := flutter_app 2 | file := test/coverage_helper_test.dart 3 | 4 | check: 5 | @echo "\033[32m Run fluttergen... \033[0m" 6 | @fluttergen -c pubspec.yaml 7 | @echo "\033[32m Formatting code... \033[0m" 8 | @fvm flutter format --line-length 120 lib test 9 | @echo "\033[32m Validate localization files... \033[0m" 10 | @fvm dart tools/arb_files_validator/bin/arb_files_validator.dart lib/localization/ 11 | @echo "\033[32m Flutter analyze... \033[0m" 12 | @fvm flutter analyze 13 | @echo "\033[32m Flutter clean architecture lints... \033[0m" 14 | @fvm flutter pub get ; pushd tools/custom_lints/clean_architecture_lints ; fvm flutter pub get ; popd 15 | @fvm flutter pub run custom_lint 16 | @echo "\033[32m Removing all golden files... \033[0m" 17 | @find ./test -name '*.png' | xargs rm -r 18 | @echo "\033[32m Flutter test --update-goldens... \033[0m" 19 | @fvm flutter test --update-goldens 20 | @echo "\033[32m Code metrics analyze: \033[0m" 21 | @fvm flutter pub run dart_code_metrics:metrics analyze lib --set-exit-on-violation-level=warning --fatal-style --fatal-performance --fatal-warnings 22 | @echo "\033[32m Code metrics check-unused-code: \033[0m" 23 | @fvm flutter pub run dart_code_metrics:metrics check-unused-code . --fatal-unused 24 | @echo "\033[32m Code metrics check-unused-files: \033[0m" 25 | @fvm flutter pub run dart_code_metrics:metrics check-unused-files . --fatal-unused --exclude="{templates/**,.dart_tool/**,lib/generated/**}" 26 | @echo "\033[1;32m \n\nGOOD JOB, THE CODE IS SPOTLESS CLEAN AND READY FOR PULL REQUEST! \n \033[42m Make sure to commit any code changes \033[0m \n\n" -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "io.appflate.flutter_demo.flutter_demo" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/appflate/flutter_demo/flutter_demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.appflate.flutter_demo.flutter_demo 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #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-7.4-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/images/2.0x/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/assets/images/2.0x/logo.webp -------------------------------------------------------------------------------- /assets/images/3.0x/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/assets/images/3.0x/logo.webp -------------------------------------------------------------------------------- /assets/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/assets/images/logo.webp -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | golden: -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/docs/example.png -------------------------------------------------------------------------------- /docs/modules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/docs/modules.png -------------------------------------------------------------------------------- /docs/modules_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/docs/modules_details.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/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/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_demo 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/localization 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart 4 | -------------------------------------------------------------------------------- /lib/core/domain/model/app_init_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart'; 2 | 3 | class AppInitFailure implements HasDisplayableFailure { 4 | // ignore: avoid_field_initializers_in_const_classes 5 | const AppInitFailure.unknown([this.cause]) : type = AppInitFailureType.Unknown; 6 | 7 | final AppInitFailureType type; 8 | final Object? cause; 9 | 10 | @override 11 | DisplayableFailure displayableFailure() { 12 | switch (type) { 13 | case AppInitFailureType.Unknown: 14 | return DisplayableFailure.commonError(); 15 | } 16 | } 17 | 18 | @override 19 | String toString() => 'AppInitFailure{type: $type, cause: $cause}'; 20 | } 21 | 22 | enum AppInitFailureType { 23 | Unknown, 24 | } 25 | -------------------------------------------------------------------------------- /lib/core/domain/model/displayable_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 2 | 3 | /// A failure with the title and message that could be easly displayed as a dialog or snackbar 4 | class DisplayableFailure { 5 | DisplayableFailure({ 6 | required this.title, 7 | required this.message, 8 | }); 9 | 10 | DisplayableFailure.commonError([String? message]) 11 | : title = appLocalizations.commonErrorTitle, 12 | // TODO move this to strings file 13 | message = message ?? appLocalizations.commonErrorMessage; 14 | 15 | String title; 16 | String message; 17 | } 18 | 19 | abstract class HasDisplayableFailure { 20 | DisplayableFailure displayableFailure(); 21 | } 22 | -------------------------------------------------------------------------------- /lib/core/domain/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class User extends Equatable { 4 | const User({ 5 | required this.id, 6 | required this.username, 7 | }); 8 | 9 | const User.anonymous() 10 | : id = '', 11 | username = ''; 12 | 13 | const User.empty() : this.anonymous(); 14 | 15 | final String id; 16 | final String username; 17 | 18 | @override 19 | List get props => [ 20 | id, 21 | username, 22 | ]; 23 | 24 | User copyWith({ 25 | String? id, 26 | String? username, 27 | }) { 28 | return User( 29 | id: id ?? this.id, 30 | username: username ?? this.username, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/core/domain/stores/user_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_demo/core/domain/model/user.dart'; 3 | 4 | class UserStore extends Cubit { 5 | UserStore({User? user}) : super(user ?? const User.anonymous()); 6 | 7 | User get user => state; 8 | 9 | set user(User user) { 10 | emit(user); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/domain/use_cases/app_init_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart'; 3 | import 'package:flutter_demo/core/utils/either_extensions.dart'; 4 | 5 | /// Takes care of the entire app initialization, populating stores with values, setting up the state the entire app 6 | class AppInitUseCase { 7 | const AppInitUseCase(); 8 | 9 | Future> execute() async { 10 | // TODO add app initialization code here, like loading user data from local storage etc. 11 | await Future.delayed(const Duration(seconds: 2)); 12 | return success(unit); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/core/utils/logging.dart'; 3 | import 'package:flutter_demo/navigation/app_navigator.dart'; 4 | 5 | /// does... nothing ;) 6 | T? doNothing() { 7 | return null; 8 | } 9 | 10 | /// shows dialog with "not implemented" message. its var so that it can be replaced for tests 11 | void Function({BuildContext? context, String? message}) notImplemented = ({String? message, BuildContext? context}) { 12 | logError(UnimplementedError('not implemented${message == null ? '' : ':\n$message'}'), StackTrace.current); 13 | showDialog( 14 | context: context ?? AppNavigator.navigatorKey.currentContext!, 15 | builder: (context) => AlertDialog( 16 | title: const Text('Not implemented'), 17 | content: Text(message ?? "This feature is not yet implemented"), 18 | actions: [ 19 | TextButton( 20 | onPressed: () => Navigator.of(context).pop(), 21 | child: const Text('OK'), 22 | ), 23 | ], 24 | ), 25 | ); 26 | }; 27 | 28 | /// method that allows suppressing the `unused-code` metric 29 | /// https://github.com/dart-code-checker/dart-code-metrics/pull/929 30 | void suppressUnusedCodeWarning(dynamic anything) => doNothing(); 31 | -------------------------------------------------------------------------------- /lib/core/utils/bloc_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | //ignore:prefer-match-file-name 4 | enum FutureStatus { 5 | notStarted, 6 | pending, 7 | fulfilled, 8 | rejected, 9 | } 10 | 11 | class FutureResult extends Equatable { 12 | const FutureResult( 13 | this.result, 14 | this.status, 15 | this.error, 16 | ); 17 | 18 | const FutureResult.empty() 19 | : result = null, 20 | error = null, 21 | status = FutureStatus.notStarted; 22 | 23 | const FutureResult.pending() 24 | : result = null, 25 | error = null, 26 | status = FutureStatus.pending; 27 | 28 | final T? result; 29 | final FutureStatus status; 30 | final dynamic error; 31 | 32 | @override 33 | List get props => [ 34 | result, 35 | status, 36 | error, 37 | ]; 38 | 39 | bool isPending() => status == FutureStatus.pending; 40 | } 41 | 42 | extension AsObservableFuture on Future { 43 | Future observeStatusChanges(void Function(FutureResult) onChange) { 44 | onChange( 45 | //ignore: prefer-trailing-comma 46 | const FutureResult(null, FutureStatus.pending, null), 47 | ); 48 | 49 | return then((value) { 50 | onChange( 51 | //ignore: prefer-trailing-comma 52 | FutureResult(value, FutureStatus.fulfilled, null), 53 | ); 54 | 55 | return value; 56 | }).catchError((error) { 57 | onChange( 58 | //ignore: prefer-trailing-comma 59 | FutureResult(null, FutureStatus.rejected, error), 60 | ); 61 | 62 | throw error as Object; 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/core/utils/current_time_provider.dart: -------------------------------------------------------------------------------- 1 | /// used to retrieve current time, abstracting away the logic behind it. should be used instead of `DateTime.now()` 2 | // ignore_for_file: no_date_time_now 3 | class CurrentTimeProvider { 4 | DateTime get currentTime => DateTime.now(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/core/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// Used to debounce the operation. i.e operation is scheduled to run in the future, 4 | /// if there is a new operation scheduled to run before the previous is executed, 5 | /// the previous one gets cancelled and the timer is reset 6 | /// useful for example to perform action few milliseconds after user stops typing text 7 | class Debouncer { 8 | Timer? _timer; 9 | 10 | void debounce(Duration duration, dynamic Function() func) { 11 | _timer?.cancel(); 12 | _timer = Timer(duration, func); 13 | } 14 | 15 | void cancel() { 16 | _timer?.cancel(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/core/utils/durations.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused-code 2 | class Durations { 3 | static const long = 500; 4 | static const medium = 300; 5 | static const short = 150; 6 | static const extraShort = 80; 7 | } 8 | 9 | class LongDuration extends Duration { 10 | const LongDuration() : super(milliseconds: Durations.long); 11 | } 12 | 13 | class MediumDuration extends Duration { 14 | const MediumDuration() : super(milliseconds: Durations.medium); 15 | } 16 | 17 | class ShortDuration extends Duration { 18 | const ShortDuration() : super(milliseconds: Durations.short); 19 | } 20 | 21 | class ExtraShortDuration extends Duration { 22 | const ExtraShortDuration() : super(milliseconds: Durations.extraShort); 23 | } 24 | -------------------------------------------------------------------------------- /lib/core/utils/either_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_demo/core/helpers.dart'; 3 | import 'package:flutter_demo/core/utils/logging.dart'; 4 | 5 | Either success(R r) => right(r); 6 | 7 | Either failure(L l) => left(l); 8 | 9 | extension EitherExtensions on Either { 10 | bool get isFailure => isLeft(); 11 | 12 | bool get isSuccess => isRight(); 13 | 14 | R? getSuccess() => fold((l) => null, (r) => r); 15 | 16 | L? getFailure() => fold((l) => l, (r) => null); 17 | } 18 | 19 | extension FutureEither on Future> { 20 | Future> flatMap(Function1>> f) { 21 | return then( 22 | (either1) => either1.fold( 23 | (l) => Future.value(left(l)), 24 | f, 25 | ), 26 | ); 27 | } 28 | 29 | Future> leftMap(Function1> f) { 30 | return then( 31 | (either1) => either1.fold( 32 | (l) => Future.value(f(l)), 33 | (r) => Future.value(right(r)), 34 | ), 35 | ); 36 | } 37 | 38 | Future> map(Function1> f) { 39 | return then( 40 | (either1) => either1.fold( 41 | (l) => Future.value(left(l)), 42 | (r) => Future.value(f(r)), 43 | ), 44 | ); 45 | } 46 | 47 | Future> mapFailure(L2 Function(L fail) errorMapper) async { 48 | return (await this).leftMap(errorMapper); 49 | } 50 | 51 | Future> mapSuccess(R2 Function(R response) responseMapper) async { 52 | return (await this).map(responseMapper); 53 | } 54 | 55 | Future> doOn({ 56 | void Function(L fail)? fail, 57 | void Function(R success)? success, 58 | }) async { 59 | try { 60 | (await this).fold( 61 | fail ?? (_) => doNothing(), 62 | success ?? (_) => doNothing(), 63 | ); 64 | return this; 65 | } catch (e, stack) { 66 | logError(e, stack); 67 | rethrow; 68 | } 69 | } 70 | 71 | Future asyncFold( 72 | R2 Function(L fail) fail, 73 | R2 Function(R success) success, 74 | ) async => 75 | (await this).fold(fail, success); 76 | 77 | Future getOrThrow() async => asyncFold( 78 | (l) => throw l as Object, 79 | (r) => r, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /lib/core/utils/logging.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | /// allows for specifying global error logger for the app. you can replace it with something 'harmless' for testing 4 | // ignore: prefer_function_declarations_over_variables 5 | void Function( 6 | dynamic error, 7 | dynamic stack, 8 | String? reason, 9 | ) errorLogger = ( 10 | error, 11 | stack, 12 | reason, 13 | ) { 14 | // TODO add firebase crashlytics or equivalent logging here 15 | debugLog( 16 | 'ERROR ${reason == null ? '' : ': $reason'}\n' 17 | '================\n' 18 | 'error: $error\n' 19 | 'stack: $stack\n' 20 | '================\n', 21 | ); 22 | }; 23 | 24 | void logError( 25 | dynamic error, [ 26 | dynamic stack, 27 | String? reason, 28 | ]) => 29 | errorLogger( 30 | error, 31 | stack, 32 | reason, 33 | ); 34 | 35 | void debugLog( 36 | String message, [ 37 | dynamic caller, 38 | ]) { 39 | debugPrint(caller == null ? message : '${caller.runtimeType}: $message'); 40 | } 41 | -------------------------------------------------------------------------------- /lib/core/utils/mvp_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | //ignore:prefer-match-file-name 7 | mixin PresenterStateMixin, T extends HasPresenter

> on State { 8 | P get presenter => widget.presenter; 9 | 10 | M get state => presenter.state; 11 | 12 | Widget stateObserver({ 13 | required BlocWidgetBuilder builder, 14 | BlocBuilderCondition? buildWhen, 15 | }) { 16 | return BlocBuilder( 17 | bloc: presenter, 18 | buildWhen: buildWhen, 19 | builder: builder, 20 | ); 21 | } 22 | 23 | @override 24 | void dispose() { 25 | super.dispose(); 26 | presenter.close(); 27 | } 28 | } 29 | 30 | mixin HasPresenter

on StatefulWidget { 31 | P get presenter; 32 | } 33 | 34 | mixin CubitToCubitCommunicationMixin on Cubit { 35 | final _subscriptions = >[]; 36 | 37 | void listenTo(Cubit cubit, {required void Function(C) onChange}) => 38 | addSubscription(cubit.stream.listen(onChange)); 39 | 40 | void addSubscription(StreamSubscription subscription) { 41 | _subscriptions.add(subscription); 42 | } 43 | 44 | @override 45 | Future close() async { 46 | await Future.wait(_subscriptions.map((it) => it.cancel())); 47 | await super.close(); 48 | _subscriptions.clear(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/core/utils/periodic_task_executor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class PeriodicTaskExecutor { 4 | Timer? _timer; 5 | bool Function() _shouldStopCallback = () => false; 6 | 7 | /// runs the given [task] periodically 8 | /// 9 | /// params: 10 | /// [shouldStop] - callback to check whether the executor should stop running the periodic task, called just after 11 | /// each invocation of [task] 12 | /// [period] - how often to run the task 13 | /// [runOnStart] - whether to run the task immediately after start or after the [period] passes 14 | void start({ 15 | bool runOnStart = true, 16 | required Duration period, 17 | required FutureOr Function() task, 18 | bool Function()? shouldStop, 19 | }) { 20 | _shouldStopCallback = shouldStop ?? () => false; 21 | if (runOnStart) { 22 | task(); 23 | } 24 | _timer = Timer.periodic( 25 | period, 26 | (timer) async { 27 | await task(); 28 | if (_shouldStopCallback()) { 29 | cancel(); 30 | } 31 | }, 32 | ); 33 | } 34 | 35 | /// cancels the periodic task 36 | void cancel() => _timer?.cancel(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/dependency_injection/app_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/domain/stores/user_store.dart'; 2 | import 'package:flutter_demo/features/app_init/dependency_injection/feature_component.dart' as app_init; 3 | import 'package:flutter_demo/features/auth/dependency_injection/feature_component.dart' as auth; 4 | import 'package:flutter_demo/navigation/app_navigator.dart'; 5 | import 'package:get_it/get_it.dart'; 6 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 7 | 8 | final getIt = GetIt.instance; 9 | 10 | /// registers all the dependencies in dependency graph in get_it package 11 | void configureDependencies() { 12 | app_init.configureDependencies(); 13 | auth.configureDependencies(); 14 | //DO-NOT-REMOVE FEATURE_COMPONENT_INIT 15 | 16 | _configureGeneralDependencies(); 17 | _configureRepositories(); 18 | _configureStores(); 19 | _configureUseCases(); 20 | _configureMvp(); 21 | } 22 | 23 | //ignore: long-method 24 | void _configureGeneralDependencies() { 25 | // ignore: unnecessary_statements 26 | getIt 27 | ..registerFactory( 28 | () => AppNavigator(), 29 | ) 30 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG 31 | ; 32 | } 33 | 34 | //ignore: long-method 35 | void _configureRepositories() { 36 | // ignore: unnecessary_statements 37 | getIt 38 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG 39 | ; 40 | } 41 | 42 | //ignore: long-method 43 | void _configureStores() { 44 | // ignore: unnecessary_statements 45 | getIt 46 | ..registerFactory( 47 | () => UserStore(), 48 | ) 49 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG 50 | ; 51 | } 52 | 53 | //ignore: long-method 54 | void _configureUseCases() { 55 | // ignore: unnecessary_statements 56 | getIt 57 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG 58 | ; 59 | } 60 | 61 | //ignore: long-method 62 | void _configureMvp() { 63 | // ignore: unnecessary_statements 64 | getIt 65 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG 66 | ; 67 | } 68 | -------------------------------------------------------------------------------- /lib/features/app_init/app_init_initial_params.dart: -------------------------------------------------------------------------------- 1 | class AppInitInitialParams { 2 | const AppInitInitialParams(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/features/app_init/app_init_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/navigation/app_navigator.dart'; 2 | import 'package:flutter_demo/navigation/error_dialog_route.dart'; 3 | import 'package:flutter_demo/navigation/no_routes.dart'; 4 | 5 | class AppInitNavigator with NoRoutes, ErrorDialogRoute { 6 | AppInitNavigator(this.appNavigator); 7 | 8 | @override 9 | final AppNavigator appNavigator; 10 | } 11 | -------------------------------------------------------------------------------- /lib/features/app_init/app_init_page.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_demo/core/utils/mvp_extensions.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 6 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart'; 7 | import 'package:flutter_demo/resources/assets.gen.dart'; 8 | 9 | class AppInitPage extends StatefulWidget with HasPresenter { 10 | const AppInitPage({ 11 | required this.presenter, 12 | super.key, 13 | }); 14 | 15 | @override 16 | final AppInitPresenter presenter; 17 | 18 | @override 19 | State createState() => _AppInitPageState(); 20 | } 21 | 22 | class _AppInitPageState extends State 23 | with PresenterStateMixin { 24 | @override 25 | void initState() { 26 | super.initState(); 27 | presenter.onInit(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) => Scaffold( 32 | body: stateObserver( 33 | builder: (context, state) => Padding( 34 | padding: const EdgeInsets.all(32.0), 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.center, 37 | children: [ 38 | Assets.images.logo.image(), 39 | const SizedBox(height: 16), 40 | if (state.isLoading) const CircularProgressIndicator(), 41 | ], 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /lib/features/app_init/app_init_presentation_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart'; 3 | import 'package:flutter_demo/core/domain/model/user.dart'; 4 | import 'package:flutter_demo/core/utils/bloc_extensions.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 6 | 7 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page) 8 | class AppInitPresentationModel implements AppInitViewModel { 9 | /// Creates the initial state 10 | AppInitPresentationModel.initial( 11 | // ignore: avoid_unused_constructor_parameters 12 | AppInitInitialParams initialParams, 13 | ) : appInitResult = const FutureResult.empty(), 14 | user = const User.anonymous(); 15 | 16 | /// Used for the copyWith method 17 | AppInitPresentationModel._({ 18 | required this.appInitResult, 19 | required this.user, 20 | }); 21 | 22 | final FutureResult> appInitResult; 23 | final User user; 24 | 25 | @override 26 | bool get isLoading => appInitResult.isPending(); 27 | 28 | AppInitPresentationModel copyWith({ 29 | FutureResult>? appInitResult, 30 | User? user, 31 | }) => 32 | AppInitPresentationModel._( 33 | appInitResult: appInitResult ?? this.appInitResult, 34 | user: user ?? this.user, 35 | ); 36 | } 37 | 38 | /// Interface to expose fields used by the view (page). 39 | abstract class AppInitViewModel { 40 | bool get isLoading; 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/app_init/app_init_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:flutter_demo/core/domain/model/user.dart'; 5 | import 'package:flutter_demo/core/domain/stores/user_store.dart'; 6 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart'; 7 | import 'package:flutter_demo/core/helpers.dart'; 8 | import 'package:flutter_demo/core/utils/bloc_extensions.dart'; 9 | import 'package:flutter_demo/core/utils/either_extensions.dart'; 10 | import 'package:flutter_demo/core/utils/mvp_extensions.dart'; 11 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart'; 12 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 13 | 14 | class AppInitPresenter extends Cubit with CubitToCubitCommunicationMixin { 15 | AppInitPresenter( 16 | AppInitPresentationModel super.model, 17 | this.navigator, 18 | this.appInitUseCase, 19 | this.userStore, 20 | ) { 21 | listenTo( 22 | userStore, 23 | onChange: (user) => emit(_model.copyWith(user: user)), 24 | ); 25 | } 26 | 27 | final AppInitNavigator navigator; 28 | final AppInitUseCase appInitUseCase; 29 | final UserStore userStore; 30 | 31 | AppInitPresentationModel get _model => state as AppInitPresentationModel; 32 | 33 | Future onInit() async { 34 | await appInitUseCase 35 | .execute() // 36 | .observeStatusChanges((result) => emit(_model.copyWith(appInitResult: result))) 37 | .asyncFold( 38 | (fail) => navigator.showError(fail.displayableFailure()), 39 | (success) => doNothing(), //todo! 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/features/app_init/dependency_injection/feature_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart'; 2 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 3 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 4 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_page.dart'; 6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart'; 8 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 9 | 10 | /// registers all the dependencies in dependency graph in get_it package 11 | void configureDependencies() { 12 | _configureGeneralDependencies(); 13 | _configureRepositories(); 14 | _configureStores(); 15 | _configureUseCases(); 16 | _configureMvp(); 17 | } 18 | 19 | //ignore: long-method 20 | void _configureGeneralDependencies() { 21 | // ignore: unnecessary_statements 22 | getIt 23 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG 24 | ; 25 | } 26 | 27 | //ignore: long-method 28 | void _configureRepositories() { 29 | // ignore: unnecessary_statements 30 | getIt 31 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG 32 | ; 33 | } 34 | 35 | //ignore: long-method 36 | void _configureStores() { 37 | // ignore: unnecessary_statements 38 | getIt 39 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG 40 | ; 41 | } 42 | 43 | //ignore: long-method 44 | void _configureUseCases() { 45 | // ignore: unnecessary_statements 46 | getIt 47 | ..registerFactory( 48 | () => const AppInitUseCase(), 49 | ) 50 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG 51 | ; 52 | } 53 | 54 | //ignore: long-method 55 | void _configureMvp() { 56 | // ignore: unnecessary_statements 57 | getIt 58 | ..registerFactory( 59 | () => AppInitNavigator(getIt()), 60 | ) 61 | ..registerFactoryParam( 62 | (params, _) => AppInitPresentationModel.initial(params), 63 | ) 64 | ..registerFactoryParam( 65 | (initialParams, _) => AppInitPresenter( 66 | getIt(param1: initialParams), 67 | getIt(), 68 | getIt(), 69 | getIt(), 70 | ), 71 | ) 72 | ..registerFactoryParam( 73 | (initialParams, _) => AppInitPage( 74 | presenter: getIt(param1: initialParams), 75 | ), 76 | ) 77 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG 78 | ; 79 | } 80 | -------------------------------------------------------------------------------- /lib/features/auth/dependency_injection/feature_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 2 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart'; 3 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 4 | import 'package:flutter_demo/features/auth/login/login_navigator.dart'; 5 | import 'package:flutter_demo/features/auth/login/login_page.dart'; 6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart'; 8 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 9 | 10 | /// registers all the dependencies in dependency graph in get_it package 11 | void configureDependencies() { 12 | _configureGeneralDependencies(); 13 | _configureRepositories(); 14 | _configureStores(); 15 | _configureUseCases(); 16 | _configureMvp(); 17 | } 18 | 19 | //ignore: long-method 20 | void _configureGeneralDependencies() { 21 | // ignore: unnecessary_statements 22 | getIt 23 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG 24 | ; 25 | } 26 | 27 | //ignore: long-method 28 | void _configureRepositories() { 29 | // ignore: unnecessary_statements 30 | getIt 31 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG 32 | ; 33 | } 34 | 35 | //ignore: long-method 36 | void _configureStores() { 37 | // ignore: unnecessary_statements 38 | getIt 39 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG 40 | ; 41 | } 42 | 43 | //ignore: long-method 44 | void _configureUseCases() { 45 | // ignore: unnecessary_statements 46 | getIt 47 | ..registerFactory( 48 | () => LogInUseCase( 49 | getIt(), 50 | ), 51 | ) 52 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG 53 | 54 | ; 55 | } 56 | 57 | //ignore: long-method 58 | void _configureMvp() { 59 | // ignore: unnecessary_statements 60 | getIt 61 | ..registerFactory( 62 | () => LoginNavigator(getIt()), 63 | ) 64 | ..registerFactoryParam( 65 | (params, _) => LoginPresentationModel.initial(params), 66 | ) 67 | ..registerFactoryParam( 68 | (initialParams, _) => LoginPresenter( 69 | getIt(param1: initialParams), 70 | getIt(), 71 | ), 72 | ) 73 | ..registerFactoryParam( 74 | (initialParams, _) => LoginPage( 75 | presenter: getIt(param1: initialParams), 76 | ), 77 | ) 78 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG 79 | 80 | ; 81 | } 82 | -------------------------------------------------------------------------------- /lib/features/auth/domain/model/log_in_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart'; 2 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 3 | 4 | class LogInFailure implements HasDisplayableFailure { 5 | // ignore: avoid_field_initializers_in_const_classes 6 | const LogInFailure.unknown([this.cause]) : type = LogInFailureType.unknown; 7 | 8 | const LogInFailure.missingCredentials([this.cause]) : type = LogInFailureType.missingCredentials; 9 | 10 | final LogInFailureType type; 11 | final Object? cause; 12 | 13 | @override 14 | DisplayableFailure displayableFailure() { 15 | switch (type) { 16 | case LogInFailureType.unknown: 17 | return DisplayableFailure.commonError(); 18 | case LogInFailureType.missingCredentials: 19 | return DisplayableFailure( 20 | title: appLocalizations.missingCredsTitle, 21 | message: appLocalizations.missingCredsMessage, 22 | ); 23 | } 24 | } 25 | 26 | @override 27 | String toString() => 'LogInFailure{type: $type, cause: $cause}'; 28 | } 29 | 30 | enum LogInFailureType { 31 | unknown, 32 | missingCredentials, 33 | } 34 | -------------------------------------------------------------------------------- /lib/features/auth/domain/use_cases/log_in_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dartz/dartz.dart'; 4 | import 'package:flutter_demo/core/domain/model/user.dart'; 5 | import 'package:flutter_demo/core/domain/stores/user_store.dart'; 6 | import 'package:flutter_demo/core/utils/either_extensions.dart'; 7 | import 'package:flutter_demo/features/auth/domain/model/log_in_failure.dart'; 8 | import 'package:flutter_demo/main.dart'; 9 | 10 | class LogInUseCase { 11 | const LogInUseCase(this._userStore); 12 | 13 | final UserStore _userStore; 14 | 15 | Future> execute({ 16 | required String username, 17 | required String password, 18 | }) async { 19 | if (username.isEmpty || password.isEmpty) { 20 | return failure(const LogInFailure.missingCredentials()); 21 | } 22 | 23 | if (!isUnitTests) { 24 | //TODO simulation of network request 25 | //ignore: no-magic-number 26 | await Future.delayed(Duration(milliseconds: 500 + Random().nextInt(1000))); 27 | } 28 | 29 | if (username == 'test' && password == 'test123') { 30 | final user = User( 31 | id: "id_$username", 32 | username: username, 33 | ); 34 | _userStore.user = user; 35 | return success( 36 | user, 37 | ); 38 | } 39 | return failure(const LogInFailure.unknown()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/features/auth/login/login_initial_params.dart: -------------------------------------------------------------------------------- 1 | class LoginInitialParams { 2 | const LoginInitialParams(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/features/auth/login/login_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 2 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 3 | import 'package:flutter_demo/features/auth/login/login_page.dart'; 4 | import 'package:flutter_demo/navigation/app_navigator.dart'; 5 | import 'package:flutter_demo/navigation/no_routes.dart'; 6 | 7 | class LoginNavigator with NoRoutes { 8 | LoginNavigator(this.appNavigator); 9 | 10 | @override 11 | final AppNavigator appNavigator; 12 | } 13 | 14 | //ignore: unused-code 15 | mixin LoginRoute { 16 | Future openLogin(LoginInitialParams initialParams) async { 17 | return appNavigator.push( 18 | materialRoute(getIt(param1: initialParams)), 19 | ); 20 | } 21 | 22 | AppNavigator get appNavigator; 23 | } 24 | -------------------------------------------------------------------------------- /lib/features/auth/login/login_page.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_demo/core/helpers.dart'; 5 | import 'package:flutter_demo/core/utils/mvp_extensions.dart'; 6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart'; 8 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 9 | 10 | class LoginPage extends StatefulWidget with HasPresenter { 11 | const LoginPage({ 12 | required this.presenter, 13 | super.key, 14 | }); 15 | 16 | @override 17 | final LoginPresenter presenter; 18 | 19 | @override 20 | State createState() => _LoginPageState(); 21 | } 22 | 23 | class _LoginPageState extends State with PresenterStateMixin { 24 | @override 25 | Widget build(BuildContext context) => Scaffold( 26 | body: Padding( 27 | padding: const EdgeInsets.all(32.0), 28 | child: Column( 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | children: [ 31 | TextField( 32 | decoration: InputDecoration( 33 | hintText: appLocalizations.usernameHint, 34 | ), 35 | onChanged: (text) => doNothing(), //TODO 36 | ), 37 | const SizedBox(height: 8), 38 | TextField( 39 | obscureText: true, 40 | decoration: InputDecoration( 41 | hintText: appLocalizations.passwordHint, 42 | ), 43 | onChanged: (text) => doNothing(), //TODO 44 | ), 45 | const SizedBox(height: 16), 46 | stateObserver( 47 | builder: (context, state) => ElevatedButton( 48 | onPressed: () => doNothing(), //TODO 49 | child: Text(appLocalizations.logInAction), 50 | ), 51 | ), 52 | ], 53 | ), 54 | ), 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /lib/features/auth/login/login_presentation_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 2 | 3 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page) 4 | class LoginPresentationModel implements LoginViewModel { 5 | /// Creates the initial state 6 | LoginPresentationModel.initial( 7 | // ignore: avoid_unused_constructor_parameters 8 | LoginInitialParams initialParams, 9 | ); 10 | 11 | /// Used for the copyWith method 12 | LoginPresentationModel._(); 13 | 14 | LoginPresentationModel copyWith() { 15 | return LoginPresentationModel._(); 16 | } 17 | } 18 | 19 | /// Interface to expose fields used by the view (page). 20 | abstract class LoginViewModel {} 21 | -------------------------------------------------------------------------------- /lib/features/auth/login/login_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_demo/features/auth/login/login_navigator.dart'; 3 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 4 | 5 | class LoginPresenter extends Cubit { 6 | LoginPresenter( 7 | LoginPresentationModel super.model, 8 | this.navigator, 9 | ); 10 | 11 | final LoginNavigator navigator; 12 | 13 | // ignore: unused_element 14 | LoginPresentationModel get _model => state as LoginPresentationModel; 15 | } 16 | -------------------------------------------------------------------------------- /lib/flutter_demo_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 3 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 4 | import 'package:flutter_demo/features/app_init/app_init_page.dart'; 5 | import 'package:flutter_demo/navigation/app_navigator.dart'; 6 | import 'package:flutter_demo/utils/locale_resolution.dart'; 7 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 8 | import 'package:flutter_localizations/flutter_localizations.dart'; 9 | 10 | class FlutterDemoApp extends StatefulWidget { 11 | const FlutterDemoApp({super.key}); 12 | 13 | @override 14 | State createState() => _FlutterDemoAppState(); 15 | } 16 | 17 | class _FlutterDemoAppState extends State { 18 | late AppInitPage page; 19 | 20 | @override 21 | void initState() { 22 | page = getIt(param1: const AppInitInitialParams()); 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return MaterialApp( 29 | home: page, 30 | debugShowCheckedModeBanner: false, 31 | navigatorKey: AppNavigator.navigatorKey, 32 | localizationsDelegates: const [ 33 | AppLocalizations.delegate, 34 | GlobalMaterialLocalizations.delegate, 35 | GlobalWidgetsLocalizations.delegate, 36 | GlobalCupertinoLocalizations.delegate, 37 | ], 38 | localeListResolutionCallback: localeResolution, 39 | supportedLocales: AppLocalizations.supportedLocales, 40 | builder: (context, child) => MediaQuery( 41 | data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), 42 | child: child!, 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /lib/localization/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "appTitle": "Flutter Demo", 4 | "okAction": "Ok", 5 | "commonErrorTitle": "Error", 6 | "commonErrorMessage": "Something went wrong, please try again", 7 | "missingCredsTitle": "Missing Credentials", 8 | "missingCredsMessage": "Please provide both username and password!", 9 | "usernameHint": "username", 10 | "passwordHint": "password", 11 | "logInAction": "Log in" 12 | } -------------------------------------------------------------------------------- /lib/localization/app_localizations_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/navigation/app_navigator.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 3 | 4 | AppLocalizations? _appLocalizations; 5 | 6 | /// Convenience getter for the app localizations 7 | AppLocalizations get appLocalizations { 8 | _appLocalizations ??= AppLocalizations.of(AppNavigator.navigatorKey.currentContext!)!; 9 | return _appLocalizations!; 10 | } 11 | 12 | /// Useful method for tests to override app localizations 13 | void overrideAppLocalizations(AppLocalizations localizations) { 14 | _appLocalizations = localizations; 15 | } 16 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused-code 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_demo/core/helpers.dart'; 4 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 5 | import 'package:flutter_demo/flutter_demo_app.dart'; 6 | import 'package:flutter_demo/navigation/close_with_result_route.dart'; 7 | 8 | /// flag modified by unit tests so that app's code can adapt to unit tests 9 | /// (i.e: disable animations in progress bars etc.) 10 | bool isUnitTests = false; 11 | 12 | void main() { 13 | configureDependencies(); 14 | _suppressUnusedCodeWarnings(); // used in tests 15 | runApp(const FlutterDemoApp()); 16 | } 17 | 18 | /// hacky way to get rid of false-positive `unused-code` warnings from dart_code_metrics 19 | /// https://github.com/dart-code-checker/dart-code-metrics/pull/929 20 | void _suppressUnusedCodeWarnings() { 21 | suppressUnusedCodeWarning([ 22 | notImplemented, 23 | CloseWithResultRoute, 24 | ]); 25 | } 26 | -------------------------------------------------------------------------------- /lib/navigation/alert_dialog_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 3 | import 'package:flutter_demo/navigation/app_navigator.dart'; 4 | 5 | //ignore: unused-code 6 | mixin AlertDialogRoute { 7 | Future showAlert({ 8 | required String title, 9 | required String message, 10 | }) { 11 | return showDialog( 12 | context: AppNavigator.navigatorKey.currentContext!, 13 | builder: (context) => _AlertDialog( 14 | title: title, 15 | message: message, 16 | ), 17 | ); 18 | } 19 | } 20 | 21 | class _AlertDialog extends StatelessWidget { 22 | const _AlertDialog({ 23 | required this.title, 24 | required this.message, 25 | }); 26 | 27 | final String title; 28 | final String message; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return AlertDialog( 33 | title: Text(title), 34 | content: Text(message), 35 | actions: [ 36 | TextButton( 37 | onPressed: () => Navigator.of(context).pop(), 38 | child: Text(appLocalizations.okAction), 39 | ), 40 | ], 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/navigation/app_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/core/helpers.dart'; 3 | import 'package:flutter_demo/core/utils/durations.dart'; 4 | import 'package:flutter_demo/navigation/transitions/fade_in_page_transition.dart'; 5 | import 'package:flutter_demo/navigation/transitions/slide_bottom_page_transition.dart'; 6 | 7 | class AppNavigator { 8 | AppNavigator() { 9 | suppressUnusedCodeWarning([fadeInRoute, slideBottomRoute]); 10 | } 11 | 12 | static final navigatorKey = GlobalKey(); 13 | 14 | Future push( 15 | Route route, { 16 | BuildContext? context, 17 | bool useRoot = false, 18 | }) async { 19 | return _navigator(context, useRoot: useRoot).push(route); 20 | } 21 | 22 | Future pushReplacement( 23 | Route route, { 24 | BuildContext? context, 25 | bool useRoot = false, 26 | }) async { 27 | return _navigator(context, useRoot: useRoot).pushReplacement(route); 28 | } 29 | 30 | void close({ 31 | BuildContext? context, 32 | }) => 33 | closeWithResult(null, context: context); 34 | 35 | void closeWithResult( 36 | T result, { 37 | BuildContext? context, 38 | }) => 39 | _navigator(context).canPop() ? _navigator(context).pop(result) : result; 40 | 41 | void popUntilRoot(BuildContext context) => _navigator(context).popUntil((route) => route.isFirst); 42 | 43 | void popUntilPageWithName( 44 | String title, { 45 | BuildContext? context, 46 | }) => 47 | _navigator(context).popUntil(ModalRoute.withName(title)); 48 | } 49 | 50 | //ignore: long-parameter-list 51 | Route fadeInRoute( 52 | Widget page, { 53 | int? durationMillis, 54 | String? pageName, 55 | bool opaque = true, 56 | bool fadeOut = true, 57 | }) => 58 | PageRouteBuilder( 59 | opaque: opaque, 60 | transitionDuration: Duration( 61 | milliseconds: durationMillis ?? Durations.medium, 62 | ), 63 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()), 64 | pageBuilder: _pageBuilder(page), 65 | transitionsBuilder: fadeInPageTransition(fadeOut: fadeOut), 66 | ); 67 | 68 | //ignore: long-parameter-list, unused-code 69 | Route noTransitionRoute( 70 | Widget page, { 71 | int? durationMillis, 72 | String? pageName, 73 | bool opaque = true, 74 | }) => 75 | PageRouteBuilder( 76 | opaque: opaque, 77 | transitionDuration: Duration.zero, 78 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()), 79 | pageBuilder: _pageBuilder(page), 80 | ); 81 | 82 | Route materialRoute( 83 | Widget page, { 84 | bool fullScreenDialog = false, 85 | String? pageName, 86 | }) => 87 | MaterialPageRoute( 88 | builder: (context) => page, 89 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()), 90 | fullscreenDialog: fullScreenDialog, 91 | ); 92 | 93 | //ignore: long-parameter-list 94 | Route slideBottomRoute( 95 | Widget page, { 96 | int? durationMillis, 97 | bool fullScreenDialog = false, 98 | String? pageName, 99 | bool opaque = true, 100 | }) => 101 | PageRouteBuilder( 102 | opaque: opaque, 103 | transitionDuration: Duration( 104 | milliseconds: durationMillis ?? Durations.medium, 105 | ), 106 | fullscreenDialog: fullScreenDialog, 107 | settings: RouteSettings(name: pageName ?? page.runtimeType.toString()), 108 | pageBuilder: _pageBuilder(page), 109 | transitionsBuilder: slideBottomPageTransition(), 110 | ); 111 | 112 | RoutePageBuilder _pageBuilder(Widget page) => ( 113 | context, 114 | animation, 115 | secondaryAnimation, 116 | ) => 117 | page; 118 | 119 | NavigatorState _navigator(BuildContext? context, {bool useRoot = false}) => 120 | (useRoot || context == null) ? AppNavigator.navigatorKey.currentState! : Navigator.of(context); 121 | -------------------------------------------------------------------------------- /lib/navigation/close_route.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused-code, unused-files 2 | import 'package:flutter_demo/navigation/app_navigator.dart'; 3 | 4 | mixin CloseRoute { 5 | AppNavigator get appNavigator; 6 | 7 | void close() => appNavigator.close(); 8 | } 9 | -------------------------------------------------------------------------------- /lib/navigation/close_with_result_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/navigation/app_navigator.dart'; 2 | 3 | mixin CloseWithResultRoute { 4 | AppNavigator get appNavigator; 5 | 6 | void closeWithResult(T? result) => appNavigator.closeWithResult(result); 7 | } 8 | -------------------------------------------------------------------------------- /lib/navigation/error_dialog_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart'; 3 | import 'package:flutter_demo/core/utils/logging.dart'; 4 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 5 | import 'package:flutter_demo/navigation/app_navigator.dart'; 6 | 7 | mixin ErrorDialogRoute { 8 | Future showError(DisplayableFailure failure, {BuildContext? context}) { 9 | logError(failure); 10 | return showDialog( 11 | context: context ?? AppNavigator.navigatorKey.currentContext!, 12 | builder: (context) => ErrorDialog(failure: failure), 13 | ); 14 | } 15 | } 16 | 17 | class ErrorDialog extends StatelessWidget { 18 | const ErrorDialog({ 19 | required this.failure, 20 | super.key, 21 | }); 22 | 23 | final DisplayableFailure failure; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return AlertDialog( 28 | title: Text(failure.title), 29 | content: Text(failure.message), 30 | actions: [ 31 | TextButton( 32 | onPressed: () => Navigator.of(context).pop(), 33 | child: Text(appLocalizations.okAction), 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/navigation/no_routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/navigation/app_navigator.dart'; 2 | 3 | /// used with navigators that don't have any routes (yet). 4 | mixin NoRoutes { 5 | AppNavigator get appNavigator; 6 | } 7 | -------------------------------------------------------------------------------- /lib/navigation/transitions/fade_in_page_transition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | RouteTransitionsBuilder fadeInPageTransition({ 4 | required bool fadeOut, 5 | }) => 6 | ( 7 | context, 8 | animation, 9 | secondaryAnimation, 10 | child, 11 | ) => 12 | FadeTransition( 13 | opacity: Tween(begin: 0, end: 1).animate(animation), 14 | child: fadeOut 15 | ? FadeTransition( 16 | opacity: Tween(begin: 1, end: 0).animate(secondaryAnimation), 17 | child: child, 18 | ) 19 | : child, 20 | ); 21 | -------------------------------------------------------------------------------- /lib/navigation/transitions/slide_bottom_page_transition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | RouteTransitionsBuilder slideBottomPageTransition() => ( 4 | context, 5 | animation, 6 | secondaryAnimation, 7 | child, 8 | ) => 9 | SlideTransition( 10 | position: Tween( 11 | begin: const Offset(0.0, 1.0), 12 | end: Offset.zero, 13 | ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutQuint)), 14 | child: child, 15 | ); 16 | -------------------------------------------------------------------------------- /lib/resources/assets.gen.dart: -------------------------------------------------------------------------------- 1 | /// GENERATED CODE - DO NOT MODIFY BY HAND 2 | /// ***************************************************** 3 | /// FlutterGen 4 | /// ***************************************************** 5 | 6 | // coverage:ignore-file 7 | // ignore_for_file: type=lint 8 | // ignore_for_file: directives_ordering,unnecessary_import 9 | 10 | import 'package:flutter/widgets.dart'; 11 | 12 | class $AssetsImagesGen { 13 | const $AssetsImagesGen(); 14 | 15 | /// File path: assets/images/logo.webp 16 | AssetGenImage get logo => const AssetGenImage('assets/images/logo.webp'); 17 | } 18 | 19 | class Assets { 20 | Assets._(); 21 | 22 | static const $AssetsImagesGen images = $AssetsImagesGen(); 23 | } 24 | 25 | class AssetGenImage { 26 | const AssetGenImage(this._assetName); 27 | 28 | final String _assetName; 29 | 30 | Image image({ 31 | Key? key, 32 | AssetBundle? bundle, 33 | ImageFrameBuilder? frameBuilder, 34 | ImageErrorWidgetBuilder? errorBuilder, 35 | String? semanticLabel, 36 | bool excludeFromSemantics = false, 37 | double? scale, 38 | double? width, 39 | double? height, 40 | Color? color, 41 | Animation? opacity, 42 | BlendMode? colorBlendMode, 43 | BoxFit? fit, 44 | AlignmentGeometry alignment = Alignment.center, 45 | ImageRepeat repeat = ImageRepeat.noRepeat, 46 | Rect? centerSlice, 47 | bool matchTextDirection = false, 48 | bool gaplessPlayback = false, 49 | bool isAntiAlias = false, 50 | String? package, 51 | FilterQuality filterQuality = FilterQuality.low, 52 | int? cacheWidth, 53 | int? cacheHeight, 54 | }) { 55 | return Image.asset( 56 | _assetName, 57 | key: key, 58 | bundle: bundle, 59 | frameBuilder: frameBuilder, 60 | errorBuilder: errorBuilder, 61 | semanticLabel: semanticLabel, 62 | excludeFromSemantics: excludeFromSemantics, 63 | scale: scale, 64 | width: width, 65 | height: height, 66 | color: color, 67 | opacity: opacity, 68 | colorBlendMode: colorBlendMode, 69 | fit: fit, 70 | alignment: alignment, 71 | repeat: repeat, 72 | centerSlice: centerSlice, 73 | matchTextDirection: matchTextDirection, 74 | gaplessPlayback: gaplessPlayback, 75 | isAntiAlias: isAntiAlias, 76 | package: package, 77 | filterQuality: filterQuality, 78 | cacheWidth: cacheWidth, 79 | cacheHeight: cacheHeight, 80 | ); 81 | } 82 | 83 | String get path => _assetName; 84 | 85 | String get keyName => _assetName; 86 | } 87 | -------------------------------------------------------------------------------- /lib/utils/locale_resolution.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | Locale localeResolution( 4 | List? locales, 5 | Iterable supportedLocales, 6 | ) => 7 | (locales ?? []).firstWhere( 8 | (locale) => supportedLocales.any( 9 | (supported) => supported.languageCode == locale.languageCode, 10 | ), 11 | orElse: () => const Locale('en'), 12 | ); 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_demo 2 | description: A new Flutter project. 3 | 4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=2.17.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_localizations: 14 | sdk: flutter 15 | 16 | # architecture 17 | bloc: 8.0.3 18 | flutter_bloc: 8.0.1 19 | 20 | # dependency injection 21 | get_it: 7.2.0 22 | 23 | # functional programming, used for Either type 24 | dartz: 0.10.1 25 | 26 | # equality checks 27 | equatable: 2.0.3 28 | 29 | # localization 30 | intl: 0.17.0 31 | 32 | # widgets 33 | gap: 2.0.0 34 | 35 | 36 | dev_dependencies: 37 | flutter_test: 38 | sdk: flutter 39 | 40 | # code analysis 41 | lint: 1.10.0 42 | dart_code_metrics: 4.17.0 43 | custom_lint: 44 | git: 45 | url: https://github.com/andrzejchm/dart_custom_lint.git 46 | path: packages/custom_lint 47 | ref: main 48 | clean_architecture_lints: 49 | path: tools/custom_lints/clean_architecture_lints 50 | 51 | # tests 52 | golden_toolkit: 0.13.0 53 | alchemist: 0.4.1 54 | mocktail_image_network: 0.3.1 55 | mocktail: 0.3.0 56 | bloc_test: 9.0.3 57 | 58 | 59 | 60 | 61 | flutter: 62 | uses-material-design: true 63 | generate: true 64 | 65 | assets: 66 | - assets/ 67 | - assets/images/ 68 | 69 | flutter_gen: 70 | output: lib/resources/ 71 | line_length: 120 72 | 73 | flutter_intl: 74 | enabled: true 75 | -------------------------------------------------------------------------------- /templates/mason-lock.json: -------------------------------------------------------------------------------- 1 | {"bricks":{"use_case":{"path":"use_case"},"page":{"path":"page"},"repository":{"path":"repository"}}} -------------------------------------------------------------------------------- /templates/mason.yaml: -------------------------------------------------------------------------------- 1 | bricks: 2 | use_case: 3 | path: use_case 4 | page: 5 | path: page 6 | repository: 7 | path: repository -------------------------------------------------------------------------------- /templates/page/README.md: -------------------------------------------------------------------------------- 1 | # page 2 | 3 | [![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) 4 | 5 | Creates the MVP (Model-View-Presenter) structure for a screen 6 | 7 | creates following files: 8 | 9 | ``` 10 | - lib/features/$FEATURE_NAME$/$NAME$_page.dart 11 | - lib/features/$FEATURE_NAME$/$NAME$_presenter.dart 12 | - lib/features/$FEATURE_NAME$/$NAME$_presentation_model.dart 13 | - lib/features/$FEATURE_NAME$/$NAME$_initial_params.dart 14 | - lib/features/$FEATURE_NAME$/$NAME$_navigator.dart 15 | 16 | - test/pages/$NAME$_page_test.dart 17 | - test/presenters/$NAME$_presenter_test.dart 18 | ``` 19 | 20 | also modifies following files 21 | 22 | - `app_component.dart` - adds getIt registration for the page and its dependencies 23 | - `test/mock_definitions.dart` - adds mock definitions for the MVP classes 24 | - `test/mocks.dart` - creates the mocks as static fields for use in tests -------------------------------------------------------------------------------- /templates/page/__brick__/{{{initial_params_absolute_path}}}: -------------------------------------------------------------------------------- 1 | class {{initial_params_name}} { 2 | const {{initial_params_name}}(); 3 | } 4 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{navigator_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart'; 2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}'; 3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{page_file_name}}}'; 4 | import 'package:{{{app_package}}}/navigation/app_navigator.dart'; 5 | import 'package:{{{app_package}}}/navigation/no_routes.dart'; 6 | 7 | class {{navigator_name}} with NoRoutes { 8 | 9 | {{navigator_name}}(this.appNavigator); 10 | 11 | @override 12 | final AppNavigator appNavigator; 13 | } 14 | 15 | mixin {{route_name}} { 16 | Future open{{stem}}({{initial_params_name}} initialParams) async { 17 | return appNavigator.push( 18 | materialRoute(getIt<{{page_name}}>(param1: initialParams)), 19 | ); 20 | } 21 | 22 | AppNavigator get appNavigator; 23 | } 24 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{page_absolute_path}}}: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:{{{app_package}}}/core/utils/mvp_extensions.dart'; 5 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}'; 6 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}'; 7 | 8 | class {{page_name}} extends StatefulWidget with HasPresenter<{{presenter_name}}> { 9 | 10 | const {{page_name}}({ 11 | required this.presenter, 12 | Key? key, 13 | }) : super(key: key); 14 | 15 | @override 16 | final {{presenter_name}} presenter; 17 | 18 | @override 19 | State<{{page_name}}> createState() => _{{page_name}}State(); 20 | } 21 | 22 | class _{{page_name}}State extends State<{{page_name}}> with PresenterStateMixin<{{view_model_name}}, {{presenter_name}}, {{page_name}}> { 23 | 24 | @override 25 | Widget build(BuildContext context) => const Scaffold( 26 | body: Center( 27 | child: Text("{{page_name}}\n(NOT IMPLEMENTED YET)"), 28 | ), 29 | ); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{page_test_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart'; 3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}'; 4 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{navigator_file_name}}}'; 5 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{page_file_name}}}'; 6 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}'; 7 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}'; 8 | 9 | import '../../../mocks/mocks.dart'; 10 | import '../../../test_utils/golden_tests_utils.dart'; 11 | 12 | Future main() async { 13 | late {{page_name}} page; 14 | late {{initial_params_name}} initParams; 15 | late {{presentation_model_name}} model; 16 | late {{presenter_name}} presenter; 17 | late {{navigator_name}} navigator; 18 | 19 | void _initMvp() { 20 | initParams = const {{initial_params_name}}(); 21 | model = {{presentation_model_name}}.initial( 22 | initParams, 23 | ); 24 | navigator = {{navigator_name}}(Mocks.appNavigator); 25 | presenter = {{presenter_name}}( 26 | model, 27 | navigator, 28 | ); 29 | page = {{page_name}}(presenter: presenter); 30 | } 31 | 32 | await screenshotTest( 33 | "{{page_name.snakeCase()}}", 34 | setUp: () async { 35 | _initMvp(); 36 | }, 37 | pageBuilder: () => page, 38 | ); 39 | 40 | test("getIt page resolves successfully", () async { 41 | _initMvp(); 42 | final page = getIt<{{page_name}}>(param1: initParams); 43 | expect(page.presenter, isNotNull); 44 | expect(page, isNotNull); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{presentation_model_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:{{{app_package}}}/{{{import_path}}}/{{initial_params_file_name}}'; 2 | 3 | 4 | 5 | /// Model used by presenter, contains fields that are relevant to presenters and implements ViewModel to expose data to view (page) 6 | class {{presentation_model_name}} implements {{view_model_name}} { 7 | /// Creates the initial state 8 | {{presentation_model_name}}.initial( 9 | // ignore: avoid_unused_constructor_parameters 10 | {{initial_params_name}} initialParams, 11 | ); 12 | 13 | /// Used for the copyWith method 14 | {{presentation_model_name}}._(); 15 | 16 | {{presentation_model_name}} copyWith() { 17 | return {{presentation_model_name}}._(); 18 | } 19 | } 20 | 21 | /// Interface to expose fields used by the view (page). 22 | abstract class {{view_model_name}} {} 23 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{presenter_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{navigator_file_name}}}'; 3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}'; 4 | 5 | 6 | class {{presenter_name}} extends Cubit<{{view_model_name}}> { 7 | 8 | {{presenter_name}}( 9 | {{presentation_model_name}} model, 10 | this.navigator, 11 | ) : super(model); 12 | 13 | final {{navigator_name}} navigator; 14 | 15 | // ignore: unused_element 16 | {{presentation_model_name}} get _model => state as {{presentation_model_name}}; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /templates/page/__brick__/{{{presenter_test_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{initial_params_file_name}}}'; 3 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presentation_model_file_name}}}'; 4 | import 'package:{{{app_package}}}/{{{import_path}}}/{{{presenter_file_name}}}'; 5 | 6 | import '../mocks/{{{feature}}}_mock_definitions.dart'; 7 | 8 | void main() { 9 | late {{presentation_model_name}} model; 10 | late {{presenter_name}} presenter; 11 | late Mock{{navigator_name}} navigator; 12 | 13 | test( 14 | 'sample test', 15 | () { 16 | expect(presenter, isNotNull); // TODO implement this 17 | }, 18 | ); 19 | 20 | setUp(() { 21 | model = {{presentation_model_name}}.initial(const {{initial_params_name}}()); 22 | navigator = Mock{{navigator_name}}(); 23 | presenter = {{presenter_name}}( 24 | model, 25 | navigator, 26 | ); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /templates/page/brick.yaml: -------------------------------------------------------------------------------- 1 | name: page 2 | description: Creates Page, Presenter, PresentationModel and relevant tests. Registers the whole structure in getIt dependency graph. 3 | 4 | version: 0.1.0+1 5 | 6 | environment: 7 | mason: ">=0.1.0-dev.26 <0.1.0" 8 | 9 | vars: 10 | page_name: 11 | type: string 12 | description: "Page name" 13 | default: "Circle Details" 14 | prompt: "page's name: " 15 | 16 | feature_name: 17 | type: string 18 | description: "What is the name of the feature?" 19 | default: "circles" 20 | prompt: "Feature name (in snake_case): " 21 | 22 | subdirectory: 23 | type: string 24 | description: "subdirectory name inside the feature (OPTIONAL)" 25 | prompt: "subdirectory name inside the feature (OPTIONAL): " 26 | 27 | -------------------------------------------------------------------------------- /templates/page/hooks/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /templates/page/hooks/pre_gen.dart: -------------------------------------------------------------------------------- 1 | import "package:mason/mason.dart"; 2 | import "package:recase/recase.dart"; 3 | 4 | Future run(HookContext context) async { 5 | var pageName = (context.vars["page_name"] as String? ?? "").trim().pascalCase; 6 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase; 7 | var subdirectory = (context.vars["subdirectory"] as String? ?? "").trim(); 8 | 9 | if (pageName.isEmpty) { 10 | throw "Cannot use empty name for page_name"; 11 | } 12 | if (featureName.isEmpty) { 13 | throw "Cannot use empty name for feature_name"; 14 | } 15 | 16 | final stem = pageName.replaceAll("Page", ""); 17 | final featurePath = "features/${featureName}/${subdirectory}" // 18 | .replaceAll(RegExp("/+"), "/") 19 | .replaceAll(RegExp("/\$"), ""); 20 | final featureTestPath = "features/${featureName}" // 21 | .replaceAll(RegExp("/+"), "/") 22 | .replaceAll(RegExp("/\$"), ""); 23 | 24 | pageName = "${stem}Page"; 25 | 26 | final presenterName = "${stem}Presenter"; 27 | final presentationModelName = "${stem}PresentationModel"; 28 | final initialParamsName = "${stem}InitialParams"; 29 | final viewModelName = "${stem}ViewModel"; 30 | final navigatorName = "${stem}Navigator"; 31 | final routeName = "${stem}Route"; 32 | 33 | final pageFileName = "${stem.snakeCase}_page.dart"; 34 | final pageTestFileName = "${stem.snakeCase}_page_test.dart"; 35 | final presenterFileName = "${stem.snakeCase}_presenter.dart"; 36 | final presenterTestFileName = "${stem.snakeCase}_presenter_test.dart"; 37 | final presentationModelFileName = "${stem.snakeCase}_presentation_model.dart"; 38 | final initialParamsFileName = "${stem.snakeCase}_initial_params.dart"; 39 | final navigatorFileName = "${stem.snakeCase}_navigator.dart"; 40 | 41 | context.vars = { 42 | ...context.vars, 43 | ...context.vars, 44 | "app_package": "flutter_demo", 45 | "import_path": "${featurePath}", 46 | "stem": "${stem}", 47 | //class names 48 | "page_name": pageName, 49 | "presenter_name": presenterName, 50 | "presentation_model_name": presentationModelName, 51 | "initial_params_name": initialParamsName, 52 | "navigator_name": navigatorName, 53 | "view_model_name": viewModelName, 54 | "route_name": routeName, 55 | //file names 56 | "page_file_name": pageFileName, 57 | "presenter_file_name": presenterFileName, 58 | "initial_params_file_name": initialParamsFileName, 59 | "presentation_model_file_name": presentationModelFileName, 60 | "navigator_file_name": navigatorFileName, 61 | // absolute paths 62 | "page_absolute_path": "../lib/${featurePath}/$pageFileName", 63 | "presenter_absolute_path": "../lib/${featurePath}/$presenterFileName", 64 | "presentation_model_absolute_path": "../lib/${featurePath}/$presentationModelFileName", 65 | "navigator_absolute_path": "../lib/${featurePath}/$navigatorFileName", 66 | "initial_params_absolute_path": "../lib/${featurePath}/$initialParamsFileName", 67 | "page_test_absolute_path": "../test/${featureTestPath}/pages/$pageTestFileName", 68 | "presenter_test_absolute_path": "../test/${featureTestPath}/presenters/$presenterTestFileName", 69 | 'feature': featureName, 70 | }; 71 | context.logger.info("Generating page, variables: ${context.vars}"); 72 | } 73 | -------------------------------------------------------------------------------- /templates/page/hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: page_hooks 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | 6 | dependencies: 7 | template_utils: 8 | git: 9 | url: https://github.com/andrzejchm/flutter-app-showcase.git 10 | path: templates/template_utils 11 | ref: main 12 | mason: any 13 | recase: 4.0.0 -------------------------------------------------------------------------------- /templates/repository/README.md: -------------------------------------------------------------------------------- 1 | # repository 2 | 3 | [![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) 4 | 5 | A new brick created with the Mason CLI. 6 | 7 | _Generated by [mason][1] 🧱_ 8 | 9 | ## Getting Started 🚀 10 | 11 | This is a starting point for a new brick. 12 | A few resources to get you started if this is your first brick template: 13 | 14 | - [Official Mason Documentation][2] 15 | - [Code generation with Mason Blog][3] 16 | - [Very Good Livestream: Felix Angelov Demos Mason][4] 17 | 18 | [1]: https://github.com/felangel/mason 19 | [2]: https://github.com/felangel/mason/tree/master/packages/mason_cli#readme 20 | [3]: https://verygood.ventures/blog/code-generation-with-mason 21 | [4]: https://youtu.be/G4PTjA6tpTU 22 | -------------------------------------------------------------------------------- /templates/repository/__brick__/{{{implementation_absolute_path}}}: -------------------------------------------------------------------------------- 1 | {{{interface_import}}} 2 | 3 | class {{{implementation_name}}} implements {{{interface_name}}} { 4 | const {{{implementation_name}}}(); 5 | } -------------------------------------------------------------------------------- /templates/repository/__brick__/{{{interface_absolute_path}}}: -------------------------------------------------------------------------------- 1 | abstract class {{{interface_name}}} { 2 | 3 | } -------------------------------------------------------------------------------- /templates/repository/brick.yaml: -------------------------------------------------------------------------------- 1 | name: repository 2 | description: A new brick created with the Mason CLI. 3 | 4 | # The following defines the version and build number for your brick. 5 | # A version number is three numbers separated by dots, like 1.2.34 6 | # followed by an optional build number (separated by a +). 7 | version: 0.1.0+1 8 | 9 | # The following defines the environment for the current brick. 10 | # It includes the version of mason that the brick requires. 11 | environment: 12 | mason: ">=0.1.0-dev.26 <0.1.0" 13 | 14 | # Variables specify dynamic values that your brick depends on. 15 | # Zero or more variables can be specified for a given brick. 16 | # Each variable has: 17 | # * a type (string, number, boolean, enum, or array) 18 | # * an optional short description 19 | # * an optional default value 20 | # * an optional list of default values (array only) 21 | # * an optional prompt phrase used when asking for the variable 22 | # * a list of values (enums only) 23 | vars: 24 | 25 | interface_name: 26 | type: string 27 | description: UseCase name 28 | default: UsersRepository 29 | prompt: "Repository name: " 30 | 31 | implementation_prefix: 32 | type: string 33 | description: UseCase name 34 | default: RestApi 35 | prompt: "Repository implementation class prefix: " 36 | 37 | feature_name: 38 | type: string 39 | description: "What is the name of the feature? Leave blank if it should be in core package" 40 | prompt: "Feature name (in snake_case), leave blank if it should be in 'core' package: " 41 | -------------------------------------------------------------------------------- /templates/repository/hooks/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /templates/repository/hooks/post_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:mason/mason.dart'; 2 | import 'package:recase/recase.dart'; 3 | import 'package:template_utils/template_utils.dart'; 4 | 5 | Future run(HookContext context) async { 6 | final appPackage = context.vars["app_package"] as String; 7 | final feature = context.vars["feature"] as String; 8 | final interfaceName = context.vars["interface_name"] as String; 9 | final implementationName = context.vars["implementation_name"] as String; 10 | final interfaceFileName = context.vars["interface_file_name"] as String; 11 | final implementationFileName = context.vars["implementation_file_name"] as String; 12 | final implementationImport = context.vars["implementation_import"] as String; 13 | final interfaceImport = context.vars["interface_import"] as String; 14 | 15 | await _replaceInMockDefinitions( 16 | context: context, 17 | interfaceName: interfaceName, 18 | interfaceFileName: interfaceFileName, 19 | interfaceImport: interfaceImport, 20 | feature: feature, 21 | ); 22 | 23 | await _replaceInMocks( 24 | context: context, 25 | interfaceName: interfaceName, 26 | feature: feature, 27 | ); 28 | 29 | await _replaceInAppComponent( 30 | appPackage: appPackage, 31 | context: context, 32 | interfaceName: interfaceName, 33 | implementationName: implementationName, 34 | interfaceFileName: interfaceFileName, 35 | implementationFileName: implementationFileName, 36 | implementationImport: implementationImport, 37 | interfaceImport: interfaceImport, 38 | feature: feature, 39 | ); 40 | } 41 | 42 | Future _replaceInAppComponent({ 43 | required HookContext context, 44 | required String appPackage, 45 | required String interfaceName, 46 | required String implementationName, 47 | required String interfaceFileName, 48 | required String implementationFileName, 49 | required String implementationImport, 50 | required String interfaceImport, 51 | required String feature, 52 | }) async { 53 | await ensureFeatureComponentFile(appPackage: appPackage, feature: feature); 54 | await replaceAllInFile( 55 | filePath: featureComponentFilePath(feature), 56 | from: "//DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG", 57 | to: """ 58 | ..registerFactory<$interfaceName>( 59 | () => const $implementationName(), 60 | ) 61 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG 62 | """, 63 | ); 64 | await replaceAllInFile( 65 | filePath: featureComponentFilePath(feature), 66 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS", 67 | to: """ 68 | $implementationImport 69 | $interfaceImport 70 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 71 | """, 72 | ); 73 | } 74 | 75 | Future _replaceInMockDefinitions({ 76 | required HookContext context, 77 | required String interfaceName, 78 | required String interfaceFileName, 79 | required String interfaceImport, 80 | required String feature, 81 | }) async { 82 | final mockDefinition = (String name) => "class Mock$name extends Mock implements $name {}"; 83 | await ensureMockDefinitionsFile(feature); 84 | await replaceAllInFile( 85 | filePath: mockDefinitionsFilePath(feature), 86 | from: "//DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS", 87 | to: """ 88 | $interfaceImport 89 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 90 | """, 91 | ); 92 | 93 | await replaceAllInFile( 94 | filePath: mockDefinitionsFilePath(feature), 95 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION", 96 | to: """ 97 | ${mockDefinition(interfaceName)} 98 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION 99 | """, 100 | ); 101 | } 102 | 103 | Future _replaceInMocks({ 104 | required HookContext context, 105 | required String interfaceName, 106 | required String feature, 107 | }) async { 108 | final mockStaticField = (String name) => "static late Mock$name ${name.camelCase};"; 109 | final mockInit = (String name) => "${name.camelCase} = Mock$name();"; 110 | final registerFallbackValue = (String name) => "registerFallbackValue(Mock$name());"; 111 | 112 | await ensureMocksFile(feature); 113 | await replaceAllInFile( 114 | filePath: mocksFilePath(feature), 115 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD", 116 | to: """ 117 | ${mockStaticField(interfaceName)} 118 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD 119 | """, 120 | ); 121 | await replaceAllInFile( 122 | filePath: mocksFilePath(feature), 123 | from: "//DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS", 124 | to: """ 125 | ${mockInit(interfaceName)} 126 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 127 | """, 128 | ); 129 | await replaceAllInFile( 130 | filePath: mocksFilePath(feature), 131 | from: "//DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE", 132 | to: """ 133 | ${registerFallbackValue(interfaceName)} 134 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE 135 | """, 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /templates/repository/hooks/pre_gen.dart: -------------------------------------------------------------------------------- 1 | import "package:mason/mason.dart"; 2 | import "package:recase/recase.dart"; 3 | 4 | Future run(HookContext context) async { 5 | var interfaceName = (context.vars["interface_name"] as String? ?? "").trim().pascalCase; 6 | var implementationPrefix = (context.vars["implementation_prefix"] as String? ?? "").trim().pascalCase; 7 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase; 8 | 9 | if (interfaceName.isEmpty) { 10 | throw "Cannot use empty name for repository"; 11 | } 12 | if (implementationPrefix.isEmpty) { 13 | throw "Cannot use empty prefix for repository implementation"; 14 | } 15 | 16 | final stem = interfaceName.replaceAll("Repository", ""); 17 | interfaceName = "${stem}Repository"; 18 | final implementationName = "${implementationPrefix}${stem}Repository"; 19 | 20 | final interfaceFileName = "${stem.snakeCase}_repository.dart"; 21 | final implementationFileName = "${implementationPrefix.snakeCase}_${interfaceFileName}"; 22 | 23 | final featurePath = featureName.isEmpty ? "core" : "features/${featureName}"; 24 | 25 | var appPackage = "flutter_demo"; 26 | context.vars = { 27 | ...context.vars, 28 | "app_package": appPackage, 29 | "stem": "${stem}", 30 | "interface_name": interfaceName, 31 | "implementation_name": implementationName, 32 | "interface_file_name": interfaceFileName, 33 | "implementation_file_name": implementationFileName, 34 | "interface_absolute_path": "../lib/$featurePath/domain/repositories/$interfaceFileName", 35 | "implementation_absolute_path": "../lib/$featurePath/data/$implementationFileName", 36 | "interface_import": "import 'package:$appPackage/$featurePath/domain/repositories/$interfaceFileName';", 37 | "implementation_import": "import 'package:$appPackage/$featurePath/data/$implementationFileName';", 38 | "feature": featureName, 39 | }; 40 | context.logger.info("Generating useCase, variables: ${context.vars}"); 41 | } 42 | -------------------------------------------------------------------------------- /templates/repository/hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: use_case_hooks 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | 6 | dependencies: 7 | template_utils: 8 | git: 9 | url: https://github.com/andrzejchm/flutter-app-showcase.git 10 | path: templates/template_utils 11 | ref: main 12 | mason: any 13 | recase: 4.0.0 -------------------------------------------------------------------------------- /templates/template_utils/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build output. 6 | build/ 7 | -------------------------------------------------------------------------------- /templates/template_utils/README.md: -------------------------------------------------------------------------------- 1 | A sample command-line application with an entrypoint in `bin/`, library code 2 | in `lib/`, and example unit test in `test/`. 3 | -------------------------------------------------------------------------------- /templates/template_utils/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /templates/template_utils/lib/feature_templates.dart: -------------------------------------------------------------------------------- 1 | import 'package:recase/recase.dart'; 2 | 3 | String featureMockDefinitionsTemplate = """ 4 | import 'package:mocktail/mocktail.dart'; 5 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 6 | 7 | // MVP 8 | 9 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION 10 | 11 | // USE CASES 12 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION 13 | 14 | // REPOSITORIES 15 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION 16 | 17 | // STORES 18 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION 19 | 20 | """; 21 | 22 | String featureMocksTemplate(String feature) => """ 23 | import 'package:mocktail/mocktail.dart'; 24 | 25 | import '${feature.snakeCase}_mock_definitions.dart'; 26 | //DO-NOT-REMOVE IMPORTS_MOCKS 27 | 28 | class ${feature.pascalCase}Mocks { 29 | 30 | // MVP 31 | 32 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD 33 | 34 | // USE CASES 35 | 36 | 37 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD 38 | 39 | // REPOSITORIES 40 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD 41 | 42 | // STORES 43 | 44 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD 45 | 46 | 47 | static void init() { 48 | _initMocks(); 49 | _initFallbacks(); 50 | } 51 | 52 | static void _initMocks() { 53 | //DO-NOT-REMOVE FEATURES_MOCKS 54 | // MVP 55 | //DO-NOT-REMOVE MVP_INIT_MOCKS 56 | 57 | // USE CASES 58 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS 59 | 60 | // REPOSITORIES 61 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 62 | 63 | // STORES 64 | //DO-NOT-REMOVE STORES_INIT_MOCKS 65 | 66 | } 67 | 68 | static void _initFallbacks() { 69 | //DO-NOT-REMOVE FEATURES_FALLBACKS 70 | // MVP 71 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE 72 | 73 | // USE CASES 74 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE 75 | 76 | // REPOSITORIES 77 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE 78 | 79 | // STORES 80 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE 81 | 82 | } 83 | } 84 | """; 85 | 86 | String featureComponentTemplate(String appPackage) => """ 87 | import 'package:$appPackage/dependency_injection/app_component.dart'; 88 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 89 | 90 | /// registers all the dependencies in dependency graph in get_it package 91 | void configureDependencies() { 92 | _configureGeneralDependencies(); 93 | _configureRepositories(); 94 | _configureStores(); 95 | _configureUseCases(); 96 | _configureMvp(); 97 | } 98 | 99 | //ignore: long-method 100 | void _configureGeneralDependencies() { 101 | // ignore: unnecessary_statements 102 | getIt 103 | //DO-NOT-REMOVE GENERAL_DEPS_GET_IT_CONFIG 104 | ; 105 | } 106 | 107 | //ignore: long-method 108 | void _configureRepositories() { 109 | // ignore: unnecessary_statements 110 | getIt 111 | //DO-NOT-REMOVE REPOSITORIES_GET_IT_CONFIG 112 | ; 113 | } 114 | 115 | //ignore: long-method 116 | void _configureStores() { 117 | // ignore: unnecessary_statements 118 | getIt 119 | //DO-NOT-REMOVE STORES_GET_IT_CONFIG 120 | ; 121 | } 122 | 123 | //ignore: long-method 124 | void _configureUseCases() { 125 | // ignore: unnecessary_statements 126 | getIt 127 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG 128 | ; 129 | } 130 | 131 | //ignore: long-method 132 | void _configureMvp() { 133 | // ignore: unnecessary_statements 134 | getIt 135 | //DO-NOT-REMOVE MVP_GET_IT_CONFIG 136 | ; 137 | } 138 | """; 139 | 140 | String featurePageTestConfigTemplate = """ 141 | import 'dart:async'; 142 | 143 | import '../../../test_utils/test_utils.dart'; 144 | 145 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain); 146 | """; 147 | -------------------------------------------------------------------------------- /templates/template_utils/lib/file_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:mason/mason.dart'; 5 | import 'package:recase/recase.dart'; 6 | import 'package:template_utils/feature_templates.dart'; 7 | 8 | String featureComponentFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') // 9 | ? '../lib/dependency_injection/app_component.dart' 10 | : '../lib/features/$feature/dependency_injection/feature_component.dart'; 11 | 12 | String mockDefinitionsFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') // 13 | ? '../test/mocks/mock_definitions.dart' 14 | : '../test/features/$feature/mocks/${feature}_mock_definitions.dart'; 15 | 16 | String mocksFilePath(String? feature) => ((feature?.isEmpty ?? true) || feature == 'core') // 17 | ? '../test/mocks/mocks.dart' 18 | : '../test/features/$feature/mocks/${feature}_mocks.dart'; 19 | 20 | String pagesTestConfigPath(String feature) => '../test/features/$feature/pages/flutter_test_config.dart'; 21 | 22 | /// makes sure the feature-specific getIt registration index file is created, 23 | /// if its not, creates one and registers in master `app_component.dart` file 24 | Future ensureFeatureComponentFile({ 25 | required String appPackage, 26 | required String? feature, 27 | }) async { 28 | var featurePath = featureComponentFilePath(feature); 29 | var filePackage = featurePath.replaceAll("../lib/features/", ""); 30 | final featureFile = File(featurePath); 31 | final coreFile = File(featureComponentFilePath(null)); 32 | if (!await featureFile.exists()) { 33 | await featureFile.create(recursive: true); 34 | await writeToFile(filePath: featureFile.path, text: featureComponentTemplate(appPackage)); 35 | await replaceAllInFile( 36 | filePath: coreFile.path, 37 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS", 38 | to: """ 39 | import 'package:$appPackage/features/$filePackage' as $feature; 40 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 41 | """, 42 | ); 43 | await replaceAllInFile( 44 | filePath: coreFile.path, 45 | from: "//DO-NOT-REMOVE FEATURE_COMPONENT_INIT", 46 | to: """ 47 | $feature.configureDependencies(); 48 | //DO-NOT-REMOVE FEATURE_COMPONENT_INIT 49 | """, 50 | ); 51 | } 52 | } 53 | 54 | /// makes sure the feature-specific mock definitions file is created, if its not, creates one 55 | Future ensureMockDefinitionsFile( 56 | String? feature, { 57 | HookContext? context, 58 | }) async { 59 | var featurePath = mockDefinitionsFilePath(feature); 60 | final featureFile = File(featurePath).absolute; 61 | final coreFile = File(mockDefinitionsFilePath(null)).absolute; 62 | context?.logger.write("feature mocks file: ${featureFile.path}"); 63 | context?.logger.write("core file: ${coreFile.path}"); 64 | if (!await featureFile.exists()) { 65 | await featureFile.create(recursive: true); 66 | await writeToFile(filePath: featureFile.path, text: featureMockDefinitionsTemplate); 67 | } 68 | } 69 | 70 | /// makes sure the feature-specific mocks file is created, 71 | /// if its not, creates one and registers in master `mocks.dart` file 72 | Future ensureMocksFile( 73 | String? feature, { 74 | HookContext? context, 75 | }) async { 76 | var featurePath = mocksFilePath(feature); 77 | var filePackage = featurePath.replaceAll("../test/", "../"); 78 | final featureFile = File(featurePath); 79 | final coreFile = File(mocksFilePath(null)); 80 | await _ensurePageTestConfigFile(feature); 81 | if (!await featureFile.exists()) { 82 | await featureFile.create(recursive: true); 83 | await writeToFile(filePath: featureFile.path, text: featureMocksTemplate(feature!)); 84 | await replaceAllInFile( 85 | filePath: coreFile.path, 86 | from: "//DO-NOT-REMOVE IMPORTS_MOCKS", 87 | to: """ 88 | import '$filePackage'; 89 | //DO-NOT-REMOVE IMPORTS_MOCKS 90 | """, 91 | ); 92 | await replaceAllInFile( 93 | filePath: coreFile.path, 94 | from: "//DO-NOT-REMOVE FEATURE_MOCKS_INIT", 95 | to: """ 96 | ${feature.pascalCase}Mocks.init(); 97 | //DO-NOT-REMOVE FEATURE_MOCKS_INIT 98 | """, 99 | ); 100 | } 101 | } 102 | 103 | Future _ensurePageTestConfigFile(String? feature) async { 104 | if (feature == null || feature.isEmpty) { 105 | return; 106 | } 107 | final testConfigFile = File(pagesTestConfigPath(feature)).absolute; 108 | 109 | if (!await testConfigFile.exists()) { 110 | await testConfigFile.create(recursive: true); 111 | await writeToFile(filePath: testConfigFile.path, text: featurePageTestConfigTemplate); 112 | } 113 | } 114 | 115 | Future replaceAllInFile({ 116 | required String filePath, 117 | required String from, 118 | required String to, 119 | }) async { 120 | final tmpFilePath = "${filePath}_write_.tmp"; 121 | final tmpFile = File(tmpFilePath); 122 | bool contains = false; 123 | try { 124 | final readStream = readFileLines(filePath); 125 | final writeSink = tmpFile.openWrite(); 126 | 127 | await for (var line in readStream) { 128 | if (line.contains(from)) { 129 | contains = true; 130 | } 131 | writeSink.writeln(line.replaceAll(from, to)); 132 | } 133 | if (!contains) { 134 | throw "Target file ($filePath) does not contain '$from' text inside"; 135 | } 136 | await writeSink.close(); 137 | } catch (ex) { 138 | tmpFile.deleteSync(); 139 | rethrow; 140 | } 141 | 142 | await tmpFile.rename(filePath); 143 | } 144 | 145 | Future writeToFile({ 146 | required String filePath, 147 | required String text, 148 | }) async { 149 | final tmpFilePath = "${filePath}_write_.tmp"; 150 | final tmpFile = File(tmpFilePath); 151 | 152 | try { 153 | await tmpFile.writeAsString(text); 154 | } catch (ex) { 155 | tmpFile.deleteSync(); 156 | rethrow; 157 | } 158 | 159 | await tmpFile.rename(filePath); 160 | } 161 | 162 | Stream readFileLines(String path) => 163 | File(path).openRead().transform(utf8.decoder).transform(const LineSplitter()); 164 | 165 | void main() { 166 | ensureMockDefinitionsFile("sample_feature"); 167 | } 168 | -------------------------------------------------------------------------------- /templates/template_utils/lib/template_utils.dart: -------------------------------------------------------------------------------- 1 | library template_utils; 2 | 3 | export 'file_utils.dart'; 4 | -------------------------------------------------------------------------------- /templates/template_utils/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: template_utils 2 | description: A sample command-line application. 3 | version: 1.0.0 4 | # homepage: https://www.example.com 5 | 6 | environment: 7 | sdk: '>=2.17.5 <3.0.0' 8 | 9 | dependencies: 10 | mason: any 11 | recase: 4.0.0 12 | 13 | dev_dependencies: 14 | lints: 2.0.0 15 | test: 1.21.4 16 | 17 | -------------------------------------------------------------------------------- /templates/use_case/README.md: -------------------------------------------------------------------------------- 1 | # use_case 2 | 3 | [![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) 4 | 5 | Creates the UseCase along with the associated failure 6 | 7 | creates following files: 8 | 9 | ``` 10 | - lib/features/$FEATURE_NAME$/domain/use_cases/$NAME$_use_case.dart 11 | - lib/features/$FEATURE_NAME$/domain/failures/$NAME$_failure.dart 12 | 13 | - test/domain/$NAME$_use_case_test.dart 14 | ``` 15 | 16 | also modifies following files 17 | 18 | - `app_component.dart` - adds getIt registration for the use case 19 | - `test/mock_definitions.dart` - adds mock definitions for the use case 20 | - `test/mocks.dart` - creates the mocks as static fields for use in tests -------------------------------------------------------------------------------- /templates/use_case/__brick__/{{{failure_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:{{{app_package}}}/core/domain/model/displayable_failure.dart'; 2 | 3 | class {{failure_name}} implements HasDisplayableFailure { 4 | // ignore: avoid_field_initializers_in_const_classes 5 | const {{failure_name}}.unknown([this.cause]) : type = {{failure_name}}Type.Unknown; 6 | 7 | final {{failure_name}}Type type; 8 | final Object? cause; 9 | 10 | @override 11 | DisplayableFailure displayableFailure() { 12 | switch (type) { 13 | case {{failure_name}}Type.Unknown: 14 | return DisplayableFailure.commonError(); 15 | } 16 | } 17 | 18 | @override 19 | String toString() => '{{failure_name}}{type: $type, cause: $cause}'; 20 | } 21 | 22 | enum {{failure_name}}Type { 23 | Unknown, 24 | } 25 | -------------------------------------------------------------------------------- /templates/use_case/__brick__/{{{use_case_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:{{{app_package}}}/core/utils/either_extensions.dart'; 3 | import 'package:{{{app_package}}}/{{{import_path}}}/domain/model/{{{failure_file_name}}}'; 4 | 5 | class {{use_case_name}} { 6 | const {{use_case_name}}(); 7 | 8 | Future> execute() async { 9 | return failure(const {{failure_name}}.unknown()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/use_case/__brick__/{{{use_case_test_absolute_path}}}: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:{{{app_package}}}/core/utils/either_extensions.dart'; 3 | import 'package:{{{app_package}}}/dependency_injection/app_component.dart'; 4 | import 'package:{{{app_package}}}/{{{import_path}}}/domain/use_cases/{{{use_case_file_name}}}'; 5 | 6 | void main() { 7 | late {{use_case_name}} useCase; 8 | 9 | setUp(() { 10 | useCase = const {{use_case_name}}(); 11 | }); 12 | 13 | test( 14 | 'use case executes normally', 15 | () async { 16 | // GIVEN 17 | 18 | // WHEN 19 | final result = await useCase.execute(); 20 | 21 | // THEN 22 | expect(result.isSuccess, true); 23 | }, 24 | ); 25 | 26 | 27 | test("getIt resolves successfully", () async { 28 | final useCase = getIt<{{use_case_name}}>(); 29 | expect(useCase, isNotNull); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /templates/use_case/brick.yaml: -------------------------------------------------------------------------------- 1 | name: use_case 2 | description: Create use case and failure classes 3 | 4 | # The following defines the version and build number for your brick. 5 | # A version number is three numbers separated by dots, like 1.2.34 6 | # followed by an optional build number (separated by a +). 7 | version: 0.1.0+1 8 | 9 | # The following defines the environment for the current brick. 10 | # It includes the version of mason that the brick requires. 11 | environment: 12 | mason: ">=0.1.0-dev.26 <0.1.0" 13 | 14 | # Variables specify dynamic values that your brick depends on. 15 | # Zero or more variables can be specified for a given brick. 16 | # Each variable has: 17 | # * a type (string, number, boolean, enum, or array) 18 | # * an optional short description 19 | # * an optional default value 20 | # * an optional list of default values (array only) 21 | # * an optional prompt phrase used when asking for the variable 22 | # * a list of values (enums only) 23 | vars: 24 | use_case_name: 25 | type: string 26 | description: UseCase name 27 | default: LogInUseCase 28 | prompt: "UseCase name:" 29 | 30 | feature_name: 31 | type: string 32 | description: "What is the name of the feature? Leave blank if it should be in core package" 33 | prompt: "Feature name (in snake_case), leave blank if it should be in 'core' package" 34 | -------------------------------------------------------------------------------- /templates/use_case/hooks/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /templates/use_case/hooks/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /templates/use_case/hooks/post_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:mason/mason.dart'; 2 | import 'package:recase/recase.dart'; 3 | import 'package:template_utils/template_utils.dart'; 4 | 5 | Future run(HookContext context) async { 6 | try { 7 | final useCaseName = context.vars["use_case_name"] as String; 8 | final failureName = context.vars["failure_name"] as String; 9 | final importPath = context.vars["import_path"] as String; 10 | final useCaseFileName = context.vars["use_case_file_name"] as String; 11 | final failureFileName = context.vars["failure_file_name"] as String; 12 | final appPackage = context.vars["app_package"] as String; 13 | final feature = context.vars["feature"] as String; 14 | 15 | context.logger.info("Modifying mock definitions..."); 16 | await _replaceInMockDefinitions( 17 | context: context, 18 | appPackage: appPackage, 19 | importPath: importPath, 20 | useCaseName: useCaseName, 21 | failureName: failureName, 22 | useCaseFileName: useCaseFileName, 23 | failureFileName: failureFileName, 24 | feature: feature, 25 | ); 26 | 27 | context.logger.info("Modifying mocks..."); 28 | await _replaceInMocks( 29 | context: context, 30 | useCaseName: useCaseName, 31 | failureName: failureName, 32 | feature: feature, 33 | ); 34 | 35 | context.logger.info("Modifying feature component..."); 36 | await _replaceInAppComponent( 37 | context: context, 38 | useCaseName: useCaseName, 39 | importPath: importPath, 40 | useCaseFileName: useCaseFileName, 41 | failureFileName: failureFileName, 42 | appPackage: appPackage, 43 | feature: feature, 44 | ); 45 | } catch (ex, stack) { 46 | context.logger.err("$ex\n$stack"); 47 | } 48 | } 49 | 50 | Future _replaceInAppComponent({ 51 | required HookContext context, 52 | required String useCaseName, 53 | required String importPath, 54 | required String useCaseFileName, 55 | required String failureFileName, 56 | required String appPackage, 57 | required String feature, 58 | }) async { 59 | await ensureFeatureComponentFile(appPackage: appPackage, feature: feature); 60 | await replaceAllInFile( 61 | filePath: featureComponentFilePath(feature), 62 | from: "//DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG", 63 | to: """ 64 | ..registerFactory<$useCaseName>( 65 | () => const $useCaseName(), 66 | ) 67 | //DO-NOT-REMOVE USE_CASES_GET_IT_CONFIG 68 | """, 69 | ); 70 | await replaceAllInFile( 71 | filePath: featureComponentFilePath(feature), 72 | from: "//DO-NOT-REMOVE APP_COMPONENT_IMPORTS", 73 | to: """ 74 | import 'package:$appPackage/$importPath/domain/use_cases/$useCaseFileName'; 75 | //DO-NOT-REMOVE APP_COMPONENT_IMPORTS 76 | """, 77 | ); 78 | } 79 | 80 | Future _replaceInMockDefinitions({ 81 | required HookContext context, 82 | required String appPackage, 83 | required String importPath, 84 | required String useCaseName, 85 | required String useCaseFileName, 86 | required String failureName, 87 | required String failureFileName, 88 | required String feature, 89 | }) async { 90 | final mockDefinition = (String name) => "class Mock$name extends Mock implements $name {}"; 91 | await ensureMockDefinitionsFile(feature, context: context); 92 | await replaceAllInFile( 93 | filePath: mockDefinitionsFilePath(feature), 94 | from: "//DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS", 95 | to: """ 96 | import 'package:$appPackage/$importPath/domain/use_cases/$useCaseFileName'; 97 | import 'package:$appPackage/$importPath/domain/model/$failureFileName'; 98 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 99 | """, 100 | ); 101 | 102 | await replaceAllInFile( 103 | filePath: mockDefinitionsFilePath(feature), 104 | from: "//DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION", 105 | to: """ 106 | ${mockDefinition(failureName)} 107 | ${mockDefinition(useCaseName)} 108 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION 109 | """, 110 | ); 111 | } 112 | 113 | Future _replaceInMocks({ 114 | required HookContext context, 115 | required String useCaseName, 116 | required String failureName, 117 | required String feature, 118 | }) async { 119 | final mockStaticField = (String name) => "static late Mock$name ${name.camelCase};"; 120 | final mockInit = (String name) => "${name.camelCase} = Mock$name();"; 121 | final registerFallbackValue = (String name) => "registerFallbackValue(Mock$name());"; 122 | 123 | await ensureMocksFile(feature); 124 | 125 | await replaceAllInFile( 126 | filePath: mocksFilePath(feature), 127 | from: "//DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD", 128 | to: """ 129 | ${mockStaticField(failureName)} 130 | ${mockStaticField(useCaseName)} 131 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD 132 | """, 133 | ); 134 | await replaceAllInFile( 135 | filePath: mocksFilePath(feature), 136 | from: "//DO-NOT-REMOVE USE_CASE_INIT_MOCKS", 137 | to: """ 138 | ${mockInit(failureName)} 139 | ${mockInit(useCaseName)} 140 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS 141 | """, 142 | ); 143 | await replaceAllInFile( 144 | filePath: mocksFilePath(feature), 145 | from: "//DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE", 146 | to: """ 147 | ${registerFallbackValue(failureName)} 148 | ${registerFallbackValue(useCaseName)} 149 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE 150 | """, 151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /templates/use_case/hooks/pre_gen.dart: -------------------------------------------------------------------------------- 1 | import "package:mason/mason.dart"; 2 | import "package:recase/recase.dart"; 3 | 4 | Future run(HookContext context) async { 5 | var useCaseName = (context.vars["use_case_name"] as String? ?? "").trim().pascalCase; 6 | final featureName = (context.vars["feature_name"] as String? ?? "").trim().snakeCase; 7 | 8 | if (useCaseName.isEmpty) { 9 | throw "Cannot use empty name for usecase"; 10 | } 11 | 12 | final stem = useCaseName.replaceAll("UseCase", ""); 13 | useCaseName = "${stem}UseCase"; 14 | 15 | final failureName = "${stem}Failure"; 16 | final useCaseFileName = "${stem.snakeCase}_use_case.dart"; 17 | final failureFileName = "${stem.snakeCase}_failure.dart"; 18 | final featurePath = featureName.isEmpty ? "core" : "features/${featureName}"; 19 | 20 | context.vars = { 21 | ...context.vars, 22 | "app_package": "flutter_demo", 23 | "import_path": "${featurePath}", 24 | "stem": "${stem}", 25 | "failure_name": failureName, 26 | "use_case_name": useCaseName, 27 | "use_case_file_name": useCaseFileName, 28 | "failure_file_name": failureFileName, 29 | "use_case_absolute_path": "../lib/${featurePath}/domain/use_cases/$useCaseFileName", 30 | "failure_absolute_path": "../lib/${featurePath}/domain/model/$failureFileName", 31 | "use_case_test_absolute_path": "../test/${featurePath}/domain/${stem.snakeCase}_use_case_test.dart", 32 | 'feature': featureName, 33 | }; 34 | context.logger.info("Generating useCase, variables: ${context.vars}"); 35 | } 36 | -------------------------------------------------------------------------------- /templates/use_case/hooks/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: use_case_hooks 2 | 3 | environment: 4 | sdk: ">=2.12.0 <3.0.0" 5 | 6 | dependencies: 7 | template_utils: 8 | git: 9 | url: https://github.com/andrzejchm/flutter-app-showcase.git 10 | path: templates/template_utils 11 | ref: main 12 | mason: any 13 | recase: 4.0.0 -------------------------------------------------------------------------------- /test/features/app_init/domain/app_init_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart'; 2 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | late AppInitUseCase useCase; 7 | 8 | setUp(() { 9 | useCase = const AppInitUseCase(); 10 | }); 11 | 12 | test( 13 | 'use case executes normally', 14 | () async { 15 | // GIVEN 16 | 17 | // WHEN 18 | final result = await useCase.execute(); 19 | 20 | // THEN 21 | expect(result.isRight(), true); 22 | }, 23 | ); 24 | 25 | test( 26 | "getIt resolves successfully", 27 | () async { 28 | final useCase = getIt(); 29 | expect(useCase, isNotNull); 30 | }, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /test/features/app_init/mocks/app_init_mock_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart'; 3 | import 'package:flutter_demo/core/domain/use_cases/app_init_use_case.dart'; 4 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart'; 6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart'; 8 | import 'package:mocktail/mocktail.dart'; 9 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 10 | 11 | // MVP 12 | 13 | class MockAppInitPresenter extends MockCubit implements AppInitPresenter {} 14 | 15 | class MockAppInitPresentationModel extends Mock implements AppInitPresentationModel {} 16 | 17 | class MockAppInitInitialParams extends Mock implements AppInitInitialParams {} 18 | 19 | class MockAppInitNavigator extends Mock implements AppInitNavigator {} 20 | 21 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION 22 | 23 | // USE CASES 24 | class MockAppInitFailure extends Mock implements AppInitFailure {} 25 | 26 | class MockAppInitUseCase extends Mock implements AppInitUseCase {} 27 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION 28 | 29 | // REPOSITORIES 30 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION 31 | 32 | // STORES 33 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION 34 | -------------------------------------------------------------------------------- /test/features/app_init/mocks/app_init_mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:mocktail/mocktail.dart'; 2 | 3 | import 'app_init_mock_definitions.dart'; 4 | //DO-NOT-REMOVE IMPORTS_MOCKS 5 | 6 | class AppInitMocks { 7 | // MVP 8 | 9 | static late MockAppInitPresenter appInitPresenter; 10 | static late MockAppInitPresentationModel appInitPresentationModel; 11 | static late MockAppInitInitialParams appInitInitialParams; 12 | static late MockAppInitNavigator appInitNavigator; 13 | 14 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD 15 | 16 | // USE CASES 17 | static late MockAppInitFailure appInitFailure; 18 | static late MockAppInitUseCase appInitUseCase; 19 | 20 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD 21 | 22 | // REPOSITORIES 23 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD 24 | 25 | // STORES 26 | 27 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD 28 | 29 | static void init() { 30 | _initMocks(); 31 | _initFallbacks(); 32 | } 33 | 34 | static void _initMocks() { 35 | //DO-NOT-REMOVE FEATURES_MOCKS 36 | // MVP 37 | appInitPresenter = MockAppInitPresenter(); 38 | appInitPresentationModel = MockAppInitPresentationModel(); 39 | appInitInitialParams = MockAppInitInitialParams(); 40 | appInitNavigator = MockAppInitNavigator(); 41 | //DO-NOT-REMOVE MVP_INIT_MOCKS 42 | 43 | // USE CASES 44 | appInitFailure = MockAppInitFailure(); 45 | appInitUseCase = MockAppInitUseCase(); 46 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS 47 | 48 | // REPOSITORIES 49 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 50 | 51 | // STORES 52 | //DO-NOT-REMOVE STORES_INIT_MOCKS 53 | } 54 | 55 | static void _initFallbacks() { 56 | //DO-NOT-REMOVE FEATURES_FALLBACKS 57 | // MVP 58 | registerFallbackValue(MockAppInitPresenter()); 59 | registerFallbackValue(MockAppInitPresentationModel()); 60 | registerFallbackValue(MockAppInitInitialParams()); 61 | registerFallbackValue(MockAppInitNavigator()); 62 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE 63 | 64 | // USE CASES 65 | registerFallbackValue(MockAppInitFailure()); 66 | registerFallbackValue(MockAppInitUseCase()); 67 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE 68 | 69 | // REPOSITORIES 70 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE 71 | 72 | // STORES 73 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/features/app_init/pages/app_init_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_demo/core/domain/stores/user_store.dart'; 3 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 4 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_navigator.dart'; 6 | import 'package:flutter_demo/features/app_init/app_init_page.dart'; 7 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 8 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:mocktail/mocktail.dart'; 11 | 12 | import '../../../test_utils/golden_tests_utils.dart'; 13 | import '../../../test_utils/test_utils.dart'; 14 | import '../mocks/app_init_mock_definitions.dart'; 15 | import '../mocks/app_init_mocks.dart'; 16 | 17 | Future main() async { 18 | late AppInitPage page; 19 | late AppInitInitialParams initParams; 20 | late AppInitPresentationModel model; 21 | late AppInitPresenter presenter; 22 | late AppInitNavigator navigator; 23 | 24 | void _initMvp() { 25 | initParams = const AppInitInitialParams(); 26 | model = AppInitPresentationModel.initial( 27 | initParams, 28 | ); 29 | navigator = MockAppInitNavigator(); 30 | presenter = AppInitPresenter( 31 | model, 32 | navigator, 33 | AppInitMocks.appInitUseCase, 34 | UserStore(), 35 | ); 36 | page = AppInitPage(presenter: presenter); 37 | } 38 | 39 | await screenshotTest( 40 | "app_init_page", 41 | setUp: () async { 42 | _initMvp(); 43 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => successFuture(unit)); 44 | }, 45 | pageBuilder: () => page, 46 | ); 47 | 48 | test("getIt page resolves successfully", () async { 49 | _initMvp(); 50 | final page = getIt(param1: initParams); 51 | expect(page.presenter, isNotNull); 52 | expect(page, isNotNull); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/features/app_init/pages/flutter_test_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../../../test_utils/test_utils.dart'; 4 | 5 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain); 6 | -------------------------------------------------------------------------------- /test/features/app_init/pages/goldens/ci/app_init_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/app_init/pages/goldens/ci/app_init_page.png -------------------------------------------------------------------------------- /test/features/app_init/pages/goldens/macos/app_init_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/app_init/pages/goldens/macos/app_init_page.png -------------------------------------------------------------------------------- /test/features/app_init/presenters/app_init_presenter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:dartz/dartz.dart'; 3 | import 'package:flutter_demo/core/domain/model/app_init_failure.dart'; 4 | import 'package:flutter_demo/core/domain/model/user.dart'; 5 | import 'package:flutter_demo/features/app_init/app_init_initial_params.dart'; 6 | import 'package:flutter_demo/features/app_init/app_init_presentation_model.dart'; 7 | import 'package:flutter_demo/features/app_init/app_init_presenter.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | import 'package:mocktail/mocktail.dart'; 10 | 11 | import '../../../mocks/mocks.dart'; 12 | import '../../../test_utils/test_utils.dart'; 13 | import '../mocks/app_init_mock_definitions.dart'; 14 | import '../mocks/app_init_mocks.dart'; 15 | 16 | void main() { 17 | late AppInitPresentationModel model; 18 | late AppInitPresenter presenter; 19 | late MockAppInitNavigator navigator; 20 | 21 | test( 22 | 'should call appInitUseCase on start', 23 | () async { 24 | // GIVEN 25 | whenListen( 26 | Mocks.userStore, 27 | Stream.fromIterable([const User.anonymous()]), 28 | ); 29 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => successFuture(unit)); 30 | 31 | // WHEN 32 | await presenter.onInit(); 33 | 34 | // THEN 35 | verify(() => AppInitMocks.appInitUseCase.execute()); 36 | verify(() => Mocks.userStore.stream); 37 | }, 38 | ); 39 | test( 40 | 'should show error when appInitUseCase fails', 41 | () async { 42 | // GIVEN 43 | whenListen( 44 | Mocks.userStore, 45 | Stream.fromIterable([const User.anonymous()]), 46 | ); 47 | when(() => AppInitMocks.appInitUseCase.execute()).thenAnswer((_) => failFuture(const AppInitFailure.unknown())); 48 | when(() => navigator.showError(any())).thenAnswer((_) => Future.value()); 49 | 50 | // WHEN 51 | await presenter.onInit(); 52 | 53 | // THEN 54 | verify(() => navigator.showError(any())); 55 | }, 56 | ); 57 | 58 | setUp(() { 59 | model = AppInitPresentationModel.initial(const AppInitInitialParams()); 60 | navigator = AppInitMocks.appInitNavigator; 61 | presenter = AppInitPresenter( 62 | model, 63 | navigator, 64 | AppInitMocks.appInitUseCase, 65 | Mocks.userStore, 66 | ); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/features/auth/domain/log_in_use_case_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/core/utils/either_extensions.dart'; 2 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 3 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../../../mocks/mocks.dart'; 7 | 8 | void main() { 9 | late LogInUseCase useCase; 10 | 11 | setUp(() { 12 | useCase = LogInUseCase(Mocks.userStore); 13 | }); 14 | 15 | test( 16 | 'use case executes normally', 17 | () async { 18 | // GIVEN 19 | 20 | // WHEN 21 | final result = await useCase.execute(username: "test", password: "test123"); 22 | 23 | // THEN 24 | expect(result.isSuccess, true); 25 | }, 26 | ); 27 | 28 | test("getIt resolves successfully", () async { 29 | final useCase = getIt(); 30 | expect(useCase, isNotNull); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/features/auth/mocks/auth_mock_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_demo/features/auth/domain/model/log_in_failure.dart'; 3 | import 'package:flutter_demo/features/auth/domain/use_cases/log_in_use_case.dart'; 4 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 5 | import 'package:flutter_demo/features/auth/login/login_navigator.dart'; 6 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 7 | import 'package:flutter_demo/features/auth/login/login_presenter.dart'; 8 | import 'package:mocktail/mocktail.dart'; 9 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 10 | 11 | // MVP 12 | 13 | class MockLoginPresenter extends MockCubit implements LoginPresenter {} 14 | 15 | class MockLoginPresentationModel extends Mock implements LoginPresentationModel {} 16 | 17 | class MockLoginInitialParams extends Mock implements LoginInitialParams {} 18 | 19 | class MockLoginNavigator extends Mock implements LoginNavigator {} 20 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION 21 | 22 | // USE CASES 23 | class MockLogInFailure extends Mock implements LogInFailure {} 24 | 25 | class MockLogInUseCase extends Mock implements LogInUseCase {} 26 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION 27 | 28 | // REPOSITORIES 29 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION 30 | 31 | // STORES 32 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION 33 | -------------------------------------------------------------------------------- /test/features/auth/mocks/auth_mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:mocktail/mocktail.dart'; 2 | 3 | import 'auth_mock_definitions.dart'; 4 | //DO-NOT-REMOVE IMPORTS_MOCKS 5 | 6 | class AuthMocks { 7 | // MVP 8 | 9 | static late MockLoginPresenter loginPresenter; 10 | static late MockLoginPresentationModel loginPresentationModel; 11 | static late MockLoginInitialParams loginInitialParams; 12 | static late MockLoginNavigator loginNavigator; 13 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD 14 | 15 | // USE CASES 16 | 17 | static late MockLogInFailure logInFailure; 18 | static late MockLogInUseCase logInUseCase; 19 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD 20 | 21 | // REPOSITORIES 22 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD 23 | 24 | // STORES 25 | 26 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD 27 | 28 | static void init() { 29 | _initMocks(); 30 | _initFallbacks(); 31 | } 32 | 33 | static void _initMocks() { 34 | //DO-NOT-REMOVE FEATURES_MOCKS 35 | // MVP 36 | loginPresenter = MockLoginPresenter(); 37 | loginPresentationModel = MockLoginPresentationModel(); 38 | loginInitialParams = MockLoginInitialParams(); 39 | loginNavigator = MockLoginNavigator(); 40 | //DO-NOT-REMOVE MVP_INIT_MOCKS 41 | 42 | // USE CASES 43 | logInFailure = MockLogInFailure(); 44 | logInUseCase = MockLogInUseCase(); 45 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS 46 | 47 | // REPOSITORIES 48 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 49 | 50 | // STORES 51 | //DO-NOT-REMOVE STORES_INIT_MOCKS 52 | } 53 | 54 | static void _initFallbacks() { 55 | //DO-NOT-REMOVE FEATURES_FALLBACKS 56 | // MVP 57 | registerFallbackValue(MockLoginPresenter()); 58 | registerFallbackValue(MockLoginPresentationModel()); 59 | registerFallbackValue(MockLoginInitialParams()); 60 | registerFallbackValue(MockLoginNavigator()); 61 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE 62 | 63 | // USE CASES 64 | registerFallbackValue(MockLogInFailure()); 65 | registerFallbackValue(MockLogInUseCase()); 66 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE 67 | 68 | // REPOSITORIES 69 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE 70 | 71 | // STORES 72 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/features/auth/pages/flutter_test_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../../../test_utils/test_utils.dart'; 4 | 5 | Future testExecutable(FutureOr Function() testMain) => preparePageTests(testMain); 6 | -------------------------------------------------------------------------------- /test/features/auth/pages/goldens/ci/login_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/auth/pages/goldens/ci/login_page.png -------------------------------------------------------------------------------- /test/features/auth/pages/goldens/macos/login_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrzejchm/flutter-app-showcase/cff55c14bbb6001733fa774c729c5ab08d9fcef9/test/features/auth/pages/goldens/macos/login_page.png -------------------------------------------------------------------------------- /test/features/auth/pages/login_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 2 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 3 | import 'package:flutter_demo/features/auth/login/login_navigator.dart'; 4 | import 'package:flutter_demo/features/auth/login/login_page.dart'; 5 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 6 | import 'package:flutter_demo/features/auth/login/login_presenter.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | import '../../../mocks/mocks.dart'; 10 | import '../../../test_utils/golden_tests_utils.dart'; 11 | 12 | Future main() async { 13 | late LoginPage page; 14 | late LoginInitialParams initParams; 15 | late LoginPresentationModel model; 16 | late LoginPresenter presenter; 17 | late LoginNavigator navigator; 18 | 19 | void _initMvp() { 20 | initParams = const LoginInitialParams(); 21 | model = LoginPresentationModel.initial( 22 | initParams, 23 | ); 24 | navigator = LoginNavigator(Mocks.appNavigator); 25 | presenter = LoginPresenter( 26 | model, 27 | navigator, 28 | ); 29 | page = LoginPage(presenter: presenter); 30 | } 31 | 32 | await screenshotTest( 33 | "login_page", 34 | setUp: () async { 35 | _initMvp(); 36 | }, 37 | pageBuilder: () => page, 38 | ); 39 | 40 | test("getIt page resolves successfully", () async { 41 | _initMvp(); 42 | final page = getIt(param1: initParams); 43 | expect(page.presenter, isNotNull); 44 | expect(page, isNotNull); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/features/auth/presenters/login_presenter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_demo/features/auth/login/login_initial_params.dart'; 2 | import 'package:flutter_demo/features/auth/login/login_presentation_model.dart'; 3 | import 'package:flutter_demo/features/auth/login/login_presenter.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | import '../mocks/auth_mock_definitions.dart'; 7 | 8 | void main() { 9 | late LoginPresentationModel model; 10 | late LoginPresenter presenter; 11 | late MockLoginNavigator navigator; 12 | 13 | test( 14 | 'sample test', 15 | () { 16 | expect(presenter, isNotNull); // TODO implement this 17 | }, 18 | ); 19 | 20 | setUp(() { 21 | model = LoginPresentationModel.initial(const LoginInitialParams()); 22 | navigator = MockLoginNavigator(); 23 | presenter = LoginPresenter( 24 | model, 25 | navigator, 26 | ); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/flutter_test_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'test_utils/test_utils.dart'; 4 | 5 | Future testExecutable(FutureOr Function() testMain) async { 6 | await prepareAppForUnitTests(); 7 | return testMain(); 8 | } 9 | -------------------------------------------------------------------------------- /test/mocks/mock_definitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_demo/core/domain/model/user.dart'; 3 | import 'package:flutter_demo/core/domain/stores/user_store.dart'; 4 | import 'package:flutter_demo/core/utils/current_time_provider.dart'; 5 | import 'package:flutter_demo/core/utils/debouncer.dart'; 6 | import 'package:flutter_demo/core/utils/periodic_task_executor.dart'; 7 | //DO-NOT-REMOVE IMPORTS_MOCK_DEFINITIONS 8 | 9 | import 'package:flutter_demo/navigation/app_navigator.dart'; 10 | import 'package:mocktail/mocktail.dart'; 11 | 12 | class MockAppNavigator extends Mock implements AppNavigator {} 13 | 14 | // MVP 15 | 16 | //DO-NOT-REMOVE MVP_MOCK_DEFINITION 17 | 18 | // USE CASES 19 | 20 | //DO-NOT-REMOVE USE_CASE_MOCK_DEFINITION 21 | 22 | // REPOSITORIES 23 | //DO-NOT-REMOVE REPOSITORIES_MOCK_DEFINITION 24 | 25 | // STORES 26 | class MockUserStore extends MockCubit implements UserStore {} 27 | //DO-NOT-REMOVE STORES_MOCK_DEFINITION 28 | 29 | class MockDebouncer extends Mock implements Debouncer {} 30 | 31 | class MockPeriodicTaskExecutor extends Mock implements PeriodicTaskExecutor {} 32 | 33 | class MockCurrentTimeProvider extends Mock implements CurrentTimeProvider {} 34 | -------------------------------------------------------------------------------- /test/mocks/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_demo/core/domain/model/displayable_failure.dart'; 3 | import 'package:flutter_demo/core/utils/periodic_task_executor.dart'; 4 | import 'package:flutter_demo/navigation/app_navigator.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | 7 | import '../features/app_init/mocks/app_init_mocks.dart'; 8 | import '../features/auth/mocks/auth_mocks.dart'; 9 | //DO-NOT-REMOVE IMPORTS_MOCKS 10 | 11 | import 'mock_definitions.dart'; 12 | 13 | class Mocks { 14 | static late MockAppNavigator appNavigator; 15 | 16 | // MVP 17 | 18 | //DO-NOT-REMOVE MVP_MOCKS_STATIC_FIELD 19 | 20 | // USE CASES 21 | 22 | //DO-NOT-REMOVE USE_CASE_MOCKS_STATIC_FIELD 23 | 24 | // REPOSITORIES 25 | //DO-NOT-REMOVE REPOSITORIES_MOCKS_STATIC_FIELD 26 | 27 | // STORES 28 | static late MockUserStore userStore; 29 | 30 | //DO-NOT-REMOVE STORES_MOCKS_STATIC_FIELD 31 | 32 | static late MockDebouncer debouncer; 33 | static late MockPeriodicTaskExecutor periodicTaskExecutor; 34 | static late MockCurrentTimeProvider currentTimeProvider; 35 | 36 | static void init() { 37 | AppInitMocks.init(); 38 | AuthMocks.init(); 39 | //DO-NOT-REMOVE FEATURE_MOCKS_INIT 40 | 41 | _initMocks(); 42 | _initFallbacks(); 43 | } 44 | 45 | static void _initMocks() { 46 | //DO-NOT-REMOVE FEATURES_MOCKS 47 | appNavigator = MockAppNavigator(); 48 | // MVP 49 | //DO-NOT-REMOVE MVP_INIT_MOCKS 50 | 51 | // USE CASES 52 | //DO-NOT-REMOVE USE_CASE_INIT_MOCKS 53 | 54 | // REPOSITORIES 55 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 56 | 57 | // STORES 58 | userStore = MockUserStore(); 59 | //DO-NOT-REMOVE REPOSITORIES_INIT_MOCKS 60 | 61 | debouncer = MockDebouncer(); 62 | periodicTaskExecutor = MockPeriodicTaskExecutor(); 63 | currentTimeProvider = MockCurrentTimeProvider(); 64 | } 65 | 66 | static void _initFallbacks() { 67 | //DO-NOT-REMOVE FEATURES_FALLBACKS 68 | registerFallbackValue(DisplayableFailure(title: "", message: "")); 69 | // MVP 70 | //DO-NOT-REMOVE MVP_MOCK_FALLBACK_VALUE 71 | 72 | // USE CASES 73 | //DO-NOT-REMOVE USE_CASE_MOCK_FALLBACK_VALUE 74 | 75 | // REPOSITORIES 76 | //DO-NOT-REMOVE REPOSITORIES_MOCK_FALLBACK_VALUE 77 | 78 | // STORES 79 | registerFallbackValue(MockUserStore()); 80 | //DO-NOT-REMOVE STORES_MOCK_FALLBACK_VALUE 81 | 82 | registerFallbackValue(materialRoute(Container())); 83 | registerFallbackValue(MockDebouncer()); 84 | registerFallbackValue(MockCurrentTimeProvider()); 85 | registerFallbackValue(PeriodicTaskExecutor()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/test_utils/golden_test_device_scenario.dart: -------------------------------------------------------------------------------- 1 | import 'package:alchemist/alchemist.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:golden_toolkit/golden_toolkit.dart'; 4 | 5 | /// Wrapper for testing widgets (primarily screens) with device constraints 6 | class GoldenTestDeviceScenario extends StatelessWidget { 7 | final Device device; 8 | final ValueGetter builder; 9 | 10 | const GoldenTestDeviceScenario({ 11 | required this.builder, 12 | required this.device, 13 | super.key, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return GoldenTestScenario( 19 | name: device.name, 20 | child: ClipRect( 21 | child: MediaQuery( 22 | data: MediaQuery.of(context).copyWith( 23 | size: device.size, 24 | padding: device.safeArea, 25 | platformBrightness: device.brightness, 26 | devicePixelRatio: device.devicePixelRatio, 27 | textScaleFactor: device.textScale, 28 | ), 29 | child: SizedBox( 30 | height: device.size.height, 31 | width: device.size.width, 32 | child: builder(), 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/test_utils/golden_tests_utils.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unused-code 2 | import 'dart:async'; 3 | 4 | import 'package:alchemist/alchemist.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:golden_toolkit/golden_toolkit.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:mocktail_image_network/mocktail_image_network.dart'; 9 | 10 | import 'golden_test_device_scenario.dart'; 11 | 12 | final testDevices = [ 13 | Device.phone.copyWith(name: "small phone"), 14 | Device.iphone11.copyWith(name: "iPhone 11"), 15 | ]; 16 | 17 | @isTest 18 | Future screenshotTest( 19 | String description, { 20 | String variantName = '', 21 | bool skip = false, 22 | FutureOr Function()? setUp, 23 | required Widget Function() pageBuilder, 24 | List tags = const ['golden'], 25 | List? devices, 26 | Duration timeout = const Duration(seconds: 5), 27 | }) async { 28 | return goldenTest( 29 | description, 30 | fileName: "$description${variantName.trim().isEmpty ? '' : '_$variantName'}", 31 | builder: () { 32 | setUp?.call(); 33 | 34 | return GoldenTestGroup( 35 | children: (devices ?? testDevices) // 36 | .map( 37 | (it) => DefaultAssetBundle( 38 | bundle: TestAssetBundle(), 39 | child: GoldenTestDeviceScenario(device: it, builder: pageBuilder), 40 | ), 41 | ) 42 | .toList(), 43 | ); 44 | }, 45 | tags: tags, 46 | skip: skip, 47 | pumpBeforeTest: (tester) => mockNetworkImages(() => precacheImages(tester)).timeout(timeout), 48 | pumpWidget: (tester, widget) => mockNetworkImages(() => tester.pumpWidget(widget)).timeout(timeout), 49 | ).timeout(timeout); 50 | } 51 | 52 | @isTest 53 | Future widgetScreenshotTest( 54 | String description, { 55 | String variantName = '', 56 | bool skip = false, 57 | required WidgetBuilder widgetBuilder, 58 | List tags = const ['golden'], 59 | Duration timeout = const Duration(seconds: 5), 60 | }) async { 61 | return goldenTest( 62 | description, 63 | fileName: "$description${variantName.trim().isEmpty ? '' : '_$variantName'}", 64 | builder: () { 65 | return DefaultAssetBundle( 66 | bundle: TestAssetBundle(), 67 | child: Builder(builder: widgetBuilder), 68 | ); 69 | }, 70 | tags: tags, 71 | skip: skip, 72 | pumpBeforeTest: (tester) => mockNetworkImages(() => precacheImages(tester)).timeout(timeout), 73 | pumpWidget: (tester, widget) => mockNetworkImages(() => tester.pumpWidget(widget)).timeout(timeout), 74 | ).timeout(timeout); 75 | } 76 | 77 | /// small helper to add container around widget with some background in order to better understand widget's bounds 78 | class TestWidgetContainer extends StatelessWidget { 79 | const TestWidgetContainer({ 80 | super.key, 81 | required this.child, 82 | }); 83 | 84 | final Widget child; 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return DecoratedBox( 89 | decoration: BoxDecoration( 90 | color: Colors.white70, 91 | border: Border.all(color: Colors.red), 92 | ), 93 | child: child, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/test_utils/test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:alchemist/alchemist.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter_demo/core/helpers.dart'; 6 | import 'package:flutter_demo/core/utils/either_extensions.dart'; 7 | import 'package:flutter_demo/dependency_injection/app_component.dart'; 8 | import 'package:flutter_demo/localization/app_localizations_utils.dart'; 9 | import 'package:flutter_demo/main.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; 11 | import 'package:golden_toolkit/golden_toolkit.dart'; 12 | 13 | import '../mocks/mocks.dart'; 14 | 15 | Future> successFuture(S result) => Future.value(success(result)); 16 | 17 | Future> failFuture(F fail) => Future.value(failure(fail)); 18 | 19 | Future prepareAppForUnitTests() async { 20 | isUnitTests = true; 21 | Mocks.init(); 22 | notImplemented = ({message, context}) => doNothing(); 23 | overrideAppLocalizations(AppLocalizationsEn()); 24 | await configureDependenciesForTests(); 25 | } 26 | 27 | Future configureDependenciesForTests() async { 28 | await getIt.reset(); 29 | configureDependencies(); 30 | } 31 | 32 | Future preparePageTests(FutureOr Function() testMain) async { 33 | overrideAppLocalizations(AppLocalizationsEn()); 34 | await loadAppFonts(); 35 | await prepareAppForUnitTests(); 36 | // ignore: do_not_use_environment 37 | const isCi = bool.fromEnvironment('isCI'); 38 | 39 | return AlchemistConfig.runWithConfig( 40 | config: const AlchemistConfig( 41 | platformGoldensConfig: PlatformGoldensConfig( 42 | // ignore: avoid_redundant_argument_values 43 | enabled: !isCi, 44 | ), 45 | ), 46 | run: () async { 47 | return testMain(); 48 | }, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /tools/arb_files_validator/bin/arb_files_validator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | Future main(List arguments) async { 4 | var directory = Directory(arguments[0]); 5 | var arbFiles = directory 6 | .list() // 7 | .where((entity) => entity.existsSync() && entity.path.endsWith(".arb")); 8 | var hasDuplicates = false; 9 | print("checking arb files validity..."); 10 | await for (final file in arbFiles) { 11 | print("checking '${file.path}'"); 12 | final duplicates = _findDuplicateKeys(File(file.path).readAsStringSync()); 13 | if (duplicates.isNotEmpty) { 14 | hasDuplicates = true; 15 | print("\n## ERROR: duplicates found! [${duplicates.join(",")}]\n"); 16 | } 17 | } 18 | if (hasDuplicates) { 19 | print("duplicates were found in arb file(s) - see logs above\n"); 20 | } else { 21 | print("no duplicates found, yay! :)"); 22 | } 23 | exit(hasDuplicates ? 1 : 0); 24 | } 25 | 26 | List _findDuplicateKeys(String readAsStringSync) { 27 | final allElems = _extractStringKeys(readAsStringSync); 28 | 29 | final tempList = []; 30 | final duplicates = []; 31 | for (final item in allElems) { 32 | if (tempList.contains(item)) { 33 | duplicates.add(item); 34 | } else { 35 | tempList.add(item); 36 | } 37 | } 38 | 39 | return duplicates; 40 | } 41 | 42 | List _extractStringKeys(String json) { 43 | var temp = json.trim().replaceFirst("{", "").trim(); 44 | temp = temp 45 | .substring(0, temp.lastIndexOf("}")) 46 | .replaceAll(RegExp(": \"[^\"]*\""), ": \"\"") 47 | .split("\n") // 48 | .map((e) => e.trim()) 49 | .join("") 50 | .trim(); 51 | var trimmed = temp; 52 | 53 | do { 54 | temp = trimmed; 55 | trimmed = _removeNestedObjects(temp); 56 | } while (trimmed != temp); 57 | final keys = trimmed 58 | .split(",") 59 | .map((e) { 60 | var match = RegExp("[\\'\\\"](.*?)[\\'\\\"]\\s?:\\s?[\\'\\\"](.*?)[\\'\\\"]").firstMatch(e); 61 | if ((match?.groupCount ?? 0) >= 2) { 62 | return match?.group(1)?.trim(); 63 | } else { 64 | return null; 65 | } 66 | }) 67 | .whereType() 68 | .toList(); 69 | return keys; 70 | } 71 | 72 | String _removeNestedObjects(String replaceAll) { 73 | return replaceAll.replaceAll(RegExp("{[^{}]*}"), "\"\""); 74 | } 75 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options.yaml 2 | 3 | analyzer: 4 | plugins: 5 | - dart_code_metrics 6 | - custom_lint 7 | exclude: 8 | 9 | language: 10 | strict-raw-types: true 11 | 12 | strong-mode: 13 | implicit-dynamic: true 14 | errors: 15 | invalid_annotation_target: error 16 | argument_type_not_assignable: error 17 | field_initializer_not_assignable: error 18 | map_value_type_not_assignable: error 19 | invalid_assignment: error 20 | return_of_invalid_type_from_closure: error 21 | return_of_invalid_type: error 22 | unnecessary_new: warning 23 | sort_pub_dependencies: ignore 24 | avoid_setters_without_getters: ignore 25 | import_of_legacy_library_into_null_safe: error 26 | avoid_single_cascade_in_expression_statements: ignore 27 | null_aware_in_logical_operator: error 28 | missing_required_param: error 29 | implicit_dynamic_map_literal: ignore 30 | prefer_single_quotes: ignore 31 | missing_return: error 32 | always_declare_return_types: error 33 | override_on_non_overriding_member: error 34 | annotate_overrides: error 35 | avoid_relative_lib_imports: error 36 | avoid_empty_else: error 37 | avoid_returning_null_for_future: error 38 | empty_statements: error 39 | always_put_control_body_on_new_line: error 40 | always_require_non_null_named_parameters: error 41 | avoid_renaming_method_parameters: error 42 | avoid_void_async: error 43 | parameter_assignments: error 44 | constant_identifier_names: ignore 45 | unawaited_futures: error 46 | non_constant_identifier_names: ignore 47 | only_throw_errors: error 48 | exhaustive_cases: error 49 | always_use_package_imports: error 50 | missing_enum_constant_in_switch: error 51 | prefer_const_constructors: error 52 | depend_on_referenced_packages: ignore 53 | use_setters_to_change_properties: ignore 54 | avoid_classes_with_only_static_members: ignore 55 | avoid_positional_boolean_parameters: error 56 | avoid_dynamic_calls: error 57 | require_trailing_commas: error 58 | 59 | linter: 60 | rules: 61 | - avoid_unnecessary_containers 62 | - no_logic_in_create_state 63 | - constant_identifier_names 64 | - prefer_const_constructors 65 | - prefer_const_constructors_in_immutables 66 | - prefer_const_declarations 67 | - prefer_const_literals_to_create_immutables 68 | - annotate_overrides 69 | - await_only_futures 70 | - camel_case_types 71 | - cancel_subscriptions 72 | - close_sinks 73 | - comment_references 74 | - control_flow_in_finally 75 | - empty_statements 76 | - always_declare_return_types 77 | - avoid_empty_else 78 | - avoid_relative_lib_imports 79 | - avoid_returning_null_for_future 80 | - always_put_control_body_on_new_line 81 | - always_require_non_null_named_parameters 82 | - avoid_renaming_method_parameters 83 | - avoid_void_async 84 | - parameter_assignments 85 | - file_names 86 | - empty_constructor_bodies 87 | - unnecessary_parenthesis 88 | - unnecessary_overrides 89 | - use_rethrow_when_possible 90 | - always_use_package_imports 91 | - avoid_init_to_null 92 | - avoid_null_checks_in_equality_operators 93 | - avoid_return_types_on_setters 94 | - avoid_shadowing_type_parameters 95 | - avoid_types_as_parameter_names 96 | - camel_case_extensions 97 | - curly_braces_in_flow_control_structures 98 | - empty_catches 99 | - library_names 100 | - library_prefixes 101 | - no_duplicate_case_values 102 | - null_closures 103 | - omit_local_variable_types 104 | - prefer_adjacent_string_concatenation 105 | - prefer_collection_literals 106 | - prefer_conditional_assignment 107 | - prefer_contains 108 | - prefer_equal_for_default_values 109 | - prefer_final_fields 110 | - prefer_for_elements_to_map_fromIterable 111 | - prefer_generic_function_type_aliases 112 | - prefer_if_null_operators 113 | - prefer_is_empty 114 | - prefer_is_not_empty 115 | - prefer_iterable_whereType 116 | - prefer_single_quotes 117 | - prefer_spread_collections 118 | - recursive_getters 119 | - slash_for_doc_comments 120 | - type_init_formals 121 | - unawaited_futures 122 | - unnecessary_const 123 | - unnecessary_new 124 | - unnecessary_null_in_if_null_operators 125 | - unnecessary_this 126 | - unrelated_type_equality_checks 127 | - use_function_type_syntax_for_parameters 128 | - valid_regexps 129 | - exhaustive_cases 130 | - require_trailing_commas 131 | 132 | dart_code_metrics: 133 | anti-patterns: 134 | - long-method 135 | - long-parameter-list 136 | metrics: 137 | cyclomatic-complexity: 15 138 | maximum-nesting-level: 3 139 | number-of-parameters: 4 140 | source-lines-of-code: 30 141 | metrics-exclude: 142 | - "test/**" 143 | - "widgetbook/**" 144 | - "**/*.gen.dart" 145 | 146 | rules-exclude: 147 | - "test/**" 148 | - "widgetbook/**" 149 | - "**/*.gen.dart" 150 | rules: 151 | - no-boolean-literal-compare 152 | - no-empty-block 153 | - prefer-trailing-comma: 154 | break-on: 3 155 | - prefer-conditional-expressions 156 | - no-equal-then-else 157 | - avoid-unnecessary-type-casts 158 | - avoid-unnecessary-type-assertions 159 | - no-magic-number 160 | - prefer-first 161 | - prefer-last 162 | - prefer-match-file-name 163 | - avoid-use-expanded-as-spacer 164 | - prefer-extracting-callbacks 165 | - prefer-async-await 166 | - prefer-moving-to-variable 167 | - avoid-returning-widgets 168 | - prefer-correct-identifier-length: 169 | exceptions: [ 'i' ] 170 | max-identifier-length: 40 171 | min-identifier-length: 2 172 | - prefer-correct-type-name: 173 | min-length: 2 174 | max-length: 40 175 | - prefer-single-widget-per-file: 176 | ignore-private-widgets: true 177 | - member-ordering: 178 | order: 179 | - constructors 180 | - public-fields 181 | - private-fields 182 | - public-getters 183 | - private-getters 184 | - public-methods 185 | - private-methods -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/custom_lint.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:analyzer/dart/analysis/results.dart'; 4 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 5 | 6 | import 'lints/domain_entity_missing_copy_with_method_lint.dart'; 7 | import 'lints/domain_entity_missing_empty_constructor_lint.dart'; 8 | import 'lints/domain_entity_missing_equatable_lint.dart'; 9 | import 'lints/domain_entity_missing_props_items_lint.dart'; 10 | import 'lints/domain_entity_non_final_fields_lint.dart'; 11 | import 'lints/domain_entity_too_many_public_members_lint.dart'; 12 | import 'lints/dont_use_datetime_now_lint.dart'; 13 | import 'lints/forbidden_import_in_domain_lint.dart'; 14 | import 'lints/forbidden_import_in_presentation_lint.dart'; 15 | import 'lints/page_too_widgets.dart'; 16 | import 'lints/presentation_model_non_final_field_lint.dart'; 17 | import 'lints/presentation_model_structure_lint.dart'; 18 | import 'lints/use_case_multiple_accessors_lint.dart'; 19 | 20 | void main(List args, SendPort sendPort) { 21 | startPlugin(sendPort, _IndexPlugin()); 22 | } 23 | 24 | class _IndexPlugin extends PluginBase { 25 | final lints = [ 26 | ForbiddenImportInPresentationLint(), 27 | ForbiddenImportInDomainLint(), 28 | PresentationModelNonFinalFieldLint(), 29 | DomainEntityNonFinalFields(), 30 | UseCaseMultipleAccessorsLint(), 31 | DomainEntityMissingCopyWithMethodLint(), 32 | DomainEntityMissingEquatableLint(), 33 | DomainEntityMissingPropsItemsLint(), 34 | DomainEntityMissingEmptyConstructorLint(), 35 | DomainEntityTooManyPublicMembersLint(), 36 | PageTooManyWidgetsLint(), 37 | DontUseDateTimeNowLint(), 38 | PresentationModelStructureLint(), 39 | ]; 40 | 41 | @override 42 | Stream getLints(ResolvedUnitResult resolvedUnitResult) async* { 43 | for (final lint in lints) { 44 | yield* lint.getLints(resolvedUnitResult); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_copy_with_method_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | import '../utils/lint_utils.dart'; 6 | 7 | /// checks whether any domain entity class (anything inside 'domain/model' folder except Failures) 8 | /// contains the 'copyWith' method 9 | class DomainEntityMissingCopyWithMethodLint extends PluginBase { 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | if (!library.isDomainEntityFile) { 14 | return; 15 | } 16 | final entitiesClasses = library.domainEntitiesClasses; 17 | for (final clazz in entitiesClasses) { 18 | final equatableFields = clazz.equatableFields; 19 | final missingCopyWith = clazz.getMethod("copyWith") == null; 20 | 21 | if (equatableFields.isNotEmpty && missingCopyWith) { 22 | yield Lint( 23 | code: LintCodes.missingCopyWithMethod, 24 | message: 'Domain entity is missing "copyWith" method: "${clazz.displayName}"', 25 | location: clazz.nameLintLocation!, 26 | severity: LintSeverity.error, 27 | correction: "Add a copyWith method, ideally generated with" 28 | " VSCode plugin (Dart Data Class Generator)" 29 | " or IntelliJ Plugin (Dart Data Class)", 30 | ); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_empty_constructor_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | import '../utils/lint_utils.dart'; 6 | 7 | /// checks whether any domain entity class (anything inside 'domain/model' folder except Failures) 8 | /// contains the non-parametrized named constructor called 'empty' that creates 9 | /// empty instance of the class with default values 10 | class DomainEntityMissingEmptyConstructorLint extends PluginBase { 11 | @override 12 | Stream getLints(ResolvedUnitResult unit) async* { 13 | final library = unit.libraryElement; 14 | if (!library.isDomainEntityFile) { 15 | return; 16 | } 17 | final entitiesClasses = library.domainEntitiesClasses; 18 | for (final clazz in entitiesClasses) { 19 | final constructor = clazz.getNamedConstructor("empty"); 20 | if (constructor == null) { 21 | yield Lint( 22 | code: LintCodes.missingEmptyConstructor, 23 | message: 'Domain entity is missing "empty" constructor', 24 | location: clazz.nameLintLocation!, 25 | severity: LintSeverity.error, 26 | ); 27 | } else if (constructor.parameters.isNotEmpty) { 28 | yield Lint( 29 | code: LintCodes.emptyConstructorContainsParams, 30 | message: '"empty" constructor contains params, but it shouldn\'t have any', 31 | location: constructor.nameLintLocation!, 32 | severity: LintSeverity.error, 33 | correction: "Add a non-parametrized, named constructor 'empty()'" 34 | " that sets all fields to their default values " 35 | "like 0, null, '' or \$CLASS_NAME\$.empty()", 36 | ); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_equatable_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | import '../utils/lint_utils.dart'; 6 | 7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures) 8 | /// extemds Equatable 9 | class DomainEntityMissingEquatableLint extends PluginBase { 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | if (!library.isDomainEntityFile) { 14 | return; 15 | } 16 | final entitiesClasses = library.domainEntitiesClasses; 17 | for (final clazz in entitiesClasses) { 18 | if (!clazz.allSupertypes.any((it) => it.element.name == 'Equatable' || it.element.name == 'EquatableMixin')) { 19 | yield Lint( 20 | code: LintCodes.missingEquatable, 21 | message: 'Domain entity is not extending Equatable', 22 | location: clazz.nameLintLocation!, 23 | severity: LintSeverity.error, 24 | correction: "make ${clazz.name} extend from Equatable", 25 | ); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_missing_props_items_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | import '../utils/lint_utils.dart'; 6 | 7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures) 8 | /// has all its fields listed in the `props` list used by Equatable to generate `==` operator 9 | class DomainEntityMissingPropsItemsLint extends PluginBase { 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | if (!library.isDomainEntityFile) { 14 | return; 15 | } 16 | final entitiesClasses = library.domainEntitiesClasses; 17 | for (final clazz in entitiesClasses) { 18 | final props = clazz.getGetter("props"); 19 | final propsListElems = clazz.propsListElements; 20 | final equatableFields = clazz.equatableFields; 21 | 22 | if (props != null && equatableFields.length > propsListElems.length) { 23 | final missingFields = equatableFields // 24 | .where((field) => !propsListElems.any((it) => it == field.name)) 25 | .map((e) => e.name) 26 | .toList(); 27 | yield Lint( 28 | code: LintCodes.missingPropsItems, 29 | message: 'props list is missing some fields: $missingFields', 30 | location: unit.lintLocationFromOffset(props.variable.nameOffset, length: props.variable.nameLength), 31 | severity: LintSeverity.error, 32 | correction: "add '${missingFields.join("', '").replaceAll(", '\$", "")} to the `props` list.", 33 | ); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_non_final_fields_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | import '../utils/lint_utils.dart'; 6 | 7 | /// checks whether domain entity class (anything inside 'domain/model' folder except Failures) 8 | /// has all its fields final (making it immutable) 9 | class DomainEntityNonFinalFields extends PluginBase { 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | if (!library.isDomainEntityFile) { 14 | return; 15 | } 16 | final entitiesClasses = library.domainEntitiesClasses; 17 | for (final clazz in entitiesClasses) { 18 | final invalidFields = clazz.fields.where((it) => !it.isFinal && !it.isStatic && !it.isConst); 19 | 20 | for (final field in invalidFields) { 21 | if (field.nameLintLocation != null) { 22 | yield Lint( 23 | code: LintCodes.nonFinalFieldInDomainEntity, 24 | message: 'non-final field in domain entity', 25 | location: field.nameLintLocation!, 26 | severity: LintSeverity.error, 27 | correction: "make '${field.name}' field final", 28 | getAnalysisErrorFixes: field.addFinalErrorFix, 29 | ); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/domain_entity_too_many_public_members_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:analyzer/dart/element/element.dart'; 3 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 4 | import 'package:recase/recase.dart'; 5 | 6 | import '../utils/lint_codes.dart'; 7 | import '../utils/lint_utils.dart'; 8 | 9 | /// checks if domain entity class has only one public member (class,enum, mixin etc.) 10 | class DomainEntityTooManyPublicMembersLint extends PluginBase { 11 | @override 12 | Stream getLints(ResolvedUnitResult unit) async* { 13 | final library = unit.libraryElement; 14 | if (!library.isDomainEntityFile || library.isFailureFile) { 15 | return; 16 | } 17 | final entitiesPublicMembers = library.topLevelElements.whereType().where( 18 | (it) => [ 19 | !it.name.endsWith("Failure"), 20 | it.isPublic, 21 | !library.source.shortName.startsWith(it.name.snakeCase), 22 | ].every((it) => it), 23 | ); 24 | 25 | for (final clazz in entitiesPublicMembers) { 26 | yield Lint( 27 | code: LintCodes.tooManyPublicMembers, 28 | message: "Domain entity's file contains more than one public top level element " 29 | "or the element does not match the file name precisely: ${clazz.name}", 30 | location: clazz.nameLintLocation!, 31 | severity: LintSeverity.error, 32 | correction: "extract ${clazz.name} to separate file", 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/dont_use_datetime_now_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | 4 | import '../utils/lint_codes.dart'; 5 | 6 | /// prevents from using `DateTime.now` anywhere in the code 7 | class DontUseDateTimeNowLint extends PluginBase { 8 | final forbiddenText = "DateTime.now()"; 9 | 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | final source = library.source.contents.data; 14 | var index = 0; 15 | final locations = []; 16 | while (index != -1) { 17 | index = source.indexOf(forbiddenText, index + 1); 18 | if (index != -1) { 19 | locations.add(unit.lintLocationFromOffset(index, length: forbiddenText.length)); 20 | } 21 | } 22 | for (final location in locations) { 23 | yield Lint( 24 | code: LintCodes.noDateTimeNow, 25 | message: "Don't use $forbiddenText in code, use `CurrentTimeProvider` instead", 26 | location: location, 27 | severity: LintSeverity.error, 28 | correction: "Use `CurrentTimeProvider` instead", 29 | ); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/forbidden_import_in_domain_lint.dart: -------------------------------------------------------------------------------- 1 | // This is the entrypoint of our custom linter 2 | import 'package:analyzer/dart/analysis/results.dart'; 3 | import 'package:collection/collection.dart'; 4 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 5 | 6 | import '../utils/lint_codes.dart'; 7 | import '../utils/lint_utils.dart'; 8 | 9 | /// checks whether any domain class (the one that is in `domain` package) contains only allowed imports 10 | /// from [allowedPackages] and does NOT contain any import matching anything from [forbiddenKeywords]. 11 | class ForbiddenImportInDomainLint extends PluginBase { 12 | static const allowedPackages = [ 13 | "dartz", 14 | "collection", 15 | "equatable", // domain entities 16 | "bloc", //for stores 17 | ]; 18 | static const forbiddenKeywords = [ 19 | "/data/", 20 | "/widgets/", 21 | "/ui/", 22 | "presenter.dart", 23 | "presentation_model.dart", 24 | "page.dart", 25 | ]; 26 | 27 | @override 28 | Stream getLints(ResolvedUnitResult unit) async* { 29 | final appPackage = packageFromUri(unit.uri.toString()); 30 | 31 | final library = unit.libraryElement; 32 | 33 | if (appPackage.isEmpty || !unit.uri.toString().contains("domain")) { 34 | return; 35 | } 36 | 37 | final imports = library.nonCoreImports // 38 | .map((it) => MapEntry(it, it.importedLibrary)) 39 | .where((entry) { 40 | final importUri = entry.value?.source.uri.toString() ?? ''; 41 | final importPackage = packageFromUri(importUri); 42 | final onlyAllowedPackages = [...allowedPackages, appPackage].contains(importPackage); 43 | final noForbiddenImports = forbiddenKeywords.none((it) => importUri.contains(it)); 44 | return !onlyAllowedPackages || !noForbiddenImports; 45 | }).toList(); 46 | if (imports.isEmpty) { 47 | return; 48 | } 49 | 50 | for (final entry in imports) { 51 | yield Lint( 52 | code: LintCodes.forbiddenImportInDomain, 53 | message: 'domain can only import app packages or following libraries: "${allowedPackages.join(", ")}"' 54 | ' and cannot contain following keywords : "${forbiddenKeywords.join(", ")}"', 55 | location: unit.lintLocationFromOffset( 56 | entry.key.nameOffset, 57 | length: "import '${entry.key.uri}';".length, 58 | ), 59 | severity: LintSeverity.error, 60 | correction: "remove forbidden imports", 61 | ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/forbidden_import_in_presentation_lint.dart: -------------------------------------------------------------------------------- 1 | // This is the entrypoint of our custom linter 2 | import 'package:analyzer/dart/analysis/results.dart'; 3 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 4 | 5 | import '../utils/lint_codes.dart'; 6 | import '../utils/lint_utils.dart'; 7 | 8 | /// checks whether any presentation class (presenter or presentationModel) contains only allowed imports 9 | /// from [allowedPackages] 10 | class ForbiddenImportInPresentationLint extends PluginBase { 11 | static const allowedPackages = [ 12 | "bloc", 13 | "dartz", 14 | "collection", 15 | ]; 16 | 17 | @override 18 | Stream getLints(ResolvedUnitResult unit) async* { 19 | final library = unit.libraryElement; 20 | if (!library.source.shortName.endsWith("presenter.dart") && 21 | !library.source.shortName.endsWith("presentation_model.dart")) { 22 | return; 23 | } 24 | 25 | final appPackage = packageFromUri(unit.uri.toString()); 26 | final imports = library.nonCoreImports.map((it) => MapEntry(it, it.importedLibrary)).where((lib) { 27 | final package = packageFromUri(lib.value?.source.uri.toString()); 28 | return ![...allowedPackages, appPackage].contains(package); 29 | }).toList(); 30 | if (imports.isEmpty) { 31 | return; 32 | } 33 | 34 | for (final entry in imports) { 35 | yield Lint( 36 | code: LintCodes.forbiddenImportInPresentation, 37 | message: 38 | 'presenter and presentation model can only import app packages or following libraries: "${allowedPackages.join(", ")}"', 39 | location: unit.lintLocationFromOffset( 40 | entry.key.nameOffset, 41 | length: "import '${entry.key.uri}';".length, 42 | ), 43 | correction: "remove forbidden imports", 44 | severity: LintSeverity.error, 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/page_too_widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 3 | import 'package:recase/recase.dart'; 4 | 5 | import '../utils/lint_codes.dart'; 6 | 7 | /// checks if the page file contains more top-level elements than needed 8 | /// (only *Page and `_*PageState classes are expected) 9 | class PageTooManyWidgetsLint extends PluginBase { 10 | @override 11 | Stream getLints(ResolvedUnitResult unit) async* { 12 | final library = unit.libraryElement; 13 | if (!library.source.shortName.endsWith("page.dart")) { 14 | return; 15 | } 16 | final topMembers = library.topLevelElements.where((element) { 17 | final name = element.name ?? ""; 18 | return !name.endsWith("State") && !name.endsWith(library.source.shortName.replaceAll(".dart", "").pascalCase); 19 | }); 20 | if (topMembers.isNotEmpty) { 21 | print("page public elements: [${topMembers.map((e) => e.name).join(", ")}]"); 22 | } 23 | for (final clazz in topMembers) { 24 | yield Lint( 25 | code: LintCodes.tooManyPageElements, 26 | message: "${clazz.name} should not be part of the page's source file " 27 | "or the element does not match the file name precisely: ${clazz.name}", 28 | location: clazz.nameLintLocation!, 29 | severity: LintSeverity.error, 30 | correction: "extract ${clazz.name} to separate file", 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/presentation_model_non_final_field_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:analyzer/dart/element/element.dart'; 3 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 4 | 5 | import '../utils/lint_codes.dart'; 6 | import '../utils/lint_utils.dart'; 7 | 8 | /// checks whether presentationModel class 9 | /// has all its fields final (making it immutable) 10 | class PresentationModelNonFinalFieldLint extends PluginBase { 11 | @override 12 | Stream getLints(ResolvedUnitResult unit) async* { 13 | final library = unit.libraryElement; 14 | if (!library.source.shortName.endsWith("presentation_model.dart")) { 15 | return; 16 | } 17 | final nonFinalFields = library.topLevelElements 18 | .whereType() 19 | .where((element) => element.name.endsWith("PresentationModel")) 20 | .expand((clazz) => clazz.fields.where((field) => !field.isFinal && !field.isStatic && !field.isConst)); 21 | 22 | if (nonFinalFields.isEmpty) { 23 | return; 24 | } 25 | 26 | for (final field in nonFinalFields) { 27 | if (field.nameLintLocation != null) { 28 | yield Lint( 29 | code: LintCodes.presentationModelNonFinalField, 30 | message: 'PresentationModel can have only final fields: "${field.displayName}"', 31 | location: field.nameLintLocation!, 32 | severity: LintSeverity.error, 33 | correction: "make ${field.name} final", 34 | getAnalysisErrorFixes: field.addFinalErrorFix, 35 | ); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/presentation_model_structure_lint.dart: -------------------------------------------------------------------------------- 1 | // This is the entrypoint of our custom linter 2 | import 'package:analyzer/dart/analysis/results.dart'; 3 | import 'package:analyzer/dart/element/element.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 6 | 7 | import '../utils/lint_codes.dart'; 8 | import '../utils/lint_utils.dart'; 9 | 10 | /// checks if presentation model contains proper structure 11 | class PresentationModelStructureLint extends PluginBase { 12 | static const allowedPackages = [ 13 | "bloc", 14 | "dartz", 15 | "collection", 16 | ]; 17 | 18 | @override 19 | Stream getLints(ResolvedUnitResult unit) async* { 20 | final library = unit.libraryElement; 21 | if (!library.source.shortName.endsWith("presentation_model.dart")) { 22 | return; 23 | } 24 | 25 | final modelClass = library.topLevelElements // 26 | .whereType() 27 | .firstWhereOrNull((it) => it.name.endsWith("PresentationModel")); 28 | final constructor = modelClass // 29 | ?.constructors 30 | .firstWhereOrNull((element) => element.name == '_'); 31 | final invalidConstructorParams = 32 | constructor?.parameters.where((it) => !it.isRequired || !it.isNamed || it.defaultValueCode != null) ?? []; 33 | if (modelClass != null) { 34 | if (constructor == null) { 35 | yield Lint( 36 | code: LintCodes.presentationModelStructure, 37 | message: 'missing a named constructor `_`', 38 | location: modelClass.nameLintLocation!, 39 | correction: "add named constructor `_` to ${modelClass.name}", 40 | severity: LintSeverity.error, 41 | ); 42 | } else if (constructor.parameters.length != modelClass.equatableFields.length) { 43 | final missingFields = modelClass.equatableFields 44 | .where((field) => constructor.parameters.none((param) => param.name == field.name)); 45 | yield Lint( 46 | code: LintCodes.presentationModelStructure, 47 | message: 'some fields are missing in the `_` constructor', 48 | location: constructor.nameLintLocation!, 49 | correction: "add ${missingFields.join(", ")} to the constructor", 50 | severity: LintSeverity.error, 51 | ); 52 | } 53 | for (final param in invalidConstructorParams) { 54 | yield Lint( 55 | code: LintCodes.presentationModelStructure, 56 | message: '`${param.name}` constructor param is not marked required and/or named, or contains default value', 57 | location: param.nameLintLocation!, 58 | correction: "make `${param.name}` a required, named parameter with no default value", 59 | severity: LintSeverity.error, 60 | ); 61 | } 62 | if (modelClass.getMethod("copyWith") == null) { 63 | yield Lint( 64 | code: LintCodes.presentationModelStructure, 65 | message: 'missing `copyWith` method', 66 | location: modelClass.nameLintLocation!, 67 | correction: "add `copyWith` method to ${modelClass.name}", 68 | severity: LintSeverity.error, 69 | ); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/lints/use_case_multiple_accessors_lint.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/analysis/results.dart'; 2 | import 'package:analyzer/dart/element/element.dart'; 3 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 4 | 5 | import '../utils/lint_codes.dart'; 6 | 7 | /// checks whether a useCase class contains only one public accessor, a method, called 'execute' 8 | class UseCaseMultipleAccessorsLint extends PluginBase { 9 | @override 10 | Stream getLints(ResolvedUnitResult unit) async* { 11 | final library = unit.libraryElement; 12 | if (!library.source.shortName.endsWith("use_case.dart")) { 13 | return; 14 | } 15 | final useCaseClass = library.topLevelElements.whereType().first; 16 | final publicFields = useCaseClass.fields.where((element) => element.isPublic && !element.isConst); 17 | final publicMethods = useCaseClass.methods.where((element) => element.isPublic); 18 | 19 | for (final field in publicFields) { 20 | if (field.nameLintLocation != null) { 21 | yield Lint( 22 | code: LintCodes.publicFieldInUseCase, 23 | message: 'use case cannot expose public fields, only one execute() method', 24 | location: field.nameLintLocation!, 25 | severity: LintSeverity.error, 26 | ); 27 | } 28 | } 29 | for (final method in publicMethods) { 30 | if (method.name != "execute") { 31 | yield Lint( 32 | code: LintCodes.publicMethodInUseCase, 33 | message: 'use case can only expose one public method called "execute"', 34 | location: method.nameLintLocation!, 35 | severity: LintSeverity.error, 36 | correction: "remove or rename ${method.name} to 'execute'", 37 | ); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/utils/lint_codes.dart: -------------------------------------------------------------------------------- 1 | class LintCodes { 2 | static const forbiddenImportInPresentation = 'forbidden_import_in_presentation'; 3 | static const forbiddenImportInDomain = 'forbidden_import_in_domain'; 4 | static const missingCopyWithMethod = 'missing_copy_with_method'; 5 | static const missingEquatable = 'missing_equatable'; 6 | static const missingPropsItems = 'missing_props_items'; 7 | static const nonFinalFieldInDomainEntity = 'non_final_field_in_domain_entity'; 8 | static const presentationModelNonFinalField = 'presentation_model_non_final_field'; 9 | static const missingEmptyConstructor = 'missing_empty_constructor'; 10 | static const emptyConstructorContainsParams = 'empty_constructor_contains_params'; 11 | static const publicFieldInUseCase = 'public_field_in_use_case'; 12 | static const publicMethodInUseCase = 'public_method_in_use_case'; 13 | static const tooManyPublicMembers = 'too_many_public_members'; 14 | static const tooManyPageElements = 'too_many_page_file_members'; 15 | static const noDateTimeNow = 'no_date_time_now'; 16 | static const presentationModelStructure = 'presentation_model_structure'; 17 | } 18 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/bin/utils/lint_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/element/element.dart'; 2 | import 'package:analyzer_plugin/protocol/protocol_generated.dart'; 3 | import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; 4 | import 'package:custom_lint_builder/custom_lint_builder.dart'; 5 | 6 | /// returns the package name for given uri, for example `package:bloc/bloc.dart` will return `bloc` 7 | String packageFromUri(String? uri) { 8 | if (uri == null) { 9 | return ''; 10 | } 11 | try { 12 | return uri.substring(uri.indexOf(":") + 1, uri.indexOf("/")); 13 | } catch (ex) { 14 | return ''; 15 | } 16 | } 17 | 18 | /// List of fields that should be part of props list for Equatable class 19 | extension ClassElementExtensions on ClassElement { 20 | Iterable get equatableFields => 21 | fields.where((field) => !field.isStatic && !field.isAbstract && (field.getter?.isSynthetic ?? false)); 22 | 23 | /// Returns list of all elements in the `List get props` getter 24 | List get propsListElements { 25 | final props = getGetter("props"); 26 | final data = props?.variable.source?.contents.data; 27 | if (data == null) { 28 | return []; 29 | } 30 | try { 31 | const startMarker = " props => ["; 32 | 33 | final startIndex = data.indexOf(startMarker) + startMarker.length; 34 | final endIndex = data.indexOf('];', startIndex); 35 | 36 | final substring = data.substring(startIndex, endIndex); 37 | final variables = substring.split(",").map((e) => e.trim()).where((element) => element.isNotEmpty); 38 | return variables.toList(); 39 | } catch (ex) { 40 | return []; 41 | } 42 | } 43 | } 44 | 45 | extension LibraryElementExtensions on Element { 46 | /// returns offset to the first character in the start line 47 | int get startLineOffset { 48 | final location = nameLintLocation; 49 | final unit = library?.definingCompilationUnit; 50 | if (location == null || unit == null) { 51 | return -1; 52 | } 53 | 54 | final startOfLine = unit.lineInfo.getOffsetOfLine(location.startLine - 1); // line counting starts from 1 not 0 55 | final indexOfFirstCharInLine = unit.source.contents.data.indexOf(RegExp(r"\S"), startOfLine); 56 | return indexOfFirstCharInLine == -1 ? startOfLine : indexOfFirstCharInLine; 57 | } 58 | 59 | /// prepares analysisErrorFix that adds `final` keyword at the beginning of the line for this element. 60 | Stream Function(Lint lint)? get addFinalErrorFix => (lint) async* { 61 | final changesBuilder = ChangeBuilder(session: library!.definingCompilationUnit.session); 62 | await changesBuilder.addDartFileEdit( 63 | library!.source.fullName, 64 | (builder) { 65 | return builder.addSimpleInsertion( 66 | startLineOffset, 67 | "final ", 68 | ); 69 | }, 70 | ); 71 | yield AnalysisErrorFixes( 72 | lint.asAnalysisError(), 73 | fixes: [ 74 | PrioritizedSourceChange( 75 | 3, 76 | changesBuilder.sourceChange..message = "Add final keyword", 77 | ), 78 | ], 79 | ); 80 | }; 81 | 82 | bool get isDomainEntityFile => library?.source.fullName.contains("domain/model") ?? false; 83 | 84 | bool get isFailureFile => library?.source.shortName.endsWith("failure.dart") ?? false; 85 | 86 | List get domainEntitiesClasses => 87 | library?.topLevelElements // 88 | .whereType() 89 | .where( 90 | (it) => [ 91 | !it.name.endsWith("Failure"), 92 | !it.isDartCoreEnum, 93 | !it.isMixin, 94 | !it.isEnum, 95 | !it.isAbstract, 96 | ].every((it) => it), 97 | ) 98 | .toList() ?? 99 | []; 100 | 101 | Iterable get nonCoreImports => 102 | library?.libraryImports.where((element) { 103 | final lib = element.importedLibrary; 104 | if (lib == null) { 105 | return false; 106 | } 107 | return !lib.isDartAsync && // 108 | !lib.isDartCore && 109 | !lib.isInSdk; 110 | }) ?? 111 | []; 112 | } 113 | -------------------------------------------------------------------------------- /tools/custom_lints/clean_architecture_lints/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: clean_architecture_lints 2 | environment: 3 | sdk: '>=2.16.0 <3.0.0' 4 | 5 | dependency_overrides: 6 | recase: 4.0.0 7 | 8 | dependencies: 9 | collection: 10 | # we will use analyzer for inspecting Dart files 11 | analyzer: 4.3.1 12 | analyzer_plugin: 0.10.0 13 | # custom_lint_builder will give us tools for writing lints 14 | custom_lint: 15 | git: 16 | url: https://github.com/andrzejchm/dart_custom_lint.git 17 | path: packages/custom_lint 18 | ref: main 19 | 20 | custom_lint_builder: 21 | git: 22 | url: https://github.com/andrzejchm/dart_custom_lint.git 23 | path: packages/custom_lint_builder 24 | ref: main 25 | 26 | 27 | dev_dependencies: 28 | lint: 1.8.2 --------------------------------------------------------------------------------