├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.md
├── CODEOWNERS
├── workflows
│ ├── ci.yaml
│ ├── flutterprint.yaml
│ └── generate_flutterprint.yaml
├── dependabot.yaml
└── PULL_REQUEST_TEMPLATE.md
├── analysis_options.yaml
├── src
└── my_app
│ ├── lib
│ ├── widgets
│ │ ├── widgets.dart
│ │ ├── backend_env_banner.dart
│ │ └── countdown_trigger.dart
│ ├── services
│ │ ├── rest_api_service
│ │ │ ├── domain
│ │ │ │ ├── token_type.dart
│ │ │ │ ├── api_method.dart
│ │ │ │ ├── api_status.dart
│ │ │ │ └── api_failure.dart
│ │ │ ├── rest_api_service.dart
│ │ │ └── data
│ │ │ │ └── rest_api_service.dart
│ │ ├── local_storage
│ │ │ ├── local_storage.dart
│ │ │ └── data
│ │ │ │ ├── fake_secure_storage_repository.dart
│ │ │ │ └── shared_preference_repository.dart
│ │ └── snackbar
│ │ │ └── app_snackbar_repository.dart
│ ├── features
│ │ ├── start_up
│ │ │ ├── start_up.dart
│ │ │ ├── data
│ │ │ │ └── start_up_repository.dart
│ │ │ └── presentation
│ │ │ │ └── splash_page.dart
│ │ ├── backend_environment
│ │ │ ├── backend_environment.dart
│ │ │ ├── domain
│ │ │ │ ├── endpoints.dart
│ │ │ │ ├── backend_env.dart
│ │ │ │ └── host.dart
│ │ │ └── application
│ │ │ │ └── host_controller.dart
│ │ ├── authentication
│ │ │ ├── authentication.dart
│ │ │ ├── domain
│ │ │ │ ├── auth_state.dart
│ │ │ │ └── auth_info.dart
│ │ │ └── data
│ │ │ │ ├── fake_auth_repository.dart
│ │ │ │ └── auth_repository.dart
│ │ ├── theme_mode
│ │ │ └── application
│ │ │ │ └── theme_mode_controller.dart
│ │ └── home
│ │ │ └── presentation
│ │ │ └── home_page.dart
│ ├── utils
│ │ ├── utils.dart
│ │ └── validators
│ │ │ ├── local_host_validator_mixin.dart
│ │ │ ├── account_password_validators_mixin.dart
│ │ │ └── string_validator.dart
│ ├── constant
│ │ ├── regex_pattern.dart
│ │ ├── theme
│ │ │ ├── theme.dart
│ │ │ └── color_schemes.dart
│ │ └── app_sizes.dart
│ ├── l10n
│ │ ├── l10n.dart
│ │ └── arb
│ │ │ ├── app_zh.arb
│ │ │ └── app_en.arb
│ └── routing
│ │ └── not_found_page.dart
│ ├── ios
│ ├── Runner
│ │ ├── Runner-Bridging-Header.h
│ │ ├── Assets.xcassets
│ │ │ ├── LaunchImage.imageset
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ ├── README.md
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ └── Info.plist
│ ├── Flutter
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── AppFrameworkInfo.plist
│ ├── Runner.xcodeproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── .gitignore
│ └── Podfile
│ ├── web
│ ├── favicon.png
│ ├── icons
│ │ ├── Icon-192.png
│ │ ├── Icon-512.png
│ │ └── favicon.png
│ └── manifest.json
│ ├── android
│ ├── gradle.properties
│ ├── app
│ │ └── src
│ │ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── values
│ │ │ │ │ └── styles.xml
│ │ │ │ └── values-night
│ │ │ │ │ └── styles.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── flutterprint
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── .gitignore
│ ├── settings.gradle
│ └── build.gradle
│ ├── l10n.yaml
│ ├── scripts
│ ├── remove_gen_files_in_lcov.sh
│ ├── test.sh
│ └── import_files_coverage.sh
│ ├── test
│ ├── fake_model.dart
│ ├── mocks.dart
│ ├── utils
│ │ └── validators
│ │ │ └── string_validator_test.dart
│ ├── robot.dart
│ └── features
│ │ ├── authentication
│ │ ├── auth_robot.dart
│ │ └── auth_flow_test.dart
│ │ └── backend_environment
│ │ └── backend_robot.dart
│ ├── .github
│ ├── dependabot.yaml
│ ├── workflows
│ │ └── main.yaml
│ └── PULL_REQUEST_TEMPLATE.md
│ ├── .vscode
│ └── extensions.json
│ ├── .idea
│ └── runConfigurations
│ │ └── main_dart.xml
│ ├── .metadata
│ ├── LICENSE
│ ├── coverage_badge.svg
│ └── analysis_options.yaml
├── brick
├── __brick__
│ └── {{project_name.snakeCase()}}
│ │ ├── lib
│ │ ├── widgets
│ │ │ ├── widgets.dart
│ │ │ ├── backend_env_banner.dart
│ │ │ └── countdown_trigger.dart
│ │ ├── services
│ │ │ ├── rest_api_service
│ │ │ │ ├── domain
│ │ │ │ │ ├── token_type.dart
│ │ │ │ │ ├── api_method.dart
│ │ │ │ │ ├── api_status.dart
│ │ │ │ │ └── api_failure.dart
│ │ │ │ ├── rest_api_service.dart
│ │ │ │ └── data
│ │ │ │ │ └── rest_api_service.dart
│ │ │ ├── local_storage
│ │ │ │ ├── local_storage.dart
│ │ │ │ └── data
│ │ │ │ │ ├── fake_secure_storage_repository.dart
│ │ │ │ │ └── shared_preference_repository.dart
│ │ │ └── snackbar
│ │ │ │ └── app_snackbar_repository.dart
│ │ ├── features
│ │ │ ├── start_up
│ │ │ │ ├── start_up.dart
│ │ │ │ ├── data
│ │ │ │ │ └── start_up_repository.dart
│ │ │ │ └── presentation
│ │ │ │ │ └── splash_page.dart
│ │ │ ├── backend_environment
│ │ │ │ ├── backend_environment.dart
│ │ │ │ ├── domain
│ │ │ │ │ ├── endpoints.dart
│ │ │ │ │ ├── backend_env.dart
│ │ │ │ │ └── host.dart
│ │ │ │ └── application
│ │ │ │ │ └── host_controller.dart
│ │ │ ├── authentication
│ │ │ │ ├── authentication.dart
│ │ │ │ ├── domain
│ │ │ │ │ ├── auth_state.dart
│ │ │ │ │ └── auth_info.dart
│ │ │ │ └── data
│ │ │ │ │ ├── fake_auth_repository.dart
│ │ │ │ │ └── auth_repository.dart
│ │ │ ├── theme_mode
│ │ │ │ └── application
│ │ │ │ │ └── theme_mode_controller.dart
│ │ │ └── home
│ │ │ │ └── presentation
│ │ │ │ └── home_page.dart
│ │ ├── utils
│ │ │ ├── utils.dart
│ │ │ └── validators
│ │ │ │ ├── local_host_validator_mixin.dart
│ │ │ │ ├── account_password_validators_mixin.dart
│ │ │ │ └── string_validator.dart
│ │ ├── constant
│ │ │ ├── regex_pattern.dart
│ │ │ ├── theme
│ │ │ │ ├── theme.dart
│ │ │ │ └── color_schemes.dart
│ │ │ └── app_sizes.dart
│ │ ├── l10n
│ │ │ ├── l10n.dart
│ │ │ └── arb
│ │ │ │ ├── app_zh.arb
│ │ │ │ └── app_en.arb
│ │ └── routing
│ │ │ └── not_found_page.dart
│ │ ├── ios
│ │ ├── Runner
│ │ │ ├── Runner-Bridging-Header.h
│ │ │ ├── Assets.xcassets
│ │ │ │ ├── LaunchImage.imageset
│ │ │ │ │ ├── LaunchImage.png
│ │ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ │ ├── README.md
│ │ │ │ │ └── Contents.json
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ │ ├── AppDelegate.swift
│ │ │ ├── Base.lproj
│ │ │ │ ├── Main.storyboard
│ │ │ │ └── LaunchScreen.storyboard
│ │ │ └── Info.plist
│ │ ├── Flutter
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ └── AppFrameworkInfo.plist
│ │ ├── Runner.xcodeproj
│ │ │ └── project.xcworkspace
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata
│ │ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ ├── Runner.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ ├── .gitignore
│ │ └── Podfile
│ │ ├── android
│ │ ├── gradle.properties
│ │ ├── app
│ │ │ └── src
│ │ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── drawable
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── drawable-v21
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── values
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── values-night
│ │ │ │ │ │ └── styles.xml
│ │ │ │ ├── kotlin
│ │ │ │ │ └── {{org_name.pathCase()}}
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── AndroidManifest.xml
│ │ │ │ ├── debug
│ │ │ │ └── AndroidManifest.xml
│ │ │ │ └── profile
│ │ │ │ └── AndroidManifest.xml
│ │ ├── gradle
│ │ │ └── wrapper
│ │ │ │ └── gradle-wrapper.properties
│ │ ├── .gitignore
│ │ ├── settings.gradle
│ │ └── build.gradle
│ │ ├── web
│ │ ├── favicon.png
│ │ ├── icons
│ │ │ ├── favicon.png
│ │ │ ├── Icon-192.png
│ │ │ └── Icon-512.png
│ │ └── manifest.json
│ │ ├── l10n.yaml
│ │ ├── scripts
│ │ ├── remove_gen_files_in_lcov.sh
│ │ ├── test.sh
│ │ └── import_files_coverage.sh
│ │ ├── test
│ │ ├── fake_model.dart
│ │ ├── mocks.dart
│ │ ├── utils
│ │ │ └── validators
│ │ │ │ └── string_validator_test.dart
│ │ ├── features
│ │ │ ├── authentication
│ │ │ │ ├── auth_robot.dart
│ │ │ │ └── auth_flow_test.dart
│ │ │ └── backend_environment
│ │ │ │ └── backend_robot.dart
│ │ └── robot.dart
│ │ ├── .github
│ │ ├── dependabot.yaml
│ │ ├── workflows
│ │ │ └── main.yaml
│ │ └── PULL_REQUEST_TEMPLATE.md
│ │ ├── .vscode
│ │ └── extensions.json
│ │ ├── .idea
│ │ └── runConfigurations
│ │ │ └── main_dart.xml
│ │ ├── .metadata
│ │ ├── coverage_badge.svg
│ │ └── analysis_options.yaml
├── hooks
│ ├── pubspec.yaml
│ ├── pre_gen.dart
│ └── post_gen.dart
├── CHANGELOG.md
├── brick.yaml
└── LICENSE
├── images
└── flutterprint_preview.gif
├── tool
└── generator
│ ├── static
│ └── MainActivity.kt
│ ├── pubspec.yaml
│ └── .gitignore
└── .gitignore
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | analyzer:
2 | exclude: [brick/__brick__/**]
3 |
--------------------------------------------------------------------------------
/src/my_app/lib/widgets/widgets.dart:
--------------------------------------------------------------------------------
1 | export 'backend_env_banner.dart';
2 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Every request must be reviewed and accepted by:
2 |
3 | * @ycchuang305
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/widgets.dart:
--------------------------------------------------------------------------------
1 | export 'backend_env_banner.dart';
2 |
--------------------------------------------------------------------------------
/src/my_app/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/favicon.png
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/domain/token_type.dart:
--------------------------------------------------------------------------------
1 | enum TokenType {
2 | none,
3 | account,
4 | }
5 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/images/flutterprint_preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/images/flutterprint_preview.gif
--------------------------------------------------------------------------------
/src/my_app/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/src/my_app/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/src/my_app/web/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/web/icons/favicon.png
--------------------------------------------------------------------------------
/src/my_app/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/start_up/start_up.dart:
--------------------------------------------------------------------------------
1 | export 'data/start_up_repository.dart';
2 | export 'presentation/splash_page.dart';
3 |
--------------------------------------------------------------------------------
/src/my_app/lib/utils/utils.dart:
--------------------------------------------------------------------------------
1 | export 'validators/account_password_validators_mixin.dart';
2 | export 'validators/string_validator.dart';
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/token_type.dart:
--------------------------------------------------------------------------------
1 | enum TokenType {
2 | none,
3 | account,
4 | }
5 |
--------------------------------------------------------------------------------
/src/my_app/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/brick/hooks/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutterprint_hooks
2 |
3 | environment:
4 | sdk: ">=2.17.0 <3.0.0"
5 |
6 | dependencies:
7 | mason: ^0.1.0-dev.40
8 |
--------------------------------------------------------------------------------
/src/my_app/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/src/my_app/lib/constant/regex_pattern.dart:
--------------------------------------------------------------------------------
1 | class RegexPattern {
2 | static const ipAddressPort = r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$';
3 | }
4 |
--------------------------------------------------------------------------------
/src/my_app/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n/arb
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 | nullable-getter: false
5 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/local_storage/local_storage.dart:
--------------------------------------------------------------------------------
1 | export 'data/secure_storage_repository.dart';
2 | export 'data/shared_preference_repository.dart';
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/start_up.dart:
--------------------------------------------------------------------------------
1 | export 'data/start_up_repository.dart';
2 | export 'presentation/splash_page.dart';
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/utils/utils.dart:
--------------------------------------------------------------------------------
1 | export 'validators/account_password_validators_mixin.dart';
2 | export 'validators/string_validator.dart';
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/favicon.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/constant/regex_pattern.dart:
--------------------------------------------------------------------------------
1 | class RegexPattern {
2 | static const ipAddressPort = r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$';
3 | }
4 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/local_storage.dart:
--------------------------------------------------------------------------------
1 | export 'data/secure_storage_repository.dart';
2 | export 'data/shared_preference_repository.dart';
3 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/web/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/favicon.png
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n/arb
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 | nullable-getter: false
5 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/src/my_app/scripts/remove_gen_files_in_lcov.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lcov --remove coverage/lcov.info 'lib/*/*.freezed.dart' 'lib/*/*.g.dart' 'lib/*/*.config.g.dart' 'lib/*/*.part.dart' 'lib/*/*.gr.dart' -o coverage/lcov.info
--------------------------------------------------------------------------------
/tool/generator/static/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package {{org_name.dotCase()}}.{{project_name.snakeCase()}}
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/backend_environment/backend_environment.dart:
--------------------------------------------------------------------------------
1 | export 'application/host_controller.dart';
2 | export 'domain/backend_env.dart';
3 | export 'domain/endpoints.dart';
4 | export 'domain/host.dart';
5 |
--------------------------------------------------------------------------------
/src/my_app/test/fake_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/features/authentication/authentication.dart';
2 |
3 | class FakeModel {
4 | static const authInfo = AuthInfo(
5 | accessToken: 'foo',
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/src/my_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .atom/
3 | .idea/*
4 | .vscode/*
5 |
6 | # Files and directories created by pub
7 | .dart_tool/
8 | .packages
9 | pubspec.lock
10 |
11 | # Conventional directory for build outputs
12 | build/
13 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/kotlin/com/example/flutterprint/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.flutterPrint
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/tool/generator/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: generator
2 | description: A template generator for Very Good CLI.
3 | publish_to: none
4 |
5 | environment:
6 | sdk: ">=2.12.0-0 <3.0.0"
7 |
8 | dependencies:
9 | path: ^1.8.0
10 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1
11 |
--------------------------------------------------------------------------------
/src/my_app/lib/constant/theme/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | export 'color_schemes.dart';
4 |
5 | extension ColorSchemesX on BuildContext {
6 | ColorScheme get colorScheme => Theme.of(this).colorScheme;
7 | }
8 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/scripts/remove_gen_files_in_lcov.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lcov --remove coverage/lcov.info 'lib/*/*.freezed.dart' 'lib/*/*.g.dart' 'lib/*/*.config.g.dart' 'lib/*/*.part.dart' 'lib/*/*.gr.dart' -o coverage/lcov.info
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/backend_environment.dart:
--------------------------------------------------------------------------------
1 | export 'application/host_controller.dart';
2 | export 'domain/backend_env.dart';
3 | export 'domain/endpoints.dart';
4 | export 'domain/host.dart';
5 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/my_app/lib/features/authentication/authentication.dart:
--------------------------------------------------------------------------------
1 | export 'domain/auth_info.dart';
2 | export 'domain/auth_state.dart';
3 | export 'data/auth_repository.dart';
4 | export 'application/auth_controller.dart';
5 | export 'presentation/sign_in_page.dart';
6 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/fake_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
2 |
3 | class FakeModel {
4 | static const authInfo = AuthInfo(
5 | accessToken: 'foo',
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/constant/theme/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | export 'color_schemes.dart';
4 |
5 | extension ColorSchemesX on BuildContext {
6 | ColorScheme get colorScheme => Theme.of(this).colorScheme;
7 | }
8 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/src/my_app/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "pub"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/backend_environment/domain/endpoints.dart:
--------------------------------------------------------------------------------
1 | enum Endpoint {
2 | // TODO: Manage your api endpoints
3 | signIn('path/to/your/api/endpoint'),
4 | refreshToken('path/to/your/api/endpoint');
5 |
6 | const Endpoint(this.path);
7 | final String path;
8 | }
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/kotlin/{{org_name.pathCase()}}/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package {{org_name.dotCase()}}.{{project_name.snakeCase()}}
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycchuang305/flutterprint/HEAD/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/authentication.dart:
--------------------------------------------------------------------------------
1 | export 'domain/auth_info.dart';
2 | export 'domain/auth_state.dart';
3 | export 'data/auth_repository.dart';
4 | export 'application/auth_controller.dart';
5 | export 'presentation/sign_in_page.dart';
6 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "pub"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/endpoints.dart:
--------------------------------------------------------------------------------
1 | enum Endpoint {
2 | // TODO: Manage your api endpoints
3 | signIn('path/to/your/api/endpoint'),
4 | refreshToken('path/to/your/api/endpoint');
5 |
6 | const Endpoint(this.path);
7 | final String path;
8 | }
9 |
--------------------------------------------------------------------------------
/src/my_app/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dart-code.dart-code",
6 | "dart-code.flutter",
7 | "robert-brunhage.flutter-riverpod-snippets"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/my_app/lib/l10n/l10n.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 |
4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 |
6 | extension AppLocalizationsX on BuildContext {
7 | AppLocalizations get l10n => AppLocalizations.of(this);
8 | }
9 |
--------------------------------------------------------------------------------
/src/my_app/.idea/runConfigurations/main_dart.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tool/generator/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub
2 | .dart_tool/
3 | .packages
4 | pubspec.lock
5 |
6 | # Conventional directory for build outputs
7 | build/
8 |
9 | # Directory created by dartdoc
10 | doc/api/
11 |
12 | # Temporary Files
13 | .tmp/
14 |
15 | # Files generated during tests
16 | .test_coverage.dart
17 | coverage/
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dart-code.dart-code",
6 | "dart-code.flutter",
7 | "robert-brunhage.flutter-riverpod-snippets"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/l10n.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 |
4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 |
6 | extension AppLocalizationsX on BuildContext {
7 | AppLocalizations get l10n => AppLocalizations.of(this);
8 | }
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.idea/runConfigurations/main_dart.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/my_app/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/src/my_app/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.
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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.
--------------------------------------------------------------------------------
/src/my_app/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: flutterprint
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | semantic-pull-request:
7 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1
8 |
9 | build:
10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
11 | with:
12 | flutter_channel: stable
13 | flutter_version: 3.7.2
14 | min_coverage: 80
15 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/domain/api_method.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'api_method.freezed.dart';
4 |
5 | @freezed
6 | class ApiMethod with _$ApiMethod {
7 | const factory ApiMethod.get() = _Get;
8 | const factory ApiMethod.post() = _Post;
9 | const factory ApiMethod.put() = _Put;
10 | const factory ApiMethod.patch() = _Patch;
11 | const factory ApiMethod.delete() = _Delete;
12 | }
13 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_method.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'api_method.freezed.dart';
4 |
5 | @freezed
6 | class ApiMethod with _$ApiMethod {
7 | const factory ApiMethod.get() = _Get;
8 | const factory ApiMethod.post() = _Post;
9 | const factory ApiMethod.put() = _Put;
10 | const factory ApiMethod.patch() = _Patch;
11 | const factory ApiMethod.delete() = _Delete;
12 | }
13 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/domain/api_status.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'api_status.freezed.dart';
5 |
6 | @freezed
7 | class ApiStatus with _$ApiStatus {
8 | const factory ApiStatus.loading() = _Loading;
9 | const factory ApiStatus.success() = _Success;
10 | const factory ApiStatus.failure(ApiFailure apiFailure) = _ApiFailure;
11 | }
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "pub"
8 | directory: "/brick/hooks"
9 | schedule:
10 | interval: "daily"
11 | - package-ecosystem: "pub"
12 | directory: "/tool/generator"
13 | schedule:
14 | interval: "daily"
15 | - package-ecosystem: "pub"
16 | directory: "/src/my_app"
17 | schedule:
18 | interval: "daily"
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: {{project_name.snakeCase()}}
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | semantic-pull-request:
7 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1
8 |
9 | build:
10 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
11 | with:
12 | flutter_channel: stable
13 | flutter_version: 3.7.2
14 | min_coverage: 80
15 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_status.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'api_status.freezed.dart';
5 |
6 | @freezed
7 | class ApiStatus with _$ApiStatus {
8 | const factory ApiStatus.loading() = _Loading;
9 | const factory ApiStatus.success() = _Success;
10 | const factory ApiStatus.failure(ApiFailure apiFailure) = _ApiFailure;
11 | }
12 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/src/my_app/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # create a helper file to make coverage work for all dart files except the generated files
3 | sh scripts/import_files_coverage.sh flutterprint
4 |
5 | # run tests and generate a coverage file (at /coverage/lcov.info)
6 | flutter test --coverage --test-randomize-ordering-seed random
7 |
8 | # remove all generated files in lcov.info
9 | sh scripts/remove_gen_files_in_lcov.sh
10 |
11 | # generate coverage info
12 | genhtml -o coverage coverage/lcov.info
13 |
14 | # open to see coverage info
15 | open coverage/index.html
16 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # create a helper file to make coverage work for all dart files except the generated files
3 | sh scripts/import_files_coverage.sh {{project_name.snakeCase()}}
4 |
5 | # run tests and generate a coverage file (at /coverage/lcov.info)
6 | flutter test --coverage --test-randomize-ordering-seed random
7 |
8 | # remove all generated files in lcov.info
9 | sh scripts/remove_gen_files_in_lcov.sh
10 |
11 | # generate coverage info
12 | genhtml -o coverage coverage/lcov.info
13 |
14 | # open to see coverage info
15 | open coverage/index.html
16 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/authentication/domain/auth_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'auth_state.freezed.dart';
5 |
6 | @freezed
7 | class AuthState with _$AuthState {
8 | const factory AuthState.authenticating() = _Authenticating;
9 | const factory AuthState.unauthenticated() = _Unauthenticated;
10 | const factory AuthState.unknown() = _Unknown;
11 | const factory AuthState.authenticated() = _Authenticated;
12 | const factory AuthState.failure(ApiFailure failure) = _Failure;
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a report to help us improve
4 | title: "fix: "
5 | labels: bug
6 | ---
7 |
8 | **Description**
9 | A clear and concise description of what the bug is.
10 |
11 | **Steps To Reproduce**
12 |
13 | 1. Go to '...'
14 | 2. Click on '....'
15 | 3. Scroll down to '....'
16 | 4. See error
17 |
18 | **Expected Behavior**
19 | A clear and concise description of what you expected to happen.
20 |
21 | **Screenshots**
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | **Additional Context**
25 | Add any other context about the problem here.
26 |
--------------------------------------------------------------------------------
/brick/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.1.5
2 |
3 | - feat: upgrade to Flutter 3.7.2
4 | - fix: debug error log not caught
5 |
6 | # 0.1.4
7 |
8 | - feat: upgrade to Flutter 3.7.0
9 |
10 | # 0.1.3
11 |
12 | - chore(brick): upgrade to mason v0.1.0-dev.40
13 | - feat: run flutter pub get on template generated
14 |
15 | # 0.1.2
16 |
17 | - chore: remove git ignored files in brick
18 |
19 | # 0.1.1
20 |
21 | - docs: update readme
22 |
23 | # 0.1.0
24 |
25 | - feat: add startup feature
26 | - feat: upgrade to Flutter 3.3.10
27 | - refactor: move snackbar under services
28 | - refactor: rename router to routing
29 |
30 | # 0.0.1
31 |
32 | - feat: initial release 🎉
33 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/domain/auth_state.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'auth_state.freezed.dart';
5 |
6 | @freezed
7 | class AuthState with _$AuthState {
8 | const factory AuthState.authenticating() = _Authenticating;
9 | const factory AuthState.unauthenticated() = _Unauthenticated;
10 | const factory AuthState.unknown() = _Unknown;
11 | const factory AuthState.authenticated() = _Authenticated;
12 | const factory AuthState.failure(ApiFailure failure) = _Failure;
13 | }
14 |
--------------------------------------------------------------------------------
/src/my_app/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Flutterprint",
3 | "short_name": "Flutterprint",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [{
12 | "src": "icons/Icon-192.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "icons/Icon-512.png",
18 | "sizes": "512x512",
19 | "type": "image/png"
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/src/my_app/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/src/my_app/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 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{project_name.titleCase()}}",
3 | "short_name": "{{project_name.titleCase()}}",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "{{{description}}}",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [{
12 | "src": "icons/Icon-192.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "icons/Icon-512.png",
18 | "sizes": "512x512",
19 | "type": "image/png"
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ## Description
10 |
11 |
12 |
13 | ## Type of Change
14 |
15 |
16 |
17 | - [ ] ✨ New feature (non-breaking change which adds functionality)
18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
20 | - [ ] 🧹 Code refactor
21 | - [ ] ✅ Build configuration change
22 | - [ ] 📝 Documentation
23 | - [ ] 🗑️ Chore
24 |
--------------------------------------------------------------------------------
/src/my_app/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ## Description
10 |
11 |
12 |
13 | ## Type of Change
14 |
15 |
16 |
17 | - [ ] ✨ New feature (non-breaking change which adds functionality)
18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
20 | - [ ] 🧹 Code refactor
21 | - [ ] ✅ Build configuration change
22 | - [ ] 📝 Documentation
23 | - [ ] 🗑️ Chore
24 |
--------------------------------------------------------------------------------
/.github/workflows/flutterprint.yaml:
--------------------------------------------------------------------------------
1 | name: flutterprint
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - ".github/workflows/flutterprint.yaml"
7 | - "src/my_app/lib/**"
8 | - "src/my_app/test/**"
9 | - "src/my_app/pubspec.yaml"
10 | - "tool/generator/**"
11 | push:
12 | branches:
13 | - main
14 | paths:
15 | - ".github/workflows/flutterprint.yaml"
16 | - "src/my_app/lib/**"
17 | - "src/my_app/test/**"
18 | - "src/my_app/pubspec.yaml"
19 | - "tool/generator/**"
20 |
21 | jobs:
22 | build:
23 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
24 | with:
25 | flutter_channel: stable
26 | flutter_version: 3.7.2
27 | min_coverage: 80
28 | working_directory: src/my_app
29 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ## Description
10 |
11 |
12 |
13 | ## Type of Change
14 |
15 |
16 |
17 | - [ ] ✨ New feature (non-breaking change which adds functionality)
18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
20 | - [ ] 🧹 Code refactor
21 | - [ ] ✅ Build configuration change
22 | - [ ] 📝 Documentation
23 | - [ ] 🗑️ Chore
24 |
--------------------------------------------------------------------------------
/brick/hooks/pre_gen.dart:
--------------------------------------------------------------------------------
1 | import 'package:mason/mason.dart';
2 |
3 | enum Platform {
4 | android,
5 | ios,
6 | }
7 |
8 | void run(HookContext context) {
9 | context.vars['application_id_android'] =
10 | _appId(context, platform: Platform.android);
11 | context.vars['application_id'] = _appId(context);
12 | }
13 |
14 | String _appId(HookContext context, {Platform? platform}) {
15 | final orgName = context.vars['org_name'] as String;
16 | final projectName = context.vars['project_name'] as String;
17 |
18 | var applicationId = context.vars['application_id'] as String?;
19 | applicationId = (applicationId?.isNotEmpty ?? false)
20 | ? applicationId
21 | : '''$orgName.${platform == Platform.android ? projectName.snakeCase : projectName.paramCase}''';
22 |
23 | return applicationId!;
24 | }
25 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/local_storage/data/fake_secure_storage_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/features/authentication/domain/auth_info.dart';
2 | import 'package:flutterprint/services/local_storage/data/secure_storage_repository.dart';
3 |
4 | class FakeSecureStorageRepository implements SecureRepository {
5 | const FakeSecureStorageRepository();
6 |
7 | @override
8 | Future deleteAll() => Future.value(null);
9 |
10 | @override
11 | Future deleteAuthInfo() => Future.value(null);
12 |
13 | @override
14 | Future getAuthInfo() => Future.value(const AuthInfo(
15 | accessToken: 'foo',
16 | ));
17 |
18 | @override
19 | Future saveAuthInfo(AuthInfo authInfo) => Future.value(null);
20 |
21 | @override
22 | Future validateSecureStorage() => Future.value(null);
23 | }
24 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/theme_mode/application/theme_mode_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart' show ThemeMode;
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/services/local_storage/data/shared_preference_repository.dart';
4 |
5 | final themeModeControllerProvider =
6 | AutoDisposeNotifierProvider(
7 | ThemeModeController.new);
8 |
9 | class ThemeModeController extends AutoDisposeNotifier {
10 | @override
11 | ThemeMode build() => ref.read(sharedPreferenceRepoProvider).getThemeMode();
12 |
13 | Future toggleAndSaveThemeMode() async {
14 | final themeMode =
15 | state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
16 | state = themeMode;
17 | return await ref.read(sharedPreferenceRepoProvider).setThemeMode(themeMode);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/my_app/lib/utils/validators/local_host_validator_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/constant/regex_pattern.dart';
2 | import 'package:flutterprint/l10n/l10n.dart';
3 | import 'package:flutterprint/utils/utils.dart';
4 |
5 | mixin LocalHostValidatorMixin {
6 | // Use a regular expression to check if the input string follows the pattern
7 | // of an IP address (four groups of digits separated by periods) followed by a
8 | // colon and a port number (one or more digits)
9 | final localHostVaidator = StringValidator.regex(RegexPattern.ipAddressPort);
10 |
11 | bool canSubmitLocalHost(String localHost) =>
12 | localHostVaidator.isValid(localHost);
13 |
14 | String? localHostErrorText(String account, AppLocalizations l10n) {
15 | final bool showErrorText = !canSubmitLocalHost(account);
16 | return showErrorText ? l10n.incorrectInputFormat : null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/data/fake_secure_storage_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/features/authentication/domain/auth_info.dart';
2 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/secure_storage_repository.dart';
3 |
4 | class FakeSecureStorageRepository implements SecureRepository {
5 | const FakeSecureStorageRepository();
6 |
7 | @override
8 | Future deleteAll() => Future.value(null);
9 |
10 | @override
11 | Future deleteAuthInfo() => Future.value(null);
12 |
13 | @override
14 | Future getAuthInfo() => Future.value(const AuthInfo(
15 | accessToken: 'foo',
16 | ));
17 |
18 | @override
19 | Future saveAuthInfo(AuthInfo authInfo) => Future.value(null);
20 |
21 | @override
22 | Future validateSecureStorage() => Future.value(null);
23 | }
24 |
--------------------------------------------------------------------------------
/src/my_app/test/mocks.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2 | import 'package:http/http.dart' as http;
3 | import 'package:flutterprint/features/authentication/authentication.dart';
4 | import 'package:flutterprint/services/local_storage/data/secure_storage_repository.dart';
5 | import 'package:mocktail/mocktail.dart';
6 |
7 | // A generic Listener class, used to keep track of when a provider notifies its listeners
8 | class Listener extends Mock {
9 | void call(T? previous, T next);
10 | }
11 |
12 | class MockAuthRepository extends Mock implements AuthRepository {}
13 |
14 | class MockSecureRepository extends Mock implements SecureRepository {}
15 |
16 | class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
17 |
18 | class MockHttpClient extends Mock implements http.Client {}
19 |
20 | class MockResponse extends Mock implements http.Response {}
21 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/backend_environment/domain/backend_env.dart:
--------------------------------------------------------------------------------
1 | /// [hostKey] is a key used to stored in shared preference
2 | /// [baseUrl] is the base url/domain name of your server
3 | /// [appBanner] is a label that indicates the current backend environment
4 | enum BackendEnv {
5 | // TODO: Manage your backend environments configurations
6 | release(
7 | hostKey: '@release',
8 | baseUrl: 'release.example.com',
9 | appBanner: '',
10 | ),
11 | demo(
12 | hostKey: '@demo',
13 | baseUrl: 'demo.example.com',
14 | appBanner: 'demo',
15 | ),
16 | develop(
17 | hostKey: '@develop',
18 | baseUrl: 'develop.example.com',
19 | appBanner: 'develop',
20 | );
21 |
22 | final String hostKey;
23 | final String baseUrl;
24 | final String appBanner;
25 | const BackendEnv({
26 | required this.hostKey,
27 | required this.baseUrl,
28 | required this.appBanner,
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/theme_mode/application/theme_mode_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart' show ThemeMode;
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/shared_preference_repository.dart';
4 |
5 | final themeModeControllerProvider =
6 | AutoDisposeNotifierProvider(
7 | ThemeModeController.new);
8 |
9 | class ThemeModeController extends AutoDisposeNotifier {
10 | @override
11 | ThemeMode build() => ref.read(sharedPreferenceRepoProvider).getThemeMode();
12 |
13 | Future toggleAndSaveThemeMode() async {
14 | final themeMode =
15 | state == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
16 | state = themeMode;
17 | return await ref.read(sharedPreferenceRepoProvider).setThemeMode(themeMode);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/my_app/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | 11.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/my_app/lib/widgets/backend_env_banner.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/features/backend_environment/application/host_controller.dart';
4 |
5 | /// Show a debug banner that indicates the current backend environment
6 | class BackendEnvBanner extends ConsumerWidget {
7 | final Widget child;
8 | const BackendEnvBanner({
9 | super.key,
10 | required this.child,
11 | });
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final bannerName =
16 | ref.watch(hostControllerProvider.select((state) => state.bannerName));
17 | return bannerName.isEmpty
18 | ? child
19 | : Banner(
20 | key: Key(bannerName),
21 | location: BannerLocation.bottomEnd,
22 | message: bannerName,
23 | child: child,
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/backend_env.dart:
--------------------------------------------------------------------------------
1 | /// [hostKey] is a key used to stored in shared preference
2 | /// [baseUrl] is the base url/domain name of your server
3 | /// [appBanner] is a label that indicates the current backend environment
4 | enum BackendEnv {
5 | // TODO: Manage your backend environments configurations
6 | release(
7 | hostKey: '@release',
8 | baseUrl: 'release.example.com',
9 | appBanner: '',
10 | ),
11 | demo(
12 | hostKey: '@demo',
13 | baseUrl: 'demo.example.com',
14 | appBanner: 'demo',
15 | ),
16 | develop(
17 | hostKey: '@develop',
18 | baseUrl: 'develop.example.com',
19 | appBanner: 'develop',
20 | );
21 |
22 | final String hostKey;
23 | final String baseUrl;
24 | final String appBanner;
25 | const BackendEnv({
26 | required this.hostKey,
27 | required this.baseUrl,
28 | required this.appBanner,
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/local_host_validator_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/constant/regex_pattern.dart';
2 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart';
3 | import 'package:{{project_name.snakeCase()}}/utils/utils.dart';
4 |
5 | mixin LocalHostValidatorMixin {
6 | // Use a regular expression to check if the input string follows the pattern
7 | // of an IP address (four groups of digits separated by periods) followed by a
8 | // colon and a port number (one or more digits)
9 | final localHostVaidator = StringValidator.regex(RegexPattern.ipAddressPort);
10 |
11 | bool canSubmitLocalHost(String localHost) =>
12 | localHostVaidator.isValid(localHost);
13 |
14 | String? localHostErrorText(String account, AppLocalizations l10n) {
15 | final bool showErrorText = !canSubmitLocalHost(account);
16 | return showErrorText ? l10n.incorrectInputFormat : null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | 11.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/mocks.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2 | import 'package:http/http.dart' as http;
3 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/secure_storage_repository.dart';
5 | import 'package:mocktail/mocktail.dart';
6 |
7 | // A generic Listener class, used to keep track of when a provider notifies its listeners
8 | class Listener extends Mock {
9 | void call(T? previous, T next);
10 | }
11 |
12 | class MockAuthRepository extends Mock implements AuthRepository {}
13 |
14 | class MockSecureRepository extends Mock implements SecureRepository {}
15 |
16 | class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
17 |
18 | class MockHttpClient extends Mock implements http.Client {}
19 |
20 | class MockResponse extends Mock implements http.Response {}
21 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/domain/api_failure.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'api_failure.freezed.dart';
4 |
5 | @freezed
6 | class ApiFailure with _$ApiFailure {
7 | const ApiFailure._();
8 | const factory ApiFailure.apiAgreementConflict() = _ApiAgreementConflict;
9 | const factory ApiFailure.badRequest({String? message}) = _BadRequest;
10 | const factory ApiFailure.forbidden({String? message}) = _Forbidden;
11 | const factory ApiFailure.socket({String? message}) = _Socket;
12 | const factory ApiFailure.notFound({String? message}) = _NotFound;
13 | const factory ApiFailure.serviceUnavailable({String? message}) =
14 | _ServiceUnavailable;
15 | const factory ApiFailure.timeout({String? message}) = _Timeout;
16 | const factory ApiFailure.unauthorized({String? message}) = _Unauthorized;
17 | const factory ApiFailure.undefined(
18 | {required int statusCode, String? message}) = _Undefined;
19 | }
20 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/backend_env_banner.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/application/host_controller.dart';
4 |
5 | /// Show a debug banner that indicates the current backend environment
6 | class BackendEnvBanner extends ConsumerWidget {
7 | final Widget child;
8 | const BackendEnvBanner({
9 | super.key,
10 | required this.child,
11 | });
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final bannerName =
16 | ref.watch(hostControllerProvider.select((state) => state.bannerName));
17 | return bannerName.isEmpty
18 | ? child
19 | : Banner(
20 | key: Key(bannerName),
21 | location: BannerLocation.bottomEnd,
22 | message: bannerName,
23 | child: child,
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/my_app/scripts/import_files_coverage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | file=test/coverage_helper_test.dart
3 | printf "// Helper file to make coverage work for all dart files\n" > $file
4 | printf "// **************************************************************************\n" >> $file
5 | printf "// Because of this: https://github.com/flutter/flutter/issues/27997#issue-410722816\n" >> $file
6 | printf "// DO NOT EDIT THIS FILE USE: sh scripts/import_files_coverage.sh flutterprint\n" >> $file
7 | printf "// **************************************************************************\n" >> $file
8 | printf "\n" >> $file
9 | printf "// ignore_for_file: unused_import\n" >> $file
10 | find lib -type f \( -iname "*.dart" ! -iname '*_event.dart' ! -iname '*_state.dart' ! -iname "*.g.dart" ! -iname "*.config.g.dart" ! -iname "*.gr.dart" ! -iname "*.freezed.dart" ! -iname "generated_plugin_registrant.dart" \) | cut -c4- | awk -v package="$1" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file
11 | printf "\nvoid main(){}" >> $file
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/domain/api_failure.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'api_failure.freezed.dart';
4 |
5 | @freezed
6 | class ApiFailure with _$ApiFailure {
7 | const ApiFailure._();
8 | const factory ApiFailure.apiAgreementConflict() = _ApiAgreementConflict;
9 | const factory ApiFailure.badRequest({String? message}) = _BadRequest;
10 | const factory ApiFailure.forbidden({String? message}) = _Forbidden;
11 | const factory ApiFailure.socket({String? message}) = _Socket;
12 | const factory ApiFailure.notFound({String? message}) = _NotFound;
13 | const factory ApiFailure.serviceUnavailable({String? message}) =
14 | _ServiceUnavailable;
15 | const factory ApiFailure.timeout({String? message}) = _Timeout;
16 | const factory ApiFailure.unauthorized({String? message}) = _Unauthorized;
17 | const factory ApiFailure.undefined(
18 | {required int statusCode, String? message}) = _Undefined;
19 | }
20 |
--------------------------------------------------------------------------------
/src/my_app/lib/utils/validators/account_password_validators_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/utils/validators/string_validator.dart';
2 | import 'package:flutterprint/l10n/l10n.dart';
3 |
4 | /// Mixin class to be used for client-side account & password validation
5 | mixin AccountAndPasswordValidatorsMixin {
6 | final nonEmptyValidator = StringValidator.nonEmpty();
7 |
8 | bool canSubmitAccount(String account) {
9 | return nonEmptyValidator.isValid(account);
10 | }
11 |
12 | bool canSubmitPassword(String password) {
13 | return nonEmptyValidator.isValid(password);
14 | }
15 |
16 | String? accountErrorText(String account, AppLocalizations l10n) {
17 | final bool showErrorText = !canSubmitAccount(account);
18 | return showErrorText ? l10n.accountCanNotBeEmpty : null;
19 | }
20 |
21 | String? passwordErrorText(String password, AppLocalizations l10n) {
22 | final bool showErrorText = !canSubmitPassword(password);
23 | return showErrorText ? l10n.passwordCanNotBeEmpty : null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/brick/brick.yaml:
--------------------------------------------------------------------------------
1 | name: flutterprint
2 | description: A starter app for building maintainable & scalable Flutter apps, streamlining development with commonly used features.
3 | repository: https://github.com/ycchuang305/flutterprint
4 | version: 0.1.5
5 |
6 | environment:
7 | mason: ">=0.1.0-dev.40 <0.1.0"
8 |
9 | vars:
10 | project_name:
11 | type: string
12 | description: The project name
13 | default: my_app
14 | prompt: What is the project name?
15 | org_name:
16 | type: string
17 | description: The organization name
18 | default: com.example
19 | prompt: What is the organization name?
20 | application_id:
21 | type: string
22 | description: The application id on Android, Bundle ID on iOS and company name on Windows. If omitted value will be formed by org_name + . + project_name.
23 | prompt: What is the application id?
24 | description:
25 | type: string
26 | description: A short project description
27 | default: A Flutter blueprint
28 | prompt: What is the project description?
29 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/authentication/data/fake_auth_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/features/authentication/authentication.dart';
2 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart';
3 |
4 | class FakeAuthRepository implements AuthRepository {
5 | const FakeAuthRepository({this.addDelay = true});
6 | final bool addDelay;
7 | final fakeAuthInfo = const AuthInfo(
8 | accessToken: 'foo',
9 | );
10 |
11 | @override
12 | Future> refreshToken() async {
13 | if (addDelay) {
14 | await Future.delayed(const Duration(seconds: 1));
15 | }
16 | return Right(fakeAuthInfo);
17 | }
18 |
19 | @override
20 | Future> signIn({
21 | required String username,
22 | required String password,
23 | }) async {
24 | if (addDelay) {
25 | await Future.delayed(const Duration(seconds: 1));
26 | }
27 | return Right(fakeAuthInfo);
28 | // return left(const ApiFailure.forbidden());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/scripts/import_files_coverage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | file=test/coverage_helper_test.dart
3 | printf "// Helper file to make coverage work for all dart files\n" > $file
4 | printf "// **************************************************************************\n" >> $file
5 | printf "// Because of this: https://github.com/flutter/flutter/issues/27997#issue-410722816\n" >> $file
6 | printf "// DO NOT EDIT THIS FILE USE: sh scripts/import_files_coverage.sh {{project_name.snakeCase()}}\n" >> $file
7 | printf "// **************************************************************************\n" >> $file
8 | printf "\n" >> $file
9 | printf "// ignore_for_file: unused_import\n" >> $file
10 | find lib -type f \( -iname "*.dart" ! -iname '*_event.dart' ! -iname '*_state.dart' ! -iname "*.g.dart" ! -iname "*.config.g.dart" ! -iname "*.gr.dart" ! -iname "*.freezed.dart" ! -iname "generated_plugin_registrant.dart" \) | cut -c4- | awk -v package="$1" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file
11 | printf "\nvoid main(){}" >> $file
--------------------------------------------------------------------------------
/src/my_app/lib/features/start_up/data/start_up_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:flutterprint/features/authentication/authentication.dart';
3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart';
4 | import 'package:flutterprint/services/local_storage/local_storage.dart';
5 |
6 | final startUpRepositoryProvider =
7 | Provider.autoDispose((ref) => StartUpRepository(ref));
8 |
9 | class StartUpRepository {
10 | const StartUpRepository(this.ref);
11 | final Ref ref;
12 |
13 | Future prepareAuthentication() async {
14 | // Considering add more setup method here only if it has to be done before authenticating
15 |
16 | // Validate secure storage
17 | await ref.read(secureRepositoryProvider).validateSecureStorage();
18 |
19 | // Setup backend host environment
20 | ref.read(hostControllerProvider.notifier).setUpHost();
21 |
22 | // Start authentication
23 | ref.read(authControllerProvider.notifier).authentication();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/account_password_validators_mixin.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/utils/validators/string_validator.dart';
2 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart';
3 |
4 | /// Mixin class to be used for client-side account & password validation
5 | mixin AccountAndPasswordValidatorsMixin {
6 | final nonEmptyValidator = StringValidator.nonEmpty();
7 |
8 | bool canSubmitAccount(String account) {
9 | return nonEmptyValidator.isValid(account);
10 | }
11 |
12 | bool canSubmitPassword(String password) {
13 | return nonEmptyValidator.isValid(password);
14 | }
15 |
16 | String? accountErrorText(String account, AppLocalizations l10n) {
17 | final bool showErrorText = !canSubmitAccount(account);
18 | return showErrorText ? l10n.accountCanNotBeEmpty : null;
19 | }
20 |
21 | String? passwordErrorText(String password, AppLocalizations l10n) {
22 | final bool showErrorText = !canSubmitPassword(password);
23 | return showErrorText ? l10n.passwordCanNotBeEmpty : null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/data/fake_auth_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
2 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart';
3 |
4 | class FakeAuthRepository implements AuthRepository {
5 | const FakeAuthRepository({this.addDelay = true});
6 | final bool addDelay;
7 | final fakeAuthInfo = const AuthInfo(
8 | accessToken: 'foo',
9 | );
10 |
11 | @override
12 | Future> refreshToken() async {
13 | if (addDelay) {
14 | await Future.delayed(const Duration(seconds: 1));
15 | }
16 | return Right(fakeAuthInfo);
17 | }
18 |
19 | @override
20 | Future> signIn({
21 | required String username,
22 | required String password,
23 | }) async {
24 | if (addDelay) {
25 | await Future.delayed(const Duration(seconds: 1));
26 | }
27 | return Right(fakeAuthInfo);
28 | // return left(const ApiFailure.forbidden());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/data/start_up_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart';
4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart';
5 |
6 | final startUpRepositoryProvider =
7 | Provider.autoDispose((ref) => StartUpRepository(ref));
8 |
9 | class StartUpRepository {
10 | const StartUpRepository(this.ref);
11 | final Ref ref;
12 |
13 | Future prepareAuthentication() async {
14 | // Considering add more setup method here only if it has to be done before authenticating
15 |
16 | // Validate secure storage
17 | await ref.read(secureRepositoryProvider).validateSecureStorage();
18 |
19 | // Setup backend host environment
20 | ref.read(hostControllerProvider.notifier).setUpHost();
21 |
22 | // Start authentication
23 | ref.read(authControllerProvider.notifier).authentication();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/brick/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Danny Chuang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/src/my_app/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Danny Chuang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/my_app/coverage_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/authentication/domain/auth_info.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert' as convert;
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'auth_info.freezed.dart';
5 |
6 | @freezed
7 | class AuthInfo with _$AuthInfo {
8 | const AuthInfo._();
9 | const factory AuthInfo({
10 | required String accessToken,
11 | }) = _AuthInfo;
12 |
13 | bool get hasToken => accessToken.isNotEmpty;
14 |
15 | factory AuthInfo.empty() => const AuthInfo(accessToken: '');
16 |
17 | factory AuthInfo.fromJson(Map json) {
18 | if (json['_data'] != null) {
19 | var accessToken = json['_data']['access_token'] as String;
20 | return AuthInfo(accessToken: accessToken);
21 | }
22 | return AuthInfo.empty();
23 | }
24 |
25 | factory AuthInfo.fromSecureStorage(String value) {
26 | var json = convert.jsonDecode(value) as Map;
27 | return AuthInfo(
28 | accessToken: json['accessToken'] as String,
29 | );
30 | }
31 |
32 | String get toSecureStorage {
33 | final Map json = {
34 | 'accessToken': accessToken,
35 | };
36 | return convert.jsonEncode(json);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/coverage_badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/domain/auth_info.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert' as convert;
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'auth_info.freezed.dart';
5 |
6 | @freezed
7 | class AuthInfo with _$AuthInfo {
8 | const AuthInfo._();
9 | const factory AuthInfo({
10 | required String accessToken,
11 | }) = _AuthInfo;
12 |
13 | bool get hasToken => accessToken.isNotEmpty;
14 |
15 | factory AuthInfo.empty() => const AuthInfo(accessToken: '');
16 |
17 | factory AuthInfo.fromJson(Map json) {
18 | if (json['_data'] != null) {
19 | var accessToken = json['_data']['access_token'] as String;
20 | return AuthInfo(accessToken: accessToken);
21 | }
22 | return AuthInfo.empty();
23 | }
24 |
25 | factory AuthInfo.fromSecureStorage(String value) {
26 | var json = convert.jsonDecode(value) as Map;
27 | return AuthInfo(
28 | accessToken: json['accessToken'] as String,
29 | );
30 | }
31 |
32 | String get toSecureStorage {
33 | final Map json = {
34 | 'accessToken': accessToken,
35 | };
36 | return convert.jsonEncode(json);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/my_app/lib/utils/validators/string_validator.dart:
--------------------------------------------------------------------------------
1 | /// This file contains some helper functions used for string validation.
2 | abstract class StringValidator {
3 | bool isValid(String value);
4 |
5 | factory StringValidator.regex(String regexSource) =>
6 | RegexValidator(regexSource: regexSource);
7 |
8 | factory StringValidator.nonEmpty() => NonEmptyStringValidator();
9 |
10 | factory StringValidator.minLength(int minLength) =>
11 | MinLengthStringValidator(minLength);
12 | }
13 |
14 | class RegexValidator implements StringValidator {
15 | RegexValidator({required this.regexSource});
16 | final String regexSource;
17 |
18 | @override
19 | bool isValid(String value) {
20 | final RegExp regex = RegExp(regexSource);
21 | return regex.hasMatch(value);
22 | }
23 | }
24 |
25 | class NonEmptyStringValidator implements StringValidator {
26 | @override
27 | bool isValid(String value) {
28 | return value.isNotEmpty;
29 | }
30 | }
31 |
32 | class MinLengthStringValidator implements StringValidator {
33 | MinLengthStringValidator(this.minLength);
34 | final int minLength;
35 |
36 | @override
37 | bool isValid(String value) {
38 | return value.length >= minLength;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/utils/validators/string_validator.dart:
--------------------------------------------------------------------------------
1 | /// This file contains some helper functions used for string validation.
2 | abstract class StringValidator {
3 | bool isValid(String value);
4 |
5 | factory StringValidator.regex(String regexSource) =>
6 | RegexValidator(regexSource: regexSource);
7 |
8 | factory StringValidator.nonEmpty() => NonEmptyStringValidator();
9 |
10 | factory StringValidator.minLength(int minLength) =>
11 | MinLengthStringValidator(minLength);
12 | }
13 |
14 | class RegexValidator implements StringValidator {
15 | RegexValidator({required this.regexSource});
16 | final String regexSource;
17 |
18 | @override
19 | bool isValid(String value) {
20 | final RegExp regex = RegExp(regexSource);
21 | return regex.hasMatch(value);
22 | }
23 | }
24 |
25 | class NonEmptyStringValidator implements StringValidator {
26 | @override
27 | bool isValid(String value) {
28 | return value.isNotEmpty;
29 | }
30 | }
31 |
32 | class MinLengthStringValidator implements StringValidator {
33 | MinLengthStringValidator(this.minLength);
34 | final int minLength;
35 |
36 | @override
37 | bool isValid(String value) {
38 | return value.length >= minLength;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/backend_environment/domain/host.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/features/backend_environment/backend_environment.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'host.freezed.dart';
5 |
6 | @freezed
7 | class Host with _$Host {
8 | const Host._();
9 | const factory Host({
10 | BackendEnv? backendEnv,
11 | @Default('') String localHost,
12 | }) = _Host;
13 |
14 | String get baseUrl =>
15 | isLocal ? localHost : backendEnv?.baseUrl ?? BackendEnv.release.baseUrl;
16 | bool get isLocal => localHost.isNotEmpty;
17 |
18 | Uri getUri(
19 | Endpoint endpoint, {
20 | Map? queryParameters,
21 | String? path,
22 | }) {
23 | var selectedbaseUrl = isLocal ? baseUrl.split(':').first : baseUrl;
24 | var port = isLocal ? int.tryParse(baseUrl.split(':').last) ?? 8000 : null;
25 | return Uri(
26 | scheme: isLocal ? 'http' : 'https',
27 | host: selectedbaseUrl,
28 | port: port,
29 | path: path == null ? endpoint.path : '${endpoint.path}/$path',
30 | queryParameters: queryParameters,
31 | );
32 | }
33 |
34 | String get bannerName {
35 | if (isLocal) {
36 | return 'local';
37 | }
38 | return backendEnv?.appBanner ?? '';
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/my_app/test/utils/validators/string_validator_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/constant/regex_pattern.dart';
2 | import 'package:flutterprint/utils/utils.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 |
5 | void main() {
6 | group(
7 | '[StringValidator]:',
8 | () {
9 | test(
10 | 'IP address with port number regex validator',
11 | () {
12 | final ipAddressHostValidator =
13 | StringValidator.regex(RegexPattern.ipAddressPort);
14 | expect(ipAddressHostValidator.isValid('192.168.0.233'), false);
15 | expect(ipAddressHostValidator.isValid('192.168.0.233:8000'), true);
16 | },
17 | );
18 |
19 | test(
20 | 'Non empty validator',
21 | () {
22 | final nonEmptyValidator = StringValidator.nonEmpty();
23 | expect(nonEmptyValidator.isValid(''), false);
24 | expect(nonEmptyValidator.isValid('123'), true);
25 | },
26 | );
27 |
28 | test(
29 | 'Minimum length validator',
30 | () {
31 | final minLengthValidator = StringValidator.minLength(8);
32 | expect(minLengthValidator.isValid('123'), false);
33 | expect(minLengthValidator.isValid('12345678'), true);
34 | },
35 | );
36 | },
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/domain/host.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'host.freezed.dart';
5 |
6 | @freezed
7 | class Host with _$Host {
8 | const Host._();
9 | const factory Host({
10 | BackendEnv? backendEnv,
11 | @Default('') String localHost,
12 | }) = _Host;
13 |
14 | String get baseUrl =>
15 | isLocal ? localHost : backendEnv?.baseUrl ?? BackendEnv.release.baseUrl;
16 | bool get isLocal => localHost.isNotEmpty;
17 |
18 | Uri getUri(
19 | Endpoint endpoint, {
20 | Map? queryParameters,
21 | String? path,
22 | }) {
23 | var selectedbaseUrl = isLocal ? baseUrl.split(':').first : baseUrl;
24 | var port = isLocal ? int.tryParse(baseUrl.split(':').last) ?? 8000 : null;
25 | return Uri(
26 | scheme: isLocal ? 'http' : 'https',
27 | host: selectedbaseUrl,
28 | port: port,
29 | path: path == null ? endpoint.path : '${endpoint.path}/$path',
30 | queryParameters: queryParameters,
31 | );
32 | }
33 |
34 | String get bannerName {
35 | if (isLocal) {
36 | return 'local';
37 | }
38 | return backendEnv?.appBanner ?? '';
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/generate_flutterprint.yaml:
--------------------------------------------------------------------------------
1 | name: generate_flutterprint
2 |
3 | on:
4 | push:
5 | paths:
6 | - tool/generator/**
7 | - src/**
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: macos-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: dart-lang/setup-dart@v1
19 |
20 | - name: Install Dependencies
21 | working-directory: tool/generator
22 | run: dart pub get
23 |
24 | - name: Generate Template
25 | run: dart ./tool/generator/main.dart
26 |
27 | - name: Config Git User
28 | run: |
29 | git config user.name VGV Bot
30 | git config user.email vgvbot@users.noreply.github.com
31 |
32 | - name: Create Pull Request
33 | uses: peter-evans/create-pull-request@v4.2.3
34 | with:
35 | base: main
36 | branch: chore/generate-flutterprint
37 | commit-message: "chore: generate flutterprint template"
38 | title: "chore: generate flutterprint template"
39 | body: Please squash and merge me!
40 | labels: bot
41 | author: VGV Bot
42 | assignees: vgvbot
43 | reviewers: ycchuang305
44 | committer: VGV Bot
45 |
--------------------------------------------------------------------------------
/src/my_app/lib/constant/app_sizes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.)
4 | class Sizes {
5 | static const p4 = 4.0;
6 | static const p8 = 8.0;
7 | static const p12 = 12.0;
8 | static const p16 = 16.0;
9 | static const p20 = 20.0;
10 | static const p24 = 24.0;
11 | static const p32 = 32.0;
12 | static const p48 = 48.0;
13 | static const p64 = 64.0;
14 | }
15 |
16 | /// Constant gap widths
17 | const gapW4 = SizedBox(width: Sizes.p4);
18 | const gapW8 = SizedBox(width: Sizes.p8);
19 | const gapW12 = SizedBox(width: Sizes.p12);
20 | const gapW16 = SizedBox(width: Sizes.p16);
21 | const gapW20 = SizedBox(width: Sizes.p20);
22 | const gapW24 = SizedBox(width: Sizes.p24);
23 | const gapW32 = SizedBox(width: Sizes.p32);
24 | const gapW48 = SizedBox(width: Sizes.p48);
25 | const gapW64 = SizedBox(width: Sizes.p64);
26 |
27 | /// Constant gap heights
28 | const gapH4 = SizedBox(height: Sizes.p4);
29 | const gapH8 = SizedBox(height: Sizes.p8);
30 | const gapH12 = SizedBox(height: Sizes.p12);
31 | const gapH16 = SizedBox(height: Sizes.p16);
32 | const gapH20 = SizedBox(height: Sizes.p20);
33 | const gapH24 = SizedBox(height: Sizes.p24);
34 | const gapH32 = SizedBox(height: Sizes.p32);
35 | const gapH48 = SizedBox(height: Sizes.p48);
36 | const gapH64 = SizedBox(height: Sizes.p64);
37 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/utils/validators/string_validator_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/constant/regex_pattern.dart';
2 | import 'package:{{project_name.snakeCase()}}/utils/utils.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 |
5 | void main() {
6 | group(
7 | '[StringValidator]:',
8 | () {
9 | test(
10 | 'IP address with port number regex validator',
11 | () {
12 | final ipAddressHostValidator =
13 | StringValidator.regex(RegexPattern.ipAddressPort);
14 | expect(ipAddressHostValidator.isValid('192.168.0.233'), false);
15 | expect(ipAddressHostValidator.isValid('192.168.0.233:8000'), true);
16 | },
17 | );
18 |
19 | test(
20 | 'Non empty validator',
21 | () {
22 | final nonEmptyValidator = StringValidator.nonEmpty();
23 | expect(nonEmptyValidator.isValid(''), false);
24 | expect(nonEmptyValidator.isValid('123'), true);
25 | },
26 | );
27 |
28 | test(
29 | 'Minimum length validator',
30 | () {
31 | final minLengthValidator = StringValidator.minLength(8);
32 | expect(minLengthValidator.isValid('123'), false);
33 | expect(minLengthValidator.isValid('12345678'), true);
34 | },
35 | );
36 | },
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/constant/app_sizes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.)
4 | class Sizes {
5 | static const p4 = 4.0;
6 | static const p8 = 8.0;
7 | static const p12 = 12.0;
8 | static const p16 = 16.0;
9 | static const p20 = 20.0;
10 | static const p24 = 24.0;
11 | static const p32 = 32.0;
12 | static const p48 = 48.0;
13 | static const p64 = 64.0;
14 | }
15 |
16 | /// Constant gap widths
17 | const gapW4 = SizedBox(width: Sizes.p4);
18 | const gapW8 = SizedBox(width: Sizes.p8);
19 | const gapW12 = SizedBox(width: Sizes.p12);
20 | const gapW16 = SizedBox(width: Sizes.p16);
21 | const gapW20 = SizedBox(width: Sizes.p20);
22 | const gapW24 = SizedBox(width: Sizes.p24);
23 | const gapW32 = SizedBox(width: Sizes.p32);
24 | const gapW48 = SizedBox(width: Sizes.p48);
25 | const gapW64 = SizedBox(width: Sizes.p64);
26 |
27 | /// Constant gap heights
28 | const gapH4 = SizedBox(height: Sizes.p4);
29 | const gapH8 = SizedBox(height: Sizes.p8);
30 | const gapH12 = SizedBox(height: Sizes.p12);
31 | const gapH16 = SizedBox(height: Sizes.p16);
32 | const gapH20 = SizedBox(height: Sizes.p20);
33 | const gapH24 = SizedBox(height: Sizes.p24);
34 | const gapH32 = SizedBox(height: Sizes.p32);
35 | const gapH48 = SizedBox(height: Sizes.p48);
36 | const gapH64 = SizedBox(height: Sizes.p64);
37 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/backend_environment/application/host_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:collection/collection.dart';
3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart';
4 | import 'package:flutterprint/services/local_storage/local_storage.dart';
5 |
6 | final hostControllerProvider =
7 | StateNotifierProvider((ref) {
8 | final sharedPreferenceRepository = ref.watch(sharedPreferenceRepoProvider);
9 | return HostController(sharedPreferenceRepository);
10 | });
11 |
12 | class HostController extends StateNotifier {
13 | HostController(this.sharedPreferenceRepository) : super(const Host());
14 |
15 | final SharedPreferenceRepository sharedPreferenceRepository;
16 |
17 | void setUpHost() => state = _getHost(sharedPreferenceRepository.hostKey);
18 |
19 | Future update({required String hostKey}) async {
20 | if (hostKey.isNotEmpty) {
21 | await sharedPreferenceRepository.updateHostKey(hostKey);
22 | } else {
23 | await sharedPreferenceRepository.removeHostKey();
24 | }
25 | state = _getHost(hostKey);
26 | }
27 |
28 | Host _getHost(String key) {
29 | final backendEnv =
30 | BackendEnv.values.firstWhereOrNull((env) => env.hostKey == key);
31 | return Host(
32 | backendEnv: backendEnv,
33 | localHost: backendEnv == null ? key : '',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/backend_environment/application/host_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:collection/collection.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart';
4 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart';
5 |
6 | final hostControllerProvider =
7 | StateNotifierProvider((ref) {
8 | final sharedPreferenceRepository = ref.watch(sharedPreferenceRepoProvider);
9 | return HostController(sharedPreferenceRepository);
10 | });
11 |
12 | class HostController extends StateNotifier {
13 | HostController(this.sharedPreferenceRepository) : super(const Host());
14 |
15 | final SharedPreferenceRepository sharedPreferenceRepository;
16 |
17 | void setUpHost() => state = _getHost(sharedPreferenceRepository.hostKey);
18 |
19 | Future update({required String hostKey}) async {
20 | if (hostKey.isNotEmpty) {
21 | await sharedPreferenceRepository.updateHostKey(hostKey);
22 | } else {
23 | await sharedPreferenceRepository.removeHostKey();
24 | }
25 | state = _getHost(hostKey);
26 | }
27 |
28 | Host _getHost(String key) {
29 | final backendEnv =
30 | BackendEnv.values.firstWhereOrNull((env) => env.hostKey == key);
31 | return Host(
32 | backendEnv: backendEnv,
33 | localHost: backendEnv == null ? key : '',
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/my_app/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '11.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/rest_api_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/l10n/l10n.dart';
4 | import 'package:flutterprint/services/rest_api_service/domain/api_failure.dart';
5 | import 'package:flutterprint/services/snackbar/app_snackbar_repository.dart';
6 |
7 | export 'data/rest_api_service.dart';
8 | export 'data/rest_api_service_impl.dart';
9 | export 'domain/token_type.dart';
10 | export 'domain/api_failure.dart';
11 | export 'domain/api_method.dart';
12 | export 'domain/api_status.dart';
13 | export 'package:dartz/dartz.dart';
14 |
15 | extension ApiFailureX on ApiFailure {
16 | void showPredefinedSnackbar({
17 | required BuildContext context,
18 | required WidgetRef ref,
19 | }) =>
20 | ref.read(appSnackBarRepoProvider).showSnackbar(
21 | context,
22 | message: when(
23 | apiAgreementConflict: () => '',
24 | badRequest: (_) => context.l10n.badRequest,
25 | forbidden: (_) => context.l10n.forbidden,
26 | socket: (_) => context.l10n.socket,
27 | notFound: (_) => context.l10n.notFound,
28 | serviceUnavailable: (_) => context.l10n.serviceUnavailable,
29 | timeout: (_) => context.l10n.timeout,
30 | unauthorized: (_) => context.l10n.unauthorized,
31 | undefined: (statusCode, _) => context.l10n.unknown(statusCode),
32 | ),
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '11.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/src/my_app/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 | - use_super_parameters
28 |
29 | # Additional information about this file can be found at
30 | # https://dart.dev/guides/language/analysis-options
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/rest_api_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart';
4 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/domain/api_failure.dart';
5 | import 'package:{{project_name.snakeCase()}}/services/snackbar/app_snackbar_repository.dart';
6 |
7 | export 'data/rest_api_service.dart';
8 | export 'data/rest_api_service_impl.dart';
9 | export 'domain/token_type.dart';
10 | export 'domain/api_failure.dart';
11 | export 'domain/api_method.dart';
12 | export 'domain/api_status.dart';
13 | export 'package:dartz/dartz.dart';
14 |
15 | extension ApiFailureX on ApiFailure {
16 | void showPredefinedSnackbar({
17 | required BuildContext context,
18 | required WidgetRef ref,
19 | }) =>
20 | ref.read(appSnackBarRepoProvider).showSnackbar(
21 | context,
22 | message: when(
23 | apiAgreementConflict: () => '',
24 | badRequest: (_) => context.l10n.badRequest,
25 | forbidden: (_) => context.l10n.forbidden,
26 | socket: (_) => context.l10n.socket,
27 | notFound: (_) => context.l10n.notFound,
28 | serviceUnavailable: (_) => context.l10n.serviceUnavailable,
29 | timeout: (_) => context.l10n.timeout,
30 | unauthorized: (_) => context.l10n.unauthorized,
31 | undefined: (statusCode, _) => context.l10n.unknown(statusCode),
32 | ),
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/my_app/lib/l10n/arb/app_zh.arb:
--------------------------------------------------------------------------------
1 | {
2 | "@@locale": "zh",
3 | "account": "帳號",
4 | "@account": {
5 | "description": "用於帳號欄位的標題文字"
6 | },
7 | "password": "密碼",
8 | "@password": {
9 | "description": "用於密碼欄位的標題文字"
10 | },
11 | "signinAppBarTitle": "登入",
12 |
13 | "pageNotFound": "404 - 找不到頁面",
14 |
15 | "homePageAppBarTitle": "首頁",
16 | "signOut": "登出",
17 | "staySignedin": "保持登入",
18 | "submit": "送出",
19 | "authenticating": "驗證中...",
20 | "accountCanNotBeEmpty": "帳號不可為空白",
21 | "passwordCanNotBeEmpty": "密碼不可為空白",
22 | "incorrectAccountOrPassword": "帳號或密碼錯誤",
23 |
24 | "apiAgreementConflict": "API 協議衝突",
25 | "badRequest": "錯誤請求",
26 | "forbidden": "權限不足",
27 | "socket": "連線異常",
28 | "notFound": "未找到",
29 | "serviceUnavailable": "服務無法使用",
30 | "timeout": "請求逾時",
31 | "unauthorized": "帳戶未授權",
32 | "unknown": "錯誤碼 {statusCode}",
33 |
34 | "backendEnvEnableText": "再按 {value} 次進入後端環境設定頁",
35 | "@backendEnvEnableText": {
36 | "placeholders": {
37 | "value": {
38 | "type": "int"
39 | }
40 | }
41 | },
42 | "backendEnvAppBarTitle": "後端環境設定",
43 | "backendEnvRelease": "Release",
44 | "backendEnvDemo": "Demo",
45 | "backendEnvDevelop": "Develop",
46 | "backendEnvLocal": "Local",
47 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1",
48 | "save": "儲存",
49 |
50 | "incorrectInputFormat": "輸入格式錯誤"
51 | }
--------------------------------------------------------------------------------
/src/my_app/lib/routing/not_found_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/constant/app_sizes.dart';
4 | import 'package:flutterprint/l10n/l10n.dart';
5 | import 'package:flutterprint/routing/app_router.dart';
6 |
7 | class NotFoundPage extends ConsumerWidget {
8 | const NotFoundPage({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context, WidgetRef ref) {
12 | final l10n = context.l10n;
13 | return Scaffold(
14 | body: Padding(
15 | padding: const EdgeInsets.all(Sizes.p16),
16 | child: Center(
17 | child: Column(
18 | mainAxisSize: MainAxisSize.min,
19 | crossAxisAlignment: CrossAxisAlignment.center,
20 | children: [
21 | Text(
22 | l10n.pageNotFound,
23 | style: Theme.of(context).textTheme.headlineMedium,
24 | textAlign: TextAlign.center,
25 | ),
26 | gapH32,
27 | TextButton.icon(
28 | onPressed: () =>
29 | ref.read(appRouterProvider).go(AppRoute.home.path),
30 | icon: const Icon(
31 | Icons.home_outlined,
32 | size: Sizes.p24,
33 | ),
34 | label: Text(
35 | l10n.homePageAppBarTitle,
36 | style: const TextStyle(fontSize: Sizes.p20),
37 | ),
38 | ),
39 | ],
40 | ),
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/start_up/presentation/splash_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:after_layout/after_layout.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_riverpod/flutter_riverpod.dart';
6 | import 'package:flutterprint/constant/theme/theme.dart';
7 | import 'package:flutterprint/features/authentication/authentication.dart';
8 | import 'package:flutterprint/features/start_up/data/start_up_repository.dart';
9 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart';
10 |
11 | class SplashPage extends ConsumerStatefulWidget {
12 | const SplashPage({super.key});
13 |
14 | @override
15 | ConsumerState createState() => _SplashPageState();
16 | }
17 |
18 | class _SplashPageState extends ConsumerState
19 | with AfterLayoutMixin {
20 | @override
21 | FutureOr afterFirstLayout(BuildContext context) =>
22 | ref.read(startUpRepositoryProvider).prepareAuthentication();
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | ref.listen(
27 | authControllerProvider,
28 | (_, state) {
29 | state.whenOrNull(
30 | failure: (apiFailure) =>
31 | apiFailure.showPredefinedSnackbar(context: context, ref: ref),
32 | );
33 | },
34 | );
35 | final colorScheme = context.colorScheme;
36 | return Scaffold(
37 | backgroundColor: colorScheme.background,
38 | body: const Center(
39 | child: FlutterLogo(
40 | size: 64,
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 | - use_super_parameters
28 |
29 | # Additional information about this file can be found at
30 | # https://dart.dev/guides/language/analysis-options
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/arb/app_zh.arb:
--------------------------------------------------------------------------------
1 | {
2 | "@@locale": "zh",
3 | "account": "帳號",
4 | "@account": {
5 | "description": "用於帳號欄位的標題文字"
6 | },
7 | "password": "密碼",
8 | "@password": {
9 | "description": "用於密碼欄位的標題文字"
10 | },
11 | "signinAppBarTitle": "登入",
12 |
13 | "pageNotFound": "404 - 找不到頁面",
14 |
15 | "homePageAppBarTitle": "首頁",
16 | "signOut": "登出",
17 | "staySignedin": "保持登入",
18 | "submit": "送出",
19 | "authenticating": "驗證中...",
20 | "accountCanNotBeEmpty": "帳號不可為空白",
21 | "passwordCanNotBeEmpty": "密碼不可為空白",
22 | "incorrectAccountOrPassword": "帳號或密碼錯誤",
23 |
24 | "apiAgreementConflict": "API 協議衝突",
25 | "badRequest": "錯誤請求",
26 | "forbidden": "權限不足",
27 | "socket": "連線異常",
28 | "notFound": "未找到",
29 | "serviceUnavailable": "服務無法使用",
30 | "timeout": "請求逾時",
31 | "unauthorized": "帳戶未授權",
32 | "unknown": "錯誤碼 {statusCode}",
33 |
34 | "backendEnvEnableText": "再按 {value} 次進入後端環境設定頁",
35 | "@backendEnvEnableText": {
36 | "placeholders": {
37 | "value": {
38 | "type": "int"
39 | }
40 | }
41 | },
42 | "backendEnvAppBarTitle": "後端環境設定",
43 | "backendEnvRelease": "Release",
44 | "backendEnvDemo": "Demo",
45 | "backendEnvDevelop": "Develop",
46 | "backendEnvLocal": "Local",
47 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1",
48 | "save": "儲存",
49 |
50 | "incorrectInputFormat": "輸入格式錯誤"
51 | }
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/routing/not_found_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart';
4 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart';
5 | import 'package:{{project_name.snakeCase()}}/routing/app_router.dart';
6 |
7 | class NotFoundPage extends ConsumerWidget {
8 | const NotFoundPage({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context, WidgetRef ref) {
12 | final l10n = context.l10n;
13 | return Scaffold(
14 | body: Padding(
15 | padding: const EdgeInsets.all(Sizes.p16),
16 | child: Center(
17 | child: Column(
18 | mainAxisSize: MainAxisSize.min,
19 | crossAxisAlignment: CrossAxisAlignment.center,
20 | children: [
21 | Text(
22 | l10n.pageNotFound,
23 | style: Theme.of(context).textTheme.headlineMedium,
24 | textAlign: TextAlign.center,
25 | ),
26 | gapH32,
27 | TextButton.icon(
28 | onPressed: () =>
29 | ref.read(appRouterProvider).go(AppRoute.home.path),
30 | icon: const Icon(
31 | Icons.home_outlined,
32 | size: Sizes.p24,
33 | ),
34 | label: Text(
35 | l10n.homePageAppBarTitle,
36 | style: const TextStyle(fontSize: Sizes.p20),
37 | ),
38 | ),
39 | ],
40 | ),
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/start_up/presentation/splash_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:after_layout/after_layout.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_riverpod/flutter_riverpod.dart';
6 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart';
7 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
8 | import 'package:{{project_name.snakeCase()}}/features/start_up/data/start_up_repository.dart';
9 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart';
10 |
11 | class SplashPage extends ConsumerStatefulWidget {
12 | const SplashPage({super.key});
13 |
14 | @override
15 | ConsumerState createState() => _SplashPageState();
16 | }
17 |
18 | class _SplashPageState extends ConsumerState
19 | with AfterLayoutMixin {
20 | @override
21 | FutureOr afterFirstLayout(BuildContext context) =>
22 | ref.read(startUpRepositoryProvider).prepareAuthentication();
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | ref.listen(
27 | authControllerProvider,
28 | (_, state) {
29 | state.whenOrNull(
30 | failure: (apiFailure) =>
31 | apiFailure.showPredefinedSnackbar(context: context, ref: ref),
32 | );
33 | },
34 | );
35 | final colorScheme = context.colorScheme;
36 | return Scaffold(
37 | backgroundColor: colorScheme.background,
38 | body: const Center(
39 | child: FlutterLogo(
40 | size: 64,
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/src/my_app/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/home/presentation/home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/constant/app_sizes.dart';
4 | import 'package:flutterprint/constant/theme/theme.dart';
5 | import 'package:flutterprint/features/authentication/authentication.dart';
6 | import 'package:flutterprint/features/theme_mode/application/theme_mode_controller.dart';
7 | import 'package:flutterprint/l10n/l10n.dart';
8 |
9 | class HomePage extends ConsumerWidget {
10 | const HomePage({super.key});
11 | static const signOutKey = Key('signOut');
12 | @override
13 | Widget build(BuildContext context, WidgetRef ref) {
14 | final l10n = context.l10n;
15 | final theme = ref.watch(themeModeControllerProvider);
16 | final colorScheme = context.colorScheme;
17 | return Scaffold(
18 | backgroundColor: colorScheme.background,
19 | appBar: AppBar(
20 | title: Text(l10n.homePageAppBarTitle),
21 | actions: [
22 | IconButton(
23 | onPressed: () => ref
24 | .read(themeModeControllerProvider.notifier)
25 | .toggleAndSaveThemeMode(),
26 | icon: Icon(
27 | theme == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode,
28 | ),
29 | ),
30 | ],
31 | ),
32 | body: Center(
33 | child: TextButton.icon(
34 | key: HomePage.signOutKey,
35 | onPressed: () => ref.read(authControllerProvider.notifier).signOut(),
36 | icon: const Icon(
37 | Icons.logout_outlined,
38 | size: Sizes.p24,
39 | ),
40 | label: Text(
41 | l10n.signOut,
42 | style: const TextStyle(fontSize: Sizes.p20),
43 | ),
44 | ),
45 | ),
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/home/presentation/home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart';
4 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart';
5 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
6 | import 'package:{{project_name.snakeCase()}}/features/theme_mode/application/theme_mode_controller.dart';
7 | import 'package:{{project_name.snakeCase()}}/l10n/l10n.dart';
8 |
9 | class HomePage extends ConsumerWidget {
10 | const HomePage({super.key});
11 | static const signOutKey = Key('signOut');
12 | @override
13 | Widget build(BuildContext context, WidgetRef ref) {
14 | final l10n = context.l10n;
15 | final theme = ref.watch(themeModeControllerProvider);
16 | final colorScheme = context.colorScheme;
17 | return Scaffold(
18 | backgroundColor: colorScheme.background,
19 | appBar: AppBar(
20 | title: Text(l10n.homePageAppBarTitle),
21 | actions: [
22 | IconButton(
23 | onPressed: () => ref
24 | .read(themeModeControllerProvider.notifier)
25 | .toggleAndSaveThemeMode(),
26 | icon: Icon(
27 | theme == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode,
28 | ),
29 | ),
30 | ],
31 | ),
32 | body: Center(
33 | child: TextButton.icon(
34 | key: HomePage.signOutKey,
35 | onPressed: () => ref.read(authControllerProvider.notifier).signOut(),
36 | icon: const Icon(
37 | Icons.logout_outlined,
38 | size: Sizes.p24,
39 | ),
40 | label: Text(
41 | l10n.signOut,
42 | style: const TextStyle(fontSize: Sizes.p20),
43 | ),
44 | ),
45 | ),
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/my_app/test/robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:flutterprint/features/authentication/authentication.dart';
3 | import 'package:flutterprint/features/authentication/data/fake_auth_repository.dart';
4 | import 'package:flutterprint/main.dart';
5 | import 'package:flutterprint/services/local_storage/data/fake_secure_storage_repository.dart';
6 | import 'package:flutterprint/services/local_storage/local_storage.dart';
7 | import 'package:flutter_test/flutter_test.dart';
8 | import 'package:shared_preferences/shared_preferences.dart';
9 |
10 | import 'features/authentication/auth_robot.dart';
11 | import 'features/backend_environment/backend_robot.dart';
12 |
13 | class Robot {
14 | Robot(this.tester)
15 | : authRobot = AuthRobot(tester),
16 | backendRobot = BackendRobot(tester);
17 | final WidgetTester tester;
18 | final AuthRobot authRobot;
19 | final BackendRobot backendRobot;
20 |
21 | Future pumpAppWidget(
22 | {Map initialSharedPreferenceValues = const {}}) async {
23 | const secureStorageRepository = FakeSecureStorageRepository();
24 | const authRepository = FakeAuthRepository(addDelay: false);
25 | SharedPreferences.setMockInitialValues(initialSharedPreferenceValues);
26 | final sharedPreferences = await SharedPreferences.getInstance();
27 | final sharedPrefRepository = SharedPreferenceRepository(sharedPreferences);
28 | // Create ProviderContainer with any required overrides
29 | final container = ProviderContainer(
30 | overrides: [
31 | secureRepositoryProvider.overrideWithValue(secureStorageRepository),
32 | authRepoProvider.overrideWithValue(authRepository),
33 | sharedPreferenceRepoProvider.overrideWithValue(sharedPrefRepository),
34 | ],
35 | );
36 | await tester.pumpWidget(
37 | UncontrolledProviderScope(
38 | container: container,
39 | child: const AppWidget(),
40 | ),
41 | );
42 | await tester.pumpAndSettle();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/my_app/test/features/authentication/auth_robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/features/authentication/authentication.dart';
2 | import 'package:flutterprint/features/home/presentation/home_page.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 |
5 | class AuthRobot {
6 | const AuthRobot(this.tester);
7 | final WidgetTester tester;
8 |
9 | Future enterAccount(String value) async {
10 | final accountField = find.byKey(SignInPage.accountTextFieldKey);
11 | expect(accountField, findsOneWidget);
12 | await tester.enterText(accountField, value);
13 | }
14 |
15 | Future enterPassword(String value) async {
16 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey);
17 | expect(passwordField, findsOneWidget);
18 | await tester.enterText(passwordField, value);
19 | }
20 |
21 | Future tapEmailAndPasswordSubmitButton() async {
22 | final submitButton = find.byKey(SignInPage.submitButtonKey);
23 | expect(submitButton, findsOneWidget);
24 | await tester.tap(submitButton);
25 | await tester.pumpAndSettle();
26 | }
27 |
28 | Future enterAndSubmitAccountAndPassword() async {
29 | await enterAccount('tester');
30 | await tester.pump();
31 | await enterPassword('123456');
32 | await tapEmailAndPasswordSubmitButton();
33 | }
34 |
35 | Future tapSignOutButton() async {
36 | final signOutButton = find.byKey(HomePage.signOutKey);
37 | expect(signOutButton, findsOneWidget);
38 | await tester.tap(signOutButton);
39 | await tester.pumpAndSettle();
40 | }
41 |
42 | void expectAccountAndPasswordFieldsFound() {
43 | final accountField = find.byKey(SignInPage.accountTextFieldKey);
44 | expect(accountField, findsOneWidget);
45 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey);
46 | expect(passwordField, findsOneWidget);
47 | }
48 |
49 | void expectSignOutButtonFound() {
50 | final signOutBtn = find.byKey(HomePage.signOutKey);
51 | expect(signOutBtn, findsOneWidget);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/my_app/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Flutterprint
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Flutterprint
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 | CFBundleLocalizations
51 |
52 | en
53 | zh
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/local_storage/data/shared_preference_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart' show ThemeMode;
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/features/backend_environment/backend_environment.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 |
6 | final sharedPreferenceRepoProvider =
7 | Provider((ref) {
8 | // The value from this repository will be overriden in main method
9 | // after instantiating Shared Preferences
10 | throw UnimplementedError();
11 | });
12 |
13 | /// Develop enviornment repository
14 | class SharedPreferenceRepository {
15 | final SharedPreferences _pref;
16 |
17 | const SharedPreferenceRepository(this._pref);
18 |
19 | static const prefHostKey = 'hostKey';
20 | static const prefValidSecureStorageKey = 'validSecureStorage';
21 | static const prefStaySignedInKey = 'staySignedIn';
22 | static const prefAppThemeMode = 'appThemeMode';
23 |
24 | String get hostKey =>
25 | _pref.getString(prefHostKey)?.trim() ?? BackendEnv.release.hostKey;
26 |
27 | Future updateHostKey(String host) => _pref.setString(prefHostKey, host);
28 |
29 | Future removeHostKey() => _pref.remove(prefHostKey);
30 |
31 | bool isSecureStorageValid() =>
32 | _pref.getBool(prefValidSecureStorageKey) ?? false;
33 |
34 | Future setSecureStorageAsValid() =>
35 | _pref.setBool(prefValidSecureStorageKey, true);
36 |
37 | Future setStaySignedIn(bool value) =>
38 | _pref.setBool(prefStaySignedInKey, value);
39 |
40 | bool getStaySignedIn() => _pref.getBool(prefStaySignedInKey) ?? false;
41 |
42 | Future setThemeMode(ThemeMode themeMode) async {
43 | return await _pref.setString(prefAppThemeMode, themeMode.name);
44 | }
45 |
46 | ThemeMode getThemeMode() {
47 | final themeName = _pref.getString(prefAppThemeMode);
48 | return ThemeMode.values.firstWhere(
49 | (mode) => mode.name == themeName,
50 | orElse: () => ThemeMode.light,
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/features/authentication/auth_robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
2 | import 'package:{{project_name.snakeCase()}}/features/home/presentation/home_page.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 |
5 | class AuthRobot {
6 | const AuthRobot(this.tester);
7 | final WidgetTester tester;
8 |
9 | Future enterAccount(String value) async {
10 | final accountField = find.byKey(SignInPage.accountTextFieldKey);
11 | expect(accountField, findsOneWidget);
12 | await tester.enterText(accountField, value);
13 | }
14 |
15 | Future enterPassword(String value) async {
16 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey);
17 | expect(passwordField, findsOneWidget);
18 | await tester.enterText(passwordField, value);
19 | }
20 |
21 | Future tapEmailAndPasswordSubmitButton() async {
22 | final submitButton = find.byKey(SignInPage.submitButtonKey);
23 | expect(submitButton, findsOneWidget);
24 | await tester.tap(submitButton);
25 | await tester.pumpAndSettle();
26 | }
27 |
28 | Future enterAndSubmitAccountAndPassword() async {
29 | await enterAccount('tester');
30 | await tester.pump();
31 | await enterPassword('123456');
32 | await tapEmailAndPasswordSubmitButton();
33 | }
34 |
35 | Future tapSignOutButton() async {
36 | final signOutButton = find.byKey(HomePage.signOutKey);
37 | expect(signOutButton, findsOneWidget);
38 | await tester.tap(signOutButton);
39 | await tester.pumpAndSettle();
40 | }
41 |
42 | void expectAccountAndPasswordFieldsFound() {
43 | final accountField = find.byKey(SignInPage.accountTextFieldKey);
44 | expect(accountField, findsOneWidget);
45 | final passwordField = find.byKey(SignInPage.passwordTextFieldKey);
46 | expect(passwordField, findsOneWidget);
47 | }
48 |
49 | void expectSignOutButtonFound() {
50 | final signOutBtn = find.byKey(HomePage.signOutKey);
51 | expect(signOutBtn, findsOneWidget);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | {{project_name.titleCase()}}
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | {{project_name.titleCase()}}
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 | CFBundleLocalizations
51 |
52 | en
53 | zh
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/local_storage/data/shared_preference_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart' show ThemeMode;
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 |
6 | final sharedPreferenceRepoProvider =
7 | Provider((ref) {
8 | // The value from this repository will be overriden in main method
9 | // after instantiating Shared Preferences
10 | throw UnimplementedError();
11 | });
12 |
13 | /// Develop enviornment repository
14 | class SharedPreferenceRepository {
15 | final SharedPreferences _pref;
16 |
17 | const SharedPreferenceRepository(this._pref);
18 |
19 | static const prefHostKey = 'hostKey';
20 | static const prefValidSecureStorageKey = 'validSecureStorage';
21 | static const prefStaySignedInKey = 'staySignedIn';
22 | static const prefAppThemeMode = 'appThemeMode';
23 |
24 | String get hostKey =>
25 | _pref.getString(prefHostKey)?.trim() ?? BackendEnv.release.hostKey;
26 |
27 | Future updateHostKey(String host) => _pref.setString(prefHostKey, host);
28 |
29 | Future removeHostKey() => _pref.remove(prefHostKey);
30 |
31 | bool isSecureStorageValid() =>
32 | _pref.getBool(prefValidSecureStorageKey) ?? false;
33 |
34 | Future setSecureStorageAsValid() =>
35 | _pref.setBool(prefValidSecureStorageKey, true);
36 |
37 | Future setStaySignedIn(bool value) =>
38 | _pref.setBool(prefStaySignedInKey, value);
39 |
40 | bool getStaySignedIn() => _pref.getBool(prefStaySignedInKey) ?? false;
41 |
42 | Future setThemeMode(ThemeMode themeMode) async {
43 | return await _pref.setString(prefAppThemeMode, themeMode.name);
44 | }
45 |
46 | ThemeMode getThemeMode() {
47 | final themeName = _pref.getString(prefAppThemeMode);
48 | return ThemeMode.values.firstWhere(
49 | (mode) => mode.name == themeName,
50 | orElse: () => ThemeMode.light,
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/my_app/lib/widgets/countdown_trigger.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter/material.dart';
3 |
4 | /// A countdown trigger is a widget that starts counting down from a specified number [startFrom],
5 | /// and decreases by 1 every time the child widget is pressed, the [onUpdated] callback is triggered each time the count decreases.
6 | /// When the count reaches 0, the [onFinished] callback is triggered and the count will reset back to [startFrom].
7 | ///
8 | /// If there are no press events within [resetDurationInSec] seconds,
9 | /// the count will reset back to [startFrom].
10 | class CountdownTrigger extends StatefulWidget {
11 | final Widget child;
12 | final int startFrom;
13 | final int resetDurationInSec;
14 | final void Function(int)? onUpdated;
15 | final VoidCallback? onFinished;
16 | const CountdownTrigger({
17 | super.key,
18 | required this.startFrom,
19 | required this.child,
20 | this.resetDurationInSec = 3,
21 | this.onUpdated,
22 | this.onFinished,
23 | }) : assert(startFrom > 0),
24 | assert(resetDurationInSec > 0);
25 |
26 | @override
27 | State createState() => _CountdownTriggerState();
28 | }
29 |
30 | class _CountdownTriggerState extends State {
31 | Timer? timer;
32 | late int count;
33 |
34 | @override
35 | void initState() {
36 | super.initState();
37 | count = widget.startFrom;
38 | }
39 |
40 | @override
41 | void dispose() {
42 | timer?.cancel();
43 | super.dispose();
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return GestureDetector(
49 | onTap: () {
50 | timer?.cancel();
51 | timer = Timer(
52 | Duration(seconds: widget.resetDurationInSec),
53 | () => count = widget.startFrom,
54 | );
55 | count--;
56 | if (count > 0) {
57 | widget.onUpdated?.call(count);
58 | } else {
59 | timer?.cancel();
60 | count = widget.startFrom;
61 | widget.onFinished?.call();
62 | }
63 | },
64 | child: widget.child,
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_riverpod/flutter_riverpod.dart';
2 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/authentication/data/fake_auth_repository.dart';
4 | import 'package:{{project_name.snakeCase()}}/main.dart';
5 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/fake_secure_storage_repository.dart';
6 | import 'package:{{project_name.snakeCase()}}/services/local_storage/local_storage.dart';
7 | import 'package:flutter_test/flutter_test.dart';
8 | import 'package:shared_preferences/shared_preferences.dart';
9 |
10 | import 'features/authentication/auth_robot.dart';
11 | import 'features/backend_environment/backend_robot.dart';
12 |
13 | class Robot {
14 | Robot(this.tester)
15 | : authRobot = AuthRobot(tester),
16 | backendRobot = BackendRobot(tester);
17 | final WidgetTester tester;
18 | final AuthRobot authRobot;
19 | final BackendRobot backendRobot;
20 |
21 | Future pumpAppWidget(
22 | {Map initialSharedPreferenceValues = const {}}) async {
23 | const secureStorageRepository = FakeSecureStorageRepository();
24 | const authRepository = FakeAuthRepository(addDelay: false);
25 | SharedPreferences.setMockInitialValues(initialSharedPreferenceValues);
26 | final sharedPreferences = await SharedPreferences.getInstance();
27 | final sharedPrefRepository = SharedPreferenceRepository(sharedPreferences);
28 | // Create ProviderContainer with any required overrides
29 | final container = ProviderContainer(
30 | overrides: [
31 | secureRepositoryProvider.overrideWithValue(secureStorageRepository),
32 | authRepoProvider.overrideWithValue(authRepository),
33 | sharedPreferenceRepoProvider.overrideWithValue(sharedPrefRepository),
34 | ],
35 | );
36 | await tester.pumpWidget(
37 | UncontrolledProviderScope(
38 | container: container,
39 | child: const AppWidget(),
40 | ),
41 | );
42 | await tester.pumpAndSettle();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/widgets/countdown_trigger.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter/material.dart';
3 |
4 | /// A countdown trigger is a widget that starts counting down from a specified number [startFrom],
5 | /// and decreases by 1 every time the child widget is pressed, the [onUpdated] callback is triggered each time the count decreases.
6 | /// When the count reaches 0, the [onFinished] callback is triggered and the count will reset back to [startFrom].
7 | ///
8 | /// If there are no press events within [resetDurationInSec] seconds,
9 | /// the count will reset back to [startFrom].
10 | class CountdownTrigger extends StatefulWidget {
11 | final Widget child;
12 | final int startFrom;
13 | final int resetDurationInSec;
14 | final void Function(int)? onUpdated;
15 | final VoidCallback? onFinished;
16 | const CountdownTrigger({
17 | super.key,
18 | required this.startFrom,
19 | required this.child,
20 | this.resetDurationInSec = 3,
21 | this.onUpdated,
22 | this.onFinished,
23 | }) : assert(startFrom > 0),
24 | assert(resetDurationInSec > 0);
25 |
26 | @override
27 | State createState() => _CountdownTriggerState();
28 | }
29 |
30 | class _CountdownTriggerState extends State {
31 | Timer? timer;
32 | late int count;
33 |
34 | @override
35 | void initState() {
36 | super.initState();
37 | count = widget.startFrom;
38 | }
39 |
40 | @override
41 | void dispose() {
42 | timer?.cancel();
43 | super.dispose();
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return GestureDetector(
49 | onTap: () {
50 | timer?.cancel();
51 | timer = Timer(
52 | Duration(seconds: widget.resetDurationInSec),
53 | () => count = widget.startFrom,
54 | );
55 | count--;
56 | if (count > 0) {
57 | widget.onUpdated?.call(count);
58 | } else {
59 | timer?.cancel();
60 | count = widget.startFrom;
61 | widget.onFinished?.call();
62 | }
63 | },
64 | child: widget.child,
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/my_app/lib/features/authentication/data/auth_repository.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_riverpod/flutter_riverpod.dart';
4 | import 'package:flutterprint/features/authentication/authentication.dart';
5 | import 'package:flutterprint/features/authentication/data/fake_auth_repository.dart';
6 | import 'package:flutterprint/features/backend_environment/backend_environment.dart';
7 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart';
8 |
9 | final authRepoProvider = Provider((ref) {
10 | return const FakeAuthRepository();
11 | // TODO: Replace the repository with AuthRepositoryImpl for real implementation
12 | // final restApiService = ref.watch(restApiServiceProvider);
13 | // return AuthRepositoryImpl(restApiService, ref);
14 | });
15 |
16 | abstract class AuthRepository {
17 | Future> signIn({
18 | required String username,
19 | required String password,
20 | });
21 |
22 | Future> refreshToken();
23 | }
24 |
25 | class AuthRepositoryImpl implements AuthRepository {
26 | const AuthRepositoryImpl(this.restApiService, this.ref);
27 |
28 | final RestApiService restApiService;
29 | final Ref ref;
30 |
31 | @override
32 | Future> signIn({
33 | required String username,
34 | required String password,
35 | }) =>
36 | restApiService.post(
37 | url: ref.read(hostControllerProvider).getUri(Endpoint.signIn),
38 | tokenType: TokenType.none,
39 | body: jsonEncode({
40 | 'username': username,
41 | 'password': password,
42 | }),
43 | dataParser: _authInfoParser,
44 | );
45 |
46 | @override
47 | Future> refreshToken() => restApiService.put(
48 | url: ref.read(hostControllerProvider).getUri(Endpoint.refreshToken),
49 | tokenType: TokenType.account,
50 | dataParser: _authInfoParser,
51 | );
52 |
53 | AuthInfo _authInfoParser(String body) {
54 | final jsonResponse = jsonDecode(body);
55 | return AuthInfo.fromJson(jsonResponse as Map);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/my_app/lib/l10n/arb/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "@@locale": "en",
3 | "account": "Account",
4 | "@account": {
5 | "description": "Text shown in the title of account text field"
6 | },
7 | "password": "Password",
8 | "@password": {
9 | "description": "Text shown in the title of password text field"
10 | },
11 | "signinAppBarTitle": "Sign In",
12 | "@signinAppBarTitle": {
13 | "description": "Title of the sign in page"
14 | },
15 |
16 | "pageNotFound": "404 - Page not found",
17 |
18 | "homePageAppBarTitle": "Home",
19 | "signOut": "Sign out",
20 | "staySignedin": "Keep me signed in",
21 | "submit": "Submit",
22 | "authenticating": "Authenticating...",
23 | "accountCanNotBeEmpty": "Account can't be empty",
24 | "passwordCanNotBeEmpty": "Password can't be empty",
25 | "incorrectAccountOrPassword": "Account or password is incorrect",
26 |
27 | "apiAgreementConflict": "API agreement conflict",
28 | "badRequest": "Bad request",
29 | "forbidden": "Permission denied",
30 | "socket": "Socket error",
31 | "notFound": "Not found",
32 | "serviceUnavailable": "Service unavailable",
33 | "timeout": "Request timeout",
34 | "unauthorized": "Unauthorized",
35 | "unknown": "An error occurred with status code: {statusCode}",
36 | "@unknown": {
37 | "placeholders": {
38 | "statusCode": {
39 | "type": "int"
40 | }
41 | }
42 | },
43 |
44 | "backendEnvEnableText": "Press {value} more times to access the backend environment settings page",
45 | "@backendEnvEnableText": {
46 | "placeholders": {
47 | "value": {
48 | "type": "int"
49 | }
50 | }
51 | },
52 | "backendEnvAppBarTitle": "Backend Environment",
53 | "backendEnvRelease": "Release",
54 | "backendEnvDemo": "Demo",
55 | "backendEnvDevelop": "Develop",
56 | "backendEnvLocal": "Local",
57 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1:8000",
58 | "save": "Save",
59 |
60 | "incorrectInputFormat": "Incorrect input format"
61 | }
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/features/authentication/data/auth_repository.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_riverpod/flutter_riverpod.dart';
4 | import 'package:{{project_name.snakeCase()}}/features/authentication/authentication.dart';
5 | import 'package:{{project_name.snakeCase()}}/features/authentication/data/fake_auth_repository.dart';
6 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/backend_environment.dart';
7 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart';
8 |
9 | final authRepoProvider = Provider((ref) {
10 | return const FakeAuthRepository();
11 | // TODO: Replace the repository with AuthRepositoryImpl for real implementation
12 | // final restApiService = ref.watch(restApiServiceProvider);
13 | // return AuthRepositoryImpl(restApiService, ref);
14 | });
15 |
16 | abstract class AuthRepository {
17 | Future> signIn({
18 | required String username,
19 | required String password,
20 | });
21 |
22 | Future> refreshToken();
23 | }
24 |
25 | class AuthRepositoryImpl implements AuthRepository {
26 | const AuthRepositoryImpl(this.restApiService, this.ref);
27 |
28 | final RestApiService restApiService;
29 | final Ref ref;
30 |
31 | @override
32 | Future> signIn({
33 | required String username,
34 | required String password,
35 | }) =>
36 | restApiService.post(
37 | url: ref.read(hostControllerProvider).getUri(Endpoint.signIn),
38 | tokenType: TokenType.none,
39 | body: jsonEncode({
40 | 'username': username,
41 | 'password': password,
42 | }),
43 | dataParser: _authInfoParser,
44 | );
45 |
46 | @override
47 | Future> refreshToken() => restApiService.put(
48 | url: ref.read(hostControllerProvider).getUri(Endpoint.refreshToken),
49 | tokenType: TokenType.account,
50 | dataParser: _authInfoParser,
51 | );
52 |
53 | AuthInfo _authInfoParser(String body) {
54 | final jsonResponse = jsonDecode(body);
55 | return AuthInfo.fromJson(jsonResponse as Map);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/l10n/arb/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "@@locale": "en",
3 | "account": "Account",
4 | "@account": {
5 | "description": "Text shown in the title of account text field"
6 | },
7 | "password": "Password",
8 | "@password": {
9 | "description": "Text shown in the title of password text field"
10 | },
11 | "signinAppBarTitle": "Sign In",
12 | "@signinAppBarTitle": {
13 | "description": "Title of the sign in page"
14 | },
15 |
16 | "pageNotFound": "404 - Page not found",
17 |
18 | "homePageAppBarTitle": "Home",
19 | "signOut": "Sign out",
20 | "staySignedin": "Keep me signed in",
21 | "submit": "Submit",
22 | "authenticating": "Authenticating...",
23 | "accountCanNotBeEmpty": "Account can't be empty",
24 | "passwordCanNotBeEmpty": "Password can't be empty",
25 | "incorrectAccountOrPassword": "Account or password is incorrect",
26 |
27 | "apiAgreementConflict": "API agreement conflict",
28 | "badRequest": "Bad request",
29 | "forbidden": "Permission denied",
30 | "socket": "Socket error",
31 | "notFound": "Not found",
32 | "serviceUnavailable": "Service unavailable",
33 | "timeout": "Request timeout",
34 | "unauthorized": "Unauthorized",
35 | "unknown": "An error occurred with status code: {statusCode}",
36 | "@unknown": {
37 | "placeholders": {
38 | "statusCode": {
39 | "type": "int"
40 | }
41 | }
42 | },
43 |
44 | "backendEnvEnableText": "Press {value} more times to access the backend environment settings page",
45 | "@backendEnvEnableText": {
46 | "placeholders": {
47 | "value": {
48 | "type": "int"
49 | }
50 | }
51 | },
52 | "backendEnvAppBarTitle": "Backend Environment",
53 | "backendEnvRelease": "Release",
54 | "backendEnvDemo": "Demo",
55 | "backendEnvDevelop": "Develop",
56 | "backendEnvLocal": "Local",
57 | "backendEnvLocalDescription": "Enter the local IP address followed by a port number.\nFor example, you can access the host at http://192.168.0.1:8000 by entering 192.168.0.1:8000",
58 | "save": "Save",
59 |
60 | "incorrectInputFormat": "Incorrect input format"
61 | }
--------------------------------------------------------------------------------
/src/my_app/test/features/backend_environment/backend_robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutterprint/features/backend_environment/domain/backend_env.dart';
3 | import 'package:flutterprint/features/backend_environment/presentation/backend_environment_setting_page.dart';
4 | import 'package:flutterprint/widgets/countdown_trigger.dart';
5 | import 'package:flutter_test/flutter_test.dart';
6 |
7 | class BackendRobot {
8 | const BackendRobot(this.tester);
9 | final WidgetTester tester;
10 |
11 | void expectBannerFoundWithName(String bannerName) {
12 | final banner = find.byKey(Key(bannerName));
13 | expect(banner, findsOneWidget);
14 | }
15 |
16 | void expectBannerNotFound() {
17 | final banner = find.byType(Banner);
18 | expect(banner, findsNothing);
19 | }
20 |
21 | Future tapCountdownTriggerNTimes(int times) async {
22 | final countdownTriggerWidget = find.byType(CountdownTrigger);
23 | expect(countdownTriggerWidget, findsOneWidget);
24 |
25 | for (int i = 0; i < times; ++i) {
26 | await tester.tap(countdownTriggerWidget);
27 | await tester.pump();
28 | }
29 | }
30 |
31 | Future tapReleaseRadio() => _tapRadio(BackendEnv.release);
32 |
33 | Future tapDemoRadio() => _tapRadio(BackendEnv.demo);
34 |
35 | Future tapDevelopRadio() => _tapRadio(BackendEnv.develop);
36 |
37 | Future _tapRadio(BackendEnv env) async {
38 | final radio = find.byKey(Key(env.name));
39 | await tester.tap(radio);
40 | await tester.pumpAndSettle();
41 | }
42 |
43 | Future tapLocalHostRadio() async {
44 | final radio = find.byKey(BackendEnvironmentSettingPage.localHostRadioKey);
45 | await tester.tap(radio);
46 | await tester.pumpAndSettle();
47 | }
48 |
49 | Future enterLocalHost(String value) async {
50 | final localHostTextField =
51 | find.byKey(BackendEnvironmentSettingPage.localHostTextFieldKey);
52 | expect(localHostTextField, findsOneWidget);
53 | await tester.enterText(localHostTextField, value);
54 | }
55 |
56 | Future tapSaveButton() async {
57 | final saveButton = find.byKey(BackendEnvironmentSettingPage.saveKey);
58 | await tester.tap(saveButton);
59 | await tester.pumpAndSettle();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/features/backend_environment/backend_robot.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/domain/backend_env.dart';
3 | import 'package:{{project_name.snakeCase()}}/features/backend_environment/presentation/backend_environment_setting_page.dart';
4 | import 'package:{{project_name.snakeCase()}}/widgets/countdown_trigger.dart';
5 | import 'package:flutter_test/flutter_test.dart';
6 |
7 | class BackendRobot {
8 | const BackendRobot(this.tester);
9 | final WidgetTester tester;
10 |
11 | void expectBannerFoundWithName(String bannerName) {
12 | final banner = find.byKey(Key(bannerName));
13 | expect(banner, findsOneWidget);
14 | }
15 |
16 | void expectBannerNotFound() {
17 | final banner = find.byType(Banner);
18 | expect(banner, findsNothing);
19 | }
20 |
21 | Future tapCountdownTriggerNTimes(int times) async {
22 | final countdownTriggerWidget = find.byType(CountdownTrigger);
23 | expect(countdownTriggerWidget, findsOneWidget);
24 |
25 | for (int i = 0; i < times; ++i) {
26 | await tester.tap(countdownTriggerWidget);
27 | await tester.pump();
28 | }
29 | }
30 |
31 | Future tapReleaseRadio() => _tapRadio(BackendEnv.release);
32 |
33 | Future tapDemoRadio() => _tapRadio(BackendEnv.demo);
34 |
35 | Future tapDevelopRadio() => _tapRadio(BackendEnv.develop);
36 |
37 | Future _tapRadio(BackendEnv env) async {
38 | final radio = find.byKey(Key(env.name));
39 | await tester.tap(radio);
40 | await tester.pumpAndSettle();
41 | }
42 |
43 | Future tapLocalHostRadio() async {
44 | final radio = find.byKey(BackendEnvironmentSettingPage.localHostRadioKey);
45 | await tester.tap(radio);
46 | await tester.pumpAndSettle();
47 | }
48 |
49 | Future enterLocalHost(String value) async {
50 | final localHostTextField =
51 | find.byKey(BackendEnvironmentSettingPage.localHostTextFieldKey);
52 | expect(localHostTextField, findsOneWidget);
53 | await tester.enterText(localHostTextField, value);
54 | }
55 |
56 | Future tapSaveButton() async {
57 | final saveButton = find.byKey(BackendEnvironmentSettingPage.saveKey);
58 | await tester.tap(saveButton);
59 | await tester.pumpAndSettle();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/snackbar/app_snackbar_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/constant/app_sizes.dart';
4 | import 'package:flutterprint/constant/theme/theme.dart';
5 |
6 | final appSnackBarRepoProvider = Provider((ref) => AppSnackBarRepository(ref));
7 |
8 | class AppSnackBarRepository {
9 | final Ref ref;
10 |
11 | const AppSnackBarRepository(this.ref);
12 |
13 | /// Clear snackbars based on the given [context]
14 | void clearSnackbars(BuildContext context) =>
15 | ScaffoldMessenger.of(context).clearSnackBars();
16 |
17 | /// Show a snackbar based on the given [context]
18 | void showSnackbar(
19 | BuildContext context, {
20 | required String message,
21 | SnackBarBehavior behavior = SnackBarBehavior.floating,
22 | bool overlayOther = true,
23 | Duration duration = const Duration(seconds: 3),
24 | }) {
25 | if (overlayOther) {
26 | ScaffoldMessenger.of(context).clearSnackBars();
27 | }
28 | final colorScheme = context.colorScheme;
29 | ScaffoldMessenger.of(context).showSnackBar(
30 | SnackBar(
31 | margin: const EdgeInsets.all(Sizes.p32),
32 | padding: EdgeInsets.zero,
33 | content: Padding(
34 | padding: const EdgeInsets.symmetric(
35 | vertical: Sizes.p12,
36 | horizontal: Sizes.p16,
37 | ),
38 | child: Column(
39 | mainAxisSize: MainAxisSize.min,
40 | children: [
41 | Container(
42 | padding: EdgeInsets.zero,
43 | margin: EdgeInsets.zero,
44 | alignment: Alignment.centerLeft,
45 | child: SizedBox(
46 | child: Align(
47 | alignment: Alignment.centerLeft,
48 | child: Text(
49 | message,
50 | style: Theme.of(context)
51 | .textTheme
52 | .titleMedium!
53 | .copyWith(color: colorScheme.surface),
54 | ),
55 | ),
56 | ),
57 | ),
58 | ],
59 | ),
60 | ),
61 | behavior: SnackBarBehavior.floating,
62 | duration: duration,
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/snackbar/app_snackbar_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/constant/app_sizes.dart';
4 | import 'package:{{project_name.snakeCase()}}/constant/theme/theme.dart';
5 |
6 | final appSnackBarRepoProvider = Provider((ref) => AppSnackBarRepository(ref));
7 |
8 | class AppSnackBarRepository {
9 | final Ref ref;
10 |
11 | const AppSnackBarRepository(this.ref);
12 |
13 | /// Clear snackbars based on the given [context]
14 | void clearSnackbars(BuildContext context) =>
15 | ScaffoldMessenger.of(context).clearSnackBars();
16 |
17 | /// Show a snackbar based on the given [context]
18 | void showSnackbar(
19 | BuildContext context, {
20 | required String message,
21 | SnackBarBehavior behavior = SnackBarBehavior.floating,
22 | bool overlayOther = true,
23 | Duration duration = const Duration(seconds: 3),
24 | }) {
25 | if (overlayOther) {
26 | ScaffoldMessenger.of(context).clearSnackBars();
27 | }
28 | final colorScheme = context.colorScheme;
29 | ScaffoldMessenger.of(context).showSnackBar(
30 | SnackBar(
31 | margin: const EdgeInsets.all(Sizes.p32),
32 | padding: EdgeInsets.zero,
33 | content: Padding(
34 | padding: const EdgeInsets.symmetric(
35 | vertical: Sizes.p12,
36 | horizontal: Sizes.p16,
37 | ),
38 | child: Column(
39 | mainAxisSize: MainAxisSize.min,
40 | children: [
41 | Container(
42 | padding: EdgeInsets.zero,
43 | margin: EdgeInsets.zero,
44 | alignment: Alignment.centerLeft,
45 | child: SizedBox(
46 | child: Align(
47 | alignment: Alignment.centerLeft,
48 | child: Text(
49 | message,
50 | style: Theme.of(context)
51 | .textTheme
52 | .titleMedium!
53 | .copyWith(color: colorScheme.surface),
54 | ),
55 | ),
56 | ),
57 | ),
58 | ],
59 | ),
60 | ),
61 | behavior: SnackBarBehavior.floating,
62 | duration: duration,
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/my_app/lib/constant/theme/color_schemes.dart:
--------------------------------------------------------------------------------
1 | /// This file is generated from https://m3.material.io/theme-builder#/custom
2 | import 'package:flutter/material.dart';
3 |
4 | const lightColorScheme = ColorScheme(
5 | brightness: Brightness.light,
6 | primary: Color(0xFF6750A4),
7 | onPrimary: Color(0xFFFFFFFF),
8 | primaryContainer: Color(0xFFEADDFF),
9 | onPrimaryContainer: Color(0xFF21005D),
10 | secondary: Color(0xFF625B71),
11 | onSecondary: Color(0xFFFFFFFF),
12 | secondaryContainer: Color(0xFFE8DEF8),
13 | onSecondaryContainer: Color(0xFF1D192B),
14 | tertiary: Color(0xFF7D5260),
15 | onTertiary: Color(0xFFFFFFFF),
16 | tertiaryContainer: Color(0xFFFFD8E4),
17 | onTertiaryContainer: Color(0xFF31111D),
18 | error: Color(0xFFB3261E),
19 | onError: Color(0xFFFFFFFF),
20 | errorContainer: Color(0xFFF9DEDC),
21 | onErrorContainer: Color(0xFF410E0B),
22 | outline: Color(0xFF79747E),
23 | background: Color(0xFFFFFBFE),
24 | onBackground: Color(0xFF1C1B1F),
25 | surface: Color(0xFFFFFBFE),
26 | onSurface: Color(0xFF1C1B1F),
27 | surfaceVariant: Color(0xFFE7E0EC),
28 | onSurfaceVariant: Color(0xFF49454F),
29 | inverseSurface: Color(0xFF313033),
30 | onInverseSurface: Color(0xFFF4EFF4),
31 | inversePrimary: Color(0xFFD0BCFF),
32 | shadow: Color(0xFF000000),
33 | surfaceTint: Color(0xFF6750A4),
34 | );
35 |
36 | const darkColorScheme = ColorScheme(
37 | brightness: Brightness.dark,
38 | primary: Color(0xFFD0BCFF),
39 | onPrimary: Color(0xFF381E72),
40 | primaryContainer: Color(0xFF4F378B),
41 | onPrimaryContainer: Color(0xFFEADDFF),
42 | secondary: Color(0xFFCCC2DC),
43 | onSecondary: Color(0xFF332D41),
44 | secondaryContainer: Color(0xFF4A4458),
45 | onSecondaryContainer: Color(0xFFE8DEF8),
46 | tertiary: Color(0xFFEFB8C8),
47 | onTertiary: Color(0xFF492532),
48 | tertiaryContainer: Color(0xFF633B48),
49 | onTertiaryContainer: Color(0xFFFFD8E4),
50 | error: Color(0xFFF2B8B5),
51 | onError: Color(0xFF601410),
52 | errorContainer: Color(0xFF8C1D18),
53 | onErrorContainer: Color(0xFFF9DEDC),
54 | outline: Color(0xFF938F99),
55 | background: Color(0xFF1C1B1F),
56 | onBackground: Color(0xFFE6E1E5),
57 | surface: Color(0xFF1C1B1F),
58 | onSurface: Color(0xFFE6E1E5),
59 | surfaceVariant: Color(0xFF49454F),
60 | onSurfaceVariant: Color(0xFFCAC4D0),
61 | inverseSurface: Color(0xFFE6E1E5),
62 | onInverseSurface: Color(0xFF313033),
63 | inversePrimary: Color(0xFF6750A4),
64 | shadow: Color(0xFF000000),
65 | surfaceTint: Color(0xFFD0BCFF),
66 | );
67 |
--------------------------------------------------------------------------------
/src/my_app/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 |
--------------------------------------------------------------------------------
/src/my_app/test/features/authentication/auth_flow_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutterprint/services/local_storage/data/shared_preference_repository.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | import '../../robot.dart';
5 |
6 | void main() {
7 | group(
8 | '[AuthFlow]:',
9 | () {
10 | testWidgets(
11 | 'Verifies the sign in and sign out flow of the app by starting on the SignInPage, '
12 | 'and verifying that entering and submitting the correct account and password redirects to the HomePage, '
13 | 'and tapping the sign out button on the HomePage redirects back to the SignInPage',
14 | (tester) async {
15 | await tester.runAsync(
16 | () async {
17 | final r = Robot(tester);
18 | await r.pumpAppWidget();
19 |
20 | // SignInPage should be present on app started
21 | r.authRobot.expectAccountAndPasswordFieldsFound();
22 | await r.authRobot.enterAndSubmitAccountAndPassword();
23 |
24 | // HomePage should be present after sign in
25 | r.authRobot.expectSignOutButtonFound();
26 | await r.authRobot.tapSignOutButton();
27 |
28 | // SignInPage should be present after sign out
29 | r.authRobot.expectAccountAndPasswordFieldsFound();
30 | },
31 | );
32 | },
33 | );
34 |
35 | testWidgets(
36 | "Verifies the sign out flow of the app by starting on the HomePage if the 'stay signed in' value saved in shared preferences is true, "
37 | "and tapping the sign out button on the HomePage redirects back to the SignInPage",
38 | (tester) async {
39 | await tester.runAsync(
40 | () async {
41 | final r = Robot(tester);
42 | await r.pumpAppWidget(
43 | initialSharedPreferenceValues: {
44 | SharedPreferenceRepository.prefStaySignedInKey: true,
45 | },
46 | );
47 | // HomePage should be present on app started if 'stay signed in' value saved in shared preferences is true
48 | // upon successful authentication
49 | r.authRobot.expectSignOutButtonFound();
50 | await r.authRobot.tapSignOutButton();
51 |
52 | // SignInPage should be present after sign out
53 | r.authRobot.expectAccountAndPasswordFieldsFound();
54 | },
55 | );
56 | },
57 | );
58 | },
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/my_app/lib/services/rest_api_service/data/rest_api_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:flutterprint/services/rest_api_service/rest_api_service.dart';
4 |
5 | final restApiServiceProvider = Provider((ref) {
6 | final client = ref.read(httpClientProvider);
7 | final restApiService = RestApiServiceImpl(ref, httpClient: client);
8 | return restApiService;
9 | });
10 |
11 | /// A service class that makes HTTP REST API requests
12 | /// and returns either an ApiFailure object or a successful result of generic type T
13 | abstract class RestApiService {
14 | /// Sends an HTTP GET request with the given headers to the given [url]
15 | ///
16 | /// {@macro rest_api_request_helper}
17 | Future> get({
18 | required Uri url,
19 | required T Function(String) dataParser,
20 | Map? customHeaders,
21 | TokenType tokenType = TokenType.account,
22 | });
23 |
24 | /// Sends an HTTP POST request with the given [body] and headers to the given [url]
25 | ///
26 | /// {@macro rest_api_request_helper}
27 | Future> post({
28 | required Uri url,
29 | required T Function(String) dataParser,
30 | Map? customHeaders,
31 | TokenType tokenType = TokenType.account,
32 | required Object body,
33 | });
34 |
35 | /// Sends an HTTP PATCH request with the given [body] and headers to the given [url]
36 | ///
37 | /// {@macro rest_api_request_helper}
38 | Future> patch({
39 | required Uri url,
40 | required T Function(String) dataParser,
41 | Map? customHeaders,
42 | TokenType tokenType = TokenType.account,
43 | Object? body,
44 | });
45 |
46 | /// Sends an HTTP PUT request with the given [body] and headers to the given [url]
47 | ///
48 | /// {@macro rest_api_request_helper}
49 | Future> put({
50 | required Uri url,
51 | required T Function(String) dataParser,
52 | Map? customHeaders,
53 | TokenType tokenType = TokenType.account,
54 | Object? body,
55 | });
56 |
57 | /// Sends an HTTP DELETE request with the given headers to the given [url]
58 | ///
59 | /// {@macro rest_api_request_helper}
60 | Future> delete({
61 | required Uri url,
62 | required T Function(String) dataParser,
63 | Map? customHeaders,
64 | TokenType tokenType = TokenType.account,
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/constant/theme/color_schemes.dart:
--------------------------------------------------------------------------------
1 | /// This file is generated from https://m3.material.io/theme-builder#/custom
2 | import 'package:flutter/material.dart';
3 |
4 | const lightColorScheme = ColorScheme(
5 | brightness: Brightness.light,
6 | primary: Color(0xFF6750A4),
7 | onPrimary: Color(0xFFFFFFFF),
8 | primaryContainer: Color(0xFFEADDFF),
9 | onPrimaryContainer: Color(0xFF21005D),
10 | secondary: Color(0xFF625B71),
11 | onSecondary: Color(0xFFFFFFFF),
12 | secondaryContainer: Color(0xFFE8DEF8),
13 | onSecondaryContainer: Color(0xFF1D192B),
14 | tertiary: Color(0xFF7D5260),
15 | onTertiary: Color(0xFFFFFFFF),
16 | tertiaryContainer: Color(0xFFFFD8E4),
17 | onTertiaryContainer: Color(0xFF31111D),
18 | error: Color(0xFFB3261E),
19 | onError: Color(0xFFFFFFFF),
20 | errorContainer: Color(0xFFF9DEDC),
21 | onErrorContainer: Color(0xFF410E0B),
22 | outline: Color(0xFF79747E),
23 | background: Color(0xFFFFFBFE),
24 | onBackground: Color(0xFF1C1B1F),
25 | surface: Color(0xFFFFFBFE),
26 | onSurface: Color(0xFF1C1B1F),
27 | surfaceVariant: Color(0xFFE7E0EC),
28 | onSurfaceVariant: Color(0xFF49454F),
29 | inverseSurface: Color(0xFF313033),
30 | onInverseSurface: Color(0xFFF4EFF4),
31 | inversePrimary: Color(0xFFD0BCFF),
32 | shadow: Color(0xFF000000),
33 | surfaceTint: Color(0xFF6750A4),
34 | );
35 |
36 | const darkColorScheme = ColorScheme(
37 | brightness: Brightness.dark,
38 | primary: Color(0xFFD0BCFF),
39 | onPrimary: Color(0xFF381E72),
40 | primaryContainer: Color(0xFF4F378B),
41 | onPrimaryContainer: Color(0xFFEADDFF),
42 | secondary: Color(0xFFCCC2DC),
43 | onSecondary: Color(0xFF332D41),
44 | secondaryContainer: Color(0xFF4A4458),
45 | onSecondaryContainer: Color(0xFFE8DEF8),
46 | tertiary: Color(0xFFEFB8C8),
47 | onTertiary: Color(0xFF492532),
48 | tertiaryContainer: Color(0xFF633B48),
49 | onTertiaryContainer: Color(0xFFFFD8E4),
50 | error: Color(0xFFF2B8B5),
51 | onError: Color(0xFF601410),
52 | errorContainer: Color(0xFF8C1D18),
53 | onErrorContainer: Color(0xFFF9DEDC),
54 | outline: Color(0xFF938F99),
55 | background: Color(0xFF1C1B1F),
56 | onBackground: Color(0xFFE6E1E5),
57 | surface: Color(0xFF1C1B1F),
58 | onSurface: Color(0xFFE6E1E5),
59 | surfaceVariant: Color(0xFF49454F),
60 | onSurfaceVariant: Color(0xFFCAC4D0),
61 | inverseSurface: Color(0xFFE6E1E5),
62 | onInverseSurface: Color(0xFF313033),
63 | inversePrimary: Color(0xFF6750A4),
64 | shadow: Color(0xFF000000),
65 | surfaceTint: Color(0xFFD0BCFF),
66 | );
67 |
--------------------------------------------------------------------------------
/brick/hooks/post_gen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:mason/mason.dart';
4 |
5 | void run(HookContext context) async {
6 | final logger = context.logger;
7 | final directory = Directory.current.path;
8 | final projectName = context.vars['project_name'] as String;
9 | late List folders;
10 |
11 | if (Platform.isWindows) {
12 | folders = directory.split(r'\').toList();
13 | } else {
14 | folders = directory.split('/').toList();
15 | }
16 |
17 | final projectRoot = [...folders, projectName.snakeCase].join('/');
18 |
19 | final isFlutterInstalled = await Flutter.installed(
20 | logger: logger,
21 | workingDirectory: projectRoot,
22 | );
23 |
24 | if (isFlutterInstalled) {
25 | await Flutter.pubGet(
26 | logger: logger,
27 | workingDirectory: projectRoot,
28 | );
29 | }
30 |
31 | logger.success('${context.vars['project_name']} generated successfully 🎉');
32 | }
33 |
34 | class Flutter {
35 | static Future installed({
36 | String? workingDirectory,
37 | required Logger logger,
38 | }) async {
39 | final progress = logger.progress('Checking if flutter is installed');
40 | var isFlutterInstalled = false;
41 | try {
42 | logger.detail('Running: flutter --version');
43 | final result = await Process.run(
44 | 'flutter',
45 | ['--version'],
46 | workingDirectory: workingDirectory,
47 | runInShell: true,
48 | );
49 | logger
50 | ..detail('stdout:\n${result.stdout}')
51 | ..detail('stderr:\n${result.stderr}');
52 | isFlutterInstalled = true;
53 | } catch (_) {
54 | isFlutterInstalled = false;
55 | } finally {
56 | progress.complete();
57 | if (!isFlutterInstalled) {
58 | logger.alert('Flutter is not installed');
59 | }
60 | return isFlutterInstalled;
61 | }
62 | }
63 |
64 | static Future pubGet({
65 | String? workingDirectory,
66 | required Logger logger,
67 | }) async {
68 | final progress =
69 | logger.progress('Running flutter pub get in $workingDirectory');
70 | try {
71 | final result = await Process.run(
72 | 'flutter',
73 | ['pub', 'get'],
74 | workingDirectory: workingDirectory,
75 | runInShell: true,
76 | );
77 | logger
78 | ..detail('stdout:\n${result.stdout}')
79 | ..detail('stderr:\n${result.stderr}');
80 | progress.complete();
81 | } catch (_) {
82 | logger.alert('Running flutter pub get failed');
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/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 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/test/features/authentication/auth_flow_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:{{project_name.snakeCase()}}/services/local_storage/data/shared_preference_repository.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | import '../../robot.dart';
5 |
6 | void main() {
7 | group(
8 | '[AuthFlow]:',
9 | () {
10 | testWidgets(
11 | 'Verifies the sign in and sign out flow of the app by starting on the SignInPage, '
12 | 'and verifying that entering and submitting the correct account and password redirects to the HomePage, '
13 | 'and tapping the sign out button on the HomePage redirects back to the SignInPage',
14 | (tester) async {
15 | await tester.runAsync(
16 | () async {
17 | final r = Robot(tester);
18 | await r.pumpAppWidget();
19 |
20 | // SignInPage should be present on app started
21 | r.authRobot.expectAccountAndPasswordFieldsFound();
22 | await r.authRobot.enterAndSubmitAccountAndPassword();
23 |
24 | // HomePage should be present after sign in
25 | r.authRobot.expectSignOutButtonFound();
26 | await r.authRobot.tapSignOutButton();
27 |
28 | // SignInPage should be present after sign out
29 | r.authRobot.expectAccountAndPasswordFieldsFound();
30 | },
31 | );
32 | },
33 | );
34 |
35 | testWidgets(
36 | "Verifies the sign out flow of the app by starting on the HomePage if the 'stay signed in' value saved in shared preferences is true, "
37 | "and tapping the sign out button on the HomePage redirects back to the SignInPage",
38 | (tester) async {
39 | await tester.runAsync(
40 | () async {
41 | final r = Robot(tester);
42 | await r.pumpAppWidget(
43 | initialSharedPreferenceValues: {
44 | SharedPreferenceRepository.prefStaySignedInKey: true,
45 | },
46 | );
47 | // HomePage should be present on app started if 'stay signed in' value saved in shared preferences is true
48 | // upon successful authentication
49 | r.authRobot.expectSignOutButtonFound();
50 | await r.authRobot.tapSignOutButton();
51 |
52 | // SignInPage should be present after sign out
53 | r.authRobot.expectAccountAndPasswordFieldsFound();
54 | },
55 | );
56 | },
57 | );
58 | },
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/brick/__brick__/{{project_name.snakeCase()}}/lib/services/rest_api_service/data/rest_api_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter_riverpod/flutter_riverpod.dart';
3 | import 'package:{{project_name.snakeCase()}}/services/rest_api_service/rest_api_service.dart';
4 |
5 | final restApiServiceProvider = Provider((ref) {
6 | final client = ref.read(httpClientProvider);
7 | final restApiService = RestApiServiceImpl(ref, httpClient: client);
8 | return restApiService;
9 | });
10 |
11 | /// A service class that makes HTTP REST API requests
12 | /// and returns either an ApiFailure object or a successful result of generic type T
13 | abstract class RestApiService {
14 | /// Sends an HTTP GET request with the given headers to the given [url]
15 | ///
16 | /// {@macro rest_api_request_helper}
17 | Future> get({
18 | required Uri url,
19 | required T Function(String) dataParser,
20 | Map? customHeaders,
21 | TokenType tokenType = TokenType.account,
22 | });
23 |
24 | /// Sends an HTTP POST request with the given [body] and headers to the given [url]
25 | ///
26 | /// {@macro rest_api_request_helper}
27 | Future> post({
28 | required Uri url,
29 | required T Function(String) dataParser,
30 | Map? customHeaders,
31 | TokenType tokenType = TokenType.account,
32 | required Object body,
33 | });
34 |
35 | /// Sends an HTTP PATCH request with the given [body] and headers to the given [url]
36 | ///
37 | /// {@macro rest_api_request_helper}
38 | Future> patch({
39 | required Uri url,
40 | required T Function(String) dataParser,
41 | Map? customHeaders,
42 | TokenType tokenType = TokenType.account,
43 | Object? body,
44 | });
45 |
46 | /// Sends an HTTP PUT request with the given [body] and headers to the given [url]
47 | ///
48 | /// {@macro rest_api_request_helper}
49 | Future> put({
50 | required Uri url,
51 | required T Function(String) dataParser,
52 | Map? customHeaders,
53 | TokenType tokenType = TokenType.account,
54 | Object? body,
55 | });
56 |
57 | /// Sends an HTTP DELETE request with the given headers to the given [url]
58 | ///
59 | /// {@macro rest_api_request_helper}
60 | Future> delete({
61 | required Uri url,
62 | required T Function(String) dataParser,
63 | Map? customHeaders,
64 | TokenType tokenType = TokenType.account,
65 | });
66 | }
67 |
--------------------------------------------------------------------------------