From 401507cd8a485820078396cada580329d812c9f9 Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Fri, 5 Jan 2024 13:26:24 +0600 Subject: [PATCH 01/19] fix: Fixed freezed class typo (#135) * fix: Fixed freezed class typo * chore: Updated deployement target to 12.0 on iOS * nit --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Runner.xcodeproj/project.pbxproj | 29 ++++++++++--------- .../response_objects/error_response.dart | 2 +- .../feature/home/home_screen.dart | 2 +- .../feature/splash/splash_screen.dart | 4 +-- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d2..8c6e561 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile b/ios/Podfile index 88359b2..279576f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 53f1382..12a820b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -80,7 +80,6 @@ F324434539B9030C1AAD3483 /* Pods-Runner.profile-production.xcconfig */, F8CBFEC6A21C8B030120D56D /* Pods-Runner.profile-staging.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -168,7 +167,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -216,8 +215,8 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( @@ -257,6 +256,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -267,6 +267,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -277,7 +278,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -354,7 +355,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -432,7 +433,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -481,7 +482,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -584,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -656,7 +657,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -730,7 +731,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -808,7 +809,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -880,7 +881,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -954,7 +955,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/data/services/response_objects/error_response.dart b/lib/data/services/response_objects/error_response.dart index d5e0a44..c449c67 100644 --- a/lib/data/services/response_objects/error_response.dart +++ b/lib/data/services/response_objects/error_response.dart @@ -34,7 +34,7 @@ class ErrorResponse with _$ErrorResponse { }) = _ErrorResponse; factory ErrorResponse.fromJson(Map json) => - _$$_ErrorResponseFromJson(json); + _$ErrorResponseFromJson(json); } enum ErrorName { diff --git a/lib/presentation/feature/home/home_screen.dart b/lib/presentation/feature/home/home_screen.dart index b528350..003d4d8 100644 --- a/lib/presentation/feature/home/home_screen.dart +++ b/lib/presentation/feature/home/home_screen.dart @@ -23,8 +23,8 @@ class _HomeScreenState extends State { @override void initState() { - _tabSelection = widget.tab; super.initState(); + _tabSelection = widget.tab; } @override diff --git a/lib/presentation/feature/splash/splash_screen.dart b/lib/presentation/feature/splash/splash_screen.dart index 9204c55..3e8d3e2 100644 --- a/lib/presentation/feature/splash/splash_screen.dart +++ b/lib/presentation/feature/splash/splash_screen.dart @@ -18,10 +18,10 @@ class _SplashScreenState extends State { @override void initState() { + super.initState(); _timer = Timer(const Duration(seconds: 2), () { - context.router.navigate(HomeRoute()); + context.router.replace(HomeRoute()); }); - super.initState(); } @override From 739fad05a48da75c1e52a2301c0b0ccc0dca6faf Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Wed, 10 Jan 2024 14:46:05 +0600 Subject: [PATCH 02/19] feat: Added PR Template (#136) --- .github/pull_request_template.md | 32 +++- playbooks/_pictures/ScreenshotsBadExample.png | Bin 0 -> 19156 bytes .../_pictures/ScreenshotsGoodExample.png | Bin 0 -> 37764 bytes .../organization/WorkingWithPullRequests.md | 139 ++++++++++++++++++ 4 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 playbooks/_pictures/ScreenshotsBadExample.png create mode 100644 playbooks/_pictures/ScreenshotsGoodExample.png create mode 100644 playbooks/organization/WorkingWithPullRequests.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e449499..ef510fd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,27 @@ -# Pull Request (PR) Checklist + -- [ ] PR title includes Jira ticket reference -- if no ticket available create one if this PR is a fix or feat -- [ ] Branch name includes Jira ticket reference (parent ticket and sub-task if available), e.g. 'feat/APP-XXX_Some_Feature' or feat/APP-XXX_APP-XXX_Some_Sub_Task or 'fix/APP-XXX_Some_Fix' or 'refactor/Some_Refactoring' -- [ ] All PR checks pass and changes under /domain are covered with unit tests -- [ ] Jira ticket provides testing infos for QA -- add a comment if needed e.q. for pre-requisites, order of API calls, ... -- [ ] When merging this PR the commit message follows [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), e.g. 'feat(some optional epic): add some feature (APP-XXX) (#XXX)' +### General -## Related to other PRs +- [Jira Ticket](#) -## Notes for PR reviewers + + +### Checklists + +- [ ] Platform specific changes (input, image picking etc) are tested on both of the platforms (Android and iOS) +- [ ] It is tested that the UI changes are rendered correctly on different device sizes (such as long lists or expanded rows) with safe area conditions. +- [ ] Edge cases, such as responses being empty or invalid, missing data, no internet connection etc, are tested and the app works as expected. + +### Showcase + +- [ ] Either showcase screenshots / videos are attached, or this PR does not require such any showcase. + + diff --git a/playbooks/_pictures/ScreenshotsBadExample.png b/playbooks/_pictures/ScreenshotsBadExample.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc69c4c64bc089174c7ef0fff75346c70a6e6e3 GIT binary patch literal 19156 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 +``` + +> 💡 Tip of the day! +> +> Consider adding an `img` [text replacement](https://support.apple.com/en-gb/guide/mac-help/mh35735/mac) to your MacOS settings! + +Notice the difference between these two: + +#### First formatting + +Images are very large and cannot fit the normal screen size. In order to view a screenshot, you need to open it in a separate tab or zoom out the screen (just like it's done on the screenshot) + + +

Figure: Bad example of adding the screenshots

+ +#### Second formatting + +Images are not large, even multiple images can be placed in one row next to each other (normal browser zoom). + + +

Figure: Good example of adding the screenshots

From 615558a1f4ac8ab33132c28a150a873b4b50ad35 Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Wed, 10 Jan 2024 18:00:34 +0600 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20Integrated=20monstarlab=20mason?= =?UTF-8?q?=20bricks=20=F0=9F=A7=B1=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++ .mason/bricks.json | 1 - android/build.gradle | 2 +- bricks/README.md | 26 -------------- bricks/future_output_usecase/CHANGELOG.md | 3 -- bricks/future_output_usecase/LICENSE | 1 - bricks/future_output_usecase/README.md | 24 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/future_output_usecase/brick.yaml | 30 ---------------- bricks/future_usecase/CHANGELOG.md | 3 -- bricks/future_usecase/LICENSE | 1 - bricks/future_usecase/README.md | 25 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/future_usecase/brick.yaml | 36 ------------------- bricks/output_usecase/CHANGELOG.md | 3 -- bricks/output_usecase/LICENSE | 1 - bricks/output_usecase/README.md | 24 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/output_usecase/brick.yaml | 30 ---------------- bricks/stream_output_usecase/CHANGELOG.md | 3 -- bricks/stream_output_usecase/LICENSE | 1 - bricks/stream_output_usecase/README.md | 24 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/stream_output_usecase/brick.yaml | 30 ---------------- bricks/stream_usecase/CHANGELOG.md | 3 -- bricks/stream_usecase/LICENSE | 1 - bricks/stream_usecase/README.md | 25 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/stream_usecase/brick.yaml | 35 ------------------ bricks/usecase/CHANGELOG.md | 3 -- bricks/usecase/LICENSE | 1 - bricks/usecase/README.md | 25 ------------- .../{{name.snakeCase()}}_usecase.dart | 6 ---- bricks/usecase/brick.yaml | 35 ------------------ devtools_options.yaml | 1 + mason-lock.json | 1 - mason.yaml | 20 +++++++++++ pubspec.yaml | 36 +++++++++---------- 38 files changed, 43 insertions(+), 451 deletions(-) delete mode 100644 .mason/bricks.json delete mode 100644 bricks/README.md delete mode 100644 bricks/future_output_usecase/CHANGELOG.md delete mode 100644 bricks/future_output_usecase/LICENSE delete mode 100644 bricks/future_output_usecase/README.md delete mode 100644 bricks/future_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/future_output_usecase/brick.yaml delete mode 100644 bricks/future_usecase/CHANGELOG.md delete mode 100644 bricks/future_usecase/LICENSE delete mode 100644 bricks/future_usecase/README.md delete mode 100644 bricks/future_usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/future_usecase/brick.yaml delete mode 100644 bricks/output_usecase/CHANGELOG.md delete mode 100644 bricks/output_usecase/LICENSE delete mode 100644 bricks/output_usecase/README.md delete mode 100644 bricks/output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/output_usecase/brick.yaml delete mode 100644 bricks/stream_output_usecase/CHANGELOG.md delete mode 100644 bricks/stream_output_usecase/LICENSE delete mode 100644 bricks/stream_output_usecase/README.md delete mode 100644 bricks/stream_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/stream_output_usecase/brick.yaml delete mode 100644 bricks/stream_usecase/CHANGELOG.md delete mode 100644 bricks/stream_usecase/LICENSE delete mode 100644 bricks/stream_usecase/README.md delete mode 100644 bricks/stream_usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/stream_usecase/brick.yaml delete mode 100644 bricks/usecase/CHANGELOG.md delete mode 100644 bricks/usecase/LICENSE delete mode 100644 bricks/usecase/README.md delete mode 100644 bricks/usecase/__brick__/{{name.snakeCase()}}_usecase.dart delete mode 100644 bricks/usecase/brick.yaml create mode 100644 devtools_options.yaml delete mode 100644 mason-lock.json diff --git a/.gitignore b/.gitignore index 91264a1..d8d1126 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ .history .svn/ +# Local Mason Files +.mason/ +mason-lock.json + # IntelliJ related *.iml *.ipr diff --git a/.mason/bricks.json b/.mason/bricks.json deleted file mode 100644 index 9e26dfe..0000000 --- a/.mason/bricks.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 166e389..0496058 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/bricks/README.md b/bricks/README.md deleted file mode 100644 index 44e2ef1..0000000 --- a/bricks/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Monstar Bricks 🧱 - -A collection of bricks. - -## Installation - -Ensure you have the [mason_cli](https://github.com/felangel/mason/tree/master/packages/mason_cli) installed. Bricks can be installed from [brickhub.dev](https://brickhub.dev). - -> brickhub is currently in closed alpha testing. - -```sh -mason add [--path ] -``` - -## Bricks - -| Brick | Description | -| ----------------------- | ------------------------------------ | -| `bloc` | Generate a new Bloc | -| `cubit` | Generate a new Cubit | -| `usecase` | Generate a new usecase | -| `output_usecase` | Generate a new output usecase | -| `stream_usecase` | Generate a new stream usecase | -| `stream_output_usecase` | Generate a new stream output usecase | -| `future_usecase` | Generate a new future usecase | -| `future_output_usecase` | Generate a new future output usecase | diff --git a/bricks/future_output_usecase/CHANGELOG.md b/bricks/future_output_usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/future_output_usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/future_output_usecase/LICENSE b/bricks/future_output_usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/future_output_usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/future_output_usecase/README.md b/bricks/future_output_usecase/README.md deleted file mode 100644 index c4a1cc4..0000000 --- a/bricks/future_output_usecase/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# usecase - -Generate a new FutureOutputUseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make future_output_usecase --name GetStories --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/future_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/future_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index 305c007..0000000 --- a/bricks/future_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends FutureOutputUseCase<{{output_type.pascalCase()}}> { - @override - Future<{{output_type.pascalCase()}}> run() { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/future_output_usecase/brick.yaml b/bricks/future_output_usecase/brick.yaml deleted file mode 100644 index 1d7bd6e..0000000 --- a/bricks/future_output_usecase/brick.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: future_output_usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? diff --git a/bricks/future_usecase/CHANGELOG.md b/bricks/future_usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/future_usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/future_usecase/LICENSE b/bricks/future_usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/future_usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/future_usecase/README.md b/bricks/future_usecase/README.md deleted file mode 100644 index 1f2df34..0000000 --- a/bricks/future_usecase/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# usecase - -Generate a new FutureUseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make future_usecase --name GetStories --input_type Query --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `input_type` | The input type of the usecase | `String` | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/future_usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/future_usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index 3f75894..0000000 --- a/bricks/future_usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends FutureUseCase<{{input_type.pascalCase()}}, {{output_type.pascalCase()}}> { - @override - Future<{{output_type.pascalCase()}}> run({{input_type.pascalCase()}} input) { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/future_usecase/brick.yaml b/bricks/future_usecase/brick.yaml deleted file mode 100644 index f1dbb13..0000000 --- a/bricks/future_usecase/brick.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: future_usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - input_type: - type: string - description: The use case input type (example String) - default: String - prompt: What is the input type for the use case? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? - diff --git a/bricks/output_usecase/CHANGELOG.md b/bricks/output_usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/output_usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/output_usecase/LICENSE b/bricks/output_usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/output_usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/output_usecase/README.md b/bricks/output_usecase/README.md deleted file mode 100644 index 798d7fc..0000000 --- a/bricks/output_usecase/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# usecase - -Generate a new OutputUseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make output_usecase --name GetStories --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index 22f148a..0000000 --- a/bricks/output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends OutputUseCase<{{output_type.pascalCase()}}> { - @override - {{output_type.pascalCase()}} run() { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/output_usecase/brick.yaml b/bricks/output_usecase/brick.yaml deleted file mode 100644 index 3a88536..0000000 --- a/bricks/output_usecase/brick.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: output_usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? diff --git a/bricks/stream_output_usecase/CHANGELOG.md b/bricks/stream_output_usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/stream_output_usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/stream_output_usecase/LICENSE b/bricks/stream_output_usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/stream_output_usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/stream_output_usecase/README.md b/bricks/stream_output_usecase/README.md deleted file mode 100644 index 7db6bf0..0000000 --- a/bricks/stream_output_usecase/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# usecase - -Generate a new StreamOutputUseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make stream_output_usecase --name GetStories --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/stream_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/stream_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index af655e1..0000000 --- a/bricks/stream_output_usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends StreamOutputUseCase<{{output_type.pascalCase()}}> { - @override - Stream<{{output_type.pascalCase()}}> run() { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/stream_output_usecase/brick.yaml b/bricks/stream_output_usecase/brick.yaml deleted file mode 100644 index 1984650..0000000 --- a/bricks/stream_output_usecase/brick.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: stream_output_usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? diff --git a/bricks/stream_usecase/CHANGELOG.md b/bricks/stream_usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/stream_usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/stream_usecase/LICENSE b/bricks/stream_usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/stream_usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/stream_usecase/README.md b/bricks/stream_usecase/README.md deleted file mode 100644 index e33947e..0000000 --- a/bricks/stream_usecase/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# usecase - -Generate a new StreamUseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make stream_usecase --name GetStories --input_type Query --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `input_type` | The input type of the usecase | `String` | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/stream_usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/stream_usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index 4e0a3d6..0000000 --- a/bricks/stream_usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends StreamUseCase<{{input_type.pascalCase()}}, {{output_type.pascalCase()}}> { - @override - Stream<{{output_type.pascalCase()}}> run({{input_type.pascalCase()}} input) { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/stream_usecase/brick.yaml b/bricks/stream_usecase/brick.yaml deleted file mode 100644 index a7e53cc..0000000 --- a/bricks/stream_usecase/brick.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: stream_usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - input_type: - type: string - description: The use case input type (example String) - default: String - prompt: What is the input type for the use case? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? diff --git a/bricks/usecase/CHANGELOG.md b/bricks/usecase/CHANGELOG.md deleted file mode 100644 index f0640d6..0000000 --- a/bricks/usecase/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -# 0.1.0+1 - -- TODO: Describe initial release. diff --git a/bricks/usecase/LICENSE b/bricks/usecase/LICENSE deleted file mode 100644 index ba75c69..0000000 --- a/bricks/usecase/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/bricks/usecase/README.md b/bricks/usecase/README.md deleted file mode 100644 index 3153d52..0000000 --- a/bricks/usecase/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# usecase - -Generate a new UseCase in [Dart][1]. - -## Usage 🚀 - -```sh -mason make usecase --name GetStories --input_type Query --output_type List -``` - -## Variables ✨ - -| Variable | Description | Default | Type | -| ------------- | ------------------------------ | -------- | -------- | -| `name` | The name of the usecase class | None | `string` | -| `input_type` | The input type of the usecase | `String` | `string` | -| `output_type` | The output type of the usecase | `String` | `string` | - -## Output 📦 - -```sh -└── get_stories_usecase.dart -``` - -[1]: https://dart.dev diff --git a/bricks/usecase/__brick__/{{name.snakeCase()}}_usecase.dart b/bricks/usecase/__brick__/{{name.snakeCase()}}_usecase.dart deleted file mode 100644 index 88431a4..0000000 --- a/bricks/usecase/__brick__/{{name.snakeCase()}}_usecase.dart +++ /dev/null @@ -1,6 +0,0 @@ -class {{name.pascalCase()}}UseCase extends UseCase<{{input_type.pascalCase()}}, {{output_type.pascalCase()}}> { - @override - {{output_type.pascalCase()}} run({{input_type.pascalCase()}} input) { - // TODO: implement usecase - } -} \ No newline at end of file diff --git a/bricks/usecase/brick.yaml b/bricks/usecase/brick.yaml deleted file mode 100644 index dece5c1..0000000 --- a/bricks/usecase/brick.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: usecase -description: A new brick created with the Mason CLI. - -# The following defines the version and build number for your brick. -# A version number is three numbers separated by dots, like 1.2.34 -# followed by an optional build number (separated by a +). -version: 0.1.0+1 - -# The following defines the environment for the current brick. -# It includes the version of mason that the brick requires. -environment: - mason: ">=0.1.0-dev <0.1.0" - -# Variables specify dynamic values that your brick depends on. -# Zero or more variables can be specified for a given brick. -# Each variable has: -# * a type (string, number, or boolean) -# * an optional short description -# * an optional default value -# * an optional prompt phrase used when asking for the variable. -vars: - name: - type: string - description: Use case name - prompt: What is the use case name? - input_type: - type: string - description: The use case input type (example String) - default: String - prompt: What is the input type for the use case? - output_type: - type: string - description: The use case output type (example String) - default: String - prompt: What is the output type for the use case? diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/mason-lock.json b/mason-lock.json deleted file mode 100644 index eab10ae..0000000 --- a/mason-lock.json +++ /dev/null @@ -1 +0,0 @@ -{"bricks":{}} \ No newline at end of file diff --git a/mason.yaml b/mason.yaml index 4f00993..83977aa 100644 --- a/mason.yaml +++ b/mason.yaml @@ -1 +1,21 @@ +# Register bricks which can be consumed via the Mason CLI. +# Run "mason get" to install all registered bricks. +# To learn more, visit https://docs.brickhub.dev. bricks: + # service brick + service: + git: + url: "https://github.com/monstar-lab-oss/flutter-bricks.git" + path: bricks/service + + # usecase brick + usecase: + git: + url: "https://github.com/monstar-lab-oss/flutter-bricks.git" + path: bricks/usecase + + # feature brick + feature: + git: + url: "https://github.com/monstar-lab-oss/flutter-bricks.git" + path: bricks/feature diff --git a/pubspec.yaml b/pubspec.yaml index 2841c34..217809d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,45 +25,43 @@ dependencies: sdk: flutter # UI - cupertino_icons: ^1.0.4 - flutter_svg: ^2.0.7 + cupertino_icons: ^1.0.6 + flutter_svg: ^2.0.9 nstack: git: url: https://github.com/nstack-io/flutter-sdk.git ref: v0.5.1 # Net - dio: ^5.1.2 - jaguar_jwt: ^3.0.0 + dio: ^5.4.0 # Utility - device_info_plus: ^9.0.0 - package_info_plus: ^4.0.0 + device_info_plus: ^9.1.1 + package_info_plus: ^5.0.1 # Persistence - shared_preferences: ^2.1.1 + shared_preferences: ^2.2.2 # Architecture - freezed_annotation: ^2.2.0 + freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 - injectable: ^2.1.1 - get_it: ^7.6.0 - auto_route: ^7.1.0 - flutter_bloc: ^8.1.2 - rxdart: ^0.27.7 + injectable: ^2.3.2 + get_it: ^7.6.6 + auto_route: ^7.8.4 + flutter_bloc: ^8.1.3 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.6 - injectable_generator: ^2.1.5 - freezed: ^2.3.3 - json_serializable: ^6.6.2 - auto_route_generator: ^7.0.0 + build_runner: ^2.4.7 + injectable_generator: ^2.4.1 + freezed: ^2.4.6 + json_serializable: ^6.7.1 + auto_route_generator: ^7.3.2 flutter_lints: ^2.0.1 # Simplifed work with assets - flutter_gen_runner: ^5.3.1 + flutter_gen_runner: ^5.4.0 flutter_gen: integrations: From c6fad203cc6b17c4a222bc1450514497e77a0114 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Mon, 15 Jan 2024 15:14:52 +0300 Subject: [PATCH 04/19] fix: Use monstarlab_lints : : (#133) * fix: Use monstarlab_lints : : * fix: Configure lints dependency correctly * nit: FIx warnings --- analysis_options.yaml | 68 +------------------ lib/data/interceptor/auth_interceptor.dart | 2 +- .../response_objects/tokens_response.dart | 2 +- lib/injection/dependencies.dart | 3 +- lib/main_common.dart | 3 +- lib/main_development.dart | 4 +- lib/main_production.dart | 4 +- lib/main_staging.dart | 4 +- lib/presentation/app.dart | 2 +- .../feature/profile/profile_page.dart | 17 ++--- .../not_implemented_dialog.dart | 2 +- pubspec.yaml | 2 +- 12 files changed, 24 insertions(+), 89 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 149ffc4..8d7c335 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -5,78 +5,14 @@ # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - rules: - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - - always_declare_return_types - - always_require_non_null_named_parameters - - annotate_overrides - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_print - - avoid_relative_lib_imports - - avoid_return_types_on_setters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_types_as_parameter_names - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - - await_only_futures - - camel_case_extensions - - curly_braces_in_flow_control_structures - - empty_catches - - empty_constructor_bodies - - library_names - - library_prefixes - - no_duplicate_case_values - - null_closures - - omit_local_variable_types - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_contains - - prefer_final_fields - - prefer_for_elements_to_map_fromIterable - - prefer_generic_function_type_aliases - - prefer_if_null_operators - - prefer_inlined_adds - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - - prefer_single_quotes - - prefer_spread_collections - - recursive_getters - - slash_for_doc_comments - - sort_child_properties_last - - type_init_formals - - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_new - - unnecessary_null_in_if_null_operators - - unnecessary_this - - unrelated_type_equality_checks - - unsafe_html - - use_colored_box - - use_decorated_box - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_rethrow_when_possible - - valid_regexps +include: package:monstarlab_lints/analysis_options.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options - analyzer: errors: - # Ignore invalid annotations since freezed annotation and json serializable required it + # Ignore invalid annotations since freezed annotation and json serializable required it invalid_annotation_target: ignore exclude: # ignore DI diff --git a/lib/data/interceptor/auth_interceptor.dart b/lib/data/interceptor/auth_interceptor.dart index 88d4263..5e6e46f 100644 --- a/lib/data/interceptor/auth_interceptor.dart +++ b/lib/data/interceptor/auth_interceptor.dart @@ -116,7 +116,7 @@ class AuthInterceptor extends InterceptorsWrapper { headers: { 'Authorization': 'Bearer ${authPreferences.refreshToken}', MetaInterceptor.nMetaHeaderKey: - requestOptions.headers[MetaInterceptor.nMetaHeaderKey] + requestOptions.headers[MetaInterceptor.nMetaHeaderKey], }, ); diff --git a/lib/data/services/response_objects/tokens_response.dart b/lib/data/services/response_objects/tokens_response.dart index a08f9e0..b526276 100644 --- a/lib/data/services/response_objects/tokens_response.dart +++ b/lib/data/services/response_objects/tokens_response.dart @@ -22,7 +22,7 @@ class TokensResponse with _$TokensResponse { 'accessToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwY2MiO', 'refreshToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwY2Mi', 'tokenType': 'Bearer', - 'expiresIn': 2592000 + 'expiresIn': 2592000, }); } diff --git a/lib/injection/dependencies.dart b/lib/injection/dependencies.dart index 47fe71f..b8e045c 100644 --- a/lib/injection/dependencies.dart +++ b/lib/injection/dependencies.dart @@ -1,8 +1,7 @@ +import 'package:flutter_template/injection/injector.dart'; import 'package:flutter_template/presentation/app_flavor.dart'; import 'package:flutter_template/presentation/routes/router.dart'; -import 'injector.dart'; - class DependencyManager { static Future inject(AppFlavor flavor) async { injector.registerLazySingleton(() => flavor); diff --git a/lib/main_common.dart b/lib/main_common.dart index 7b3da26..5ede696 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_template/injection/dependencies.dart'; import 'package:flutter_template/presentation/app.dart'; - -import 'presentation/app_flavor.dart'; +import 'package:flutter_template/presentation/app_flavor.dart'; // ignore: avoid_void_async void mainCommon(AppFlavor flavor) async { diff --git a/lib/main_development.dart b/lib/main_development.dart index 9b61cea..f756cf6 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,5 +1,5 @@ -import 'presentation/app_flavor.dart'; -import 'main_common.dart'; +import 'package:flutter_template/main_common.dart'; +import 'package:flutter_template/presentation/app_flavor.dart'; void main() async { const config = AppFlavor.development; diff --git a/lib/main_production.dart b/lib/main_production.dart index b77f0d5..e2691e8 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,5 +1,5 @@ -import 'presentation/app_flavor.dart'; -import 'main_common.dart'; +import 'package:flutter_template/main_common.dart'; +import 'package:flutter_template/presentation/app_flavor.dart'; void main() async { const config = AppFlavor.production; diff --git a/lib/main_staging.dart b/lib/main_staging.dart index d934f26..8571870 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,5 +1,5 @@ -import 'presentation/app_flavor.dart'; -import 'main_common.dart'; +import 'package:flutter_template/main_common.dart'; +import 'package:flutter_template/presentation/app_flavor.dart'; void main() async { const config = AppFlavor.staging; diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index a494429..a2a587d 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_template/injection/injector.dart'; +import 'package:flutter_template/nstack/nstack.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; import 'package:flutter_template/presentation/routes/router.dart'; -import '../../nstack/nstack.dart'; class App extends StatelessWidget { const App({Key? key}) : super(key: key); diff --git a/lib/presentation/feature/profile/profile_page.dart b/lib/presentation/feature/profile/profile_page.dart index 99cc6ec..45edaa1 100644 --- a/lib/presentation/feature/profile/profile_page.dart +++ b/lib/presentation/feature/profile/profile_page.dart @@ -41,14 +41,15 @@ class _ProfilePageState extends State { child: Column( children: [ BlocBuilder( - bloc: _profilePresenter, - builder: (context, state) { - if (state.isLoading) { - return const CircularProgressIndicator(); - } else { - return Text('Hi ${state.name}!'); - } - }) + bloc: _profilePresenter, + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator(); + } else { + return Text('Hi ${state.name}!'); + } + }, + ), ], ), ), diff --git a/lib/presentation/utils/not_implemented_dialog/not_implemented_dialog.dart b/lib/presentation/utils/not_implemented_dialog/not_implemented_dialog.dart index 7bab9cb..b59df35 100644 --- a/lib/presentation/utils/not_implemented_dialog/not_implemented_dialog.dart +++ b/lib/presentation/utils/not_implemented_dialog/not_implemented_dialog.dart @@ -29,7 +29,7 @@ class NotImplementedDialog extends StatelessWidget { TextButton( onPressed: Navigator.of(context).pop, child: const Text('OK'), - ) + ), ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 217809d..dda55f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,7 @@ dev_dependencies: freezed: ^2.4.6 json_serializable: ^6.7.1 auto_route_generator: ^7.3.2 - flutter_lints: ^2.0.1 + monstarlab_lints: ^1.0.2 # Simplifed work with assets flutter_gen_runner: ^5.4.0 From 748e2b19a361405b438c2b6eddc1d65643ae3124 Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Mon, 15 Jan 2024 19:15:16 +0600 Subject: [PATCH 05/19] feat: Introduced auto tab scaffold (#138) * feat: Introduced auto tab scaffold * feat: Added class modifier to cubit * feat: Added build.yaml for build runner * nit * feat: Added strokeWidth and empty playground screen * nit * nit --- build.yaml | 68 +++++++ lib/data/interceptor/auth_interceptor.dart | 4 +- .../response_objects/error_response.dart | 2 +- .../response_error.dart | 4 +- .../response_objects/tokens_response.dart | 0 .../services/http_client/dio_http_client.dart | 2 +- lib/data/services/http_profile_service.dart | 14 -- .../profile/profile_service_impl.dart | 12 ++ .../common/base_status/base_status.dart | 31 ++++ lib/domain/common/use_case.dart | 173 ------------------ .../services/profile/profile_service.dart | 3 + lib/domain/services/profile_service.dart | 3 - .../profile/get_profile_use_case.dart | 13 ++ lib/extensions/future_extensions.dart | 2 +- lib/presentation/app.dart | 1 + .../feature/home/home_screen.dart | 73 -------- .../feature/home/home_screen_tab.dart | 4 - .../feature/profile/profile_cubit.dart | 37 ---- .../feature/profile/profile_page.dart | 60 ------ .../_playground/playground_screen.dart | 44 +++++ lib/presentation/features/home/home_page.dart | 21 +++ .../features/home/ui/home_body.dart | 10 + .../features/main/main_screen.dart | 32 ++++ .../main/ui/app_bottom_navigation_bar.dart | 25 +++ .../{feature => features}/news/news_page.dart | 4 + .../features/news/ui/news_body.dart | 13 ++ .../features/profile/cubit/profile_cubit.dart | 43 +++++ .../profile/cubit}/profile_state.dart | 10 +- .../features/profile/profile_page.dart | 27 +++ .../features/profile/ui/profile_body.dart | 58 ++++++ .../splash/splash_screen.dart | 2 +- lib/presentation/routes/router.dart | 32 +++- .../app_loading_indicator.dart | 39 ++++ 33 files changed, 486 insertions(+), 380 deletions(-) create mode 100644 build.yaml rename lib/data/{services => }/response_objects/error_response.dart (96%) rename lib/data/{services => response_objects}/response_error.dart (97%) rename lib/data/{services => }/response_objects/tokens_response.dart (100%) delete mode 100644 lib/data/services/http_profile_service.dart create mode 100644 lib/data/services/profile/profile_service_impl.dart create mode 100644 lib/domain/common/base_status/base_status.dart delete mode 100644 lib/domain/common/use_case.dart create mode 100644 lib/domain/services/profile/profile_service.dart delete mode 100644 lib/domain/services/profile_service.dart create mode 100644 lib/domain/use_cases/profile/get_profile_use_case.dart delete mode 100644 lib/presentation/feature/home/home_screen.dart delete mode 100644 lib/presentation/feature/home/home_screen_tab.dart delete mode 100644 lib/presentation/feature/profile/profile_cubit.dart delete mode 100644 lib/presentation/feature/profile/profile_page.dart create mode 100644 lib/presentation/features/_playground/playground_screen.dart create mode 100644 lib/presentation/features/home/home_page.dart create mode 100644 lib/presentation/features/home/ui/home_body.dart create mode 100644 lib/presentation/features/main/main_screen.dart create mode 100644 lib/presentation/features/main/ui/app_bottom_navigation_bar.dart rename lib/presentation/{feature => features}/news/news_page.dart (72%) create mode 100644 lib/presentation/features/news/ui/news_body.dart create mode 100644 lib/presentation/features/profile/cubit/profile_cubit.dart rename lib/presentation/{feature/profile => features/profile/cubit}/profile_state.dart (56%) create mode 100644 lib/presentation/features/profile/profile_page.dart create mode 100644 lib/presentation/features/profile/ui/profile_body.dart rename lib/presentation/{feature => features}/splash/splash_screen.dart (95%) create mode 100644 lib/presentation/widgets/loading_indicator/app_loading_indicator.dart diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..62689e4 --- /dev/null +++ b/build.yaml @@ -0,0 +1,68 @@ +targets: + $default: + builders: + # Serializer + json_serializable: + options: + explicit_to_json: true + # By deafult, field_rename: none, More options — + # snake, kebab, pascal, etc. To use, uncomment this below line — + # field_rename: snake + generate_for: + include: + # data layer: + - lib/data/model/**.dart + - lib/data/response_objects/**.dart + + # domain layer: + # Note: We added domain layer? Why? Because sometimes, we may need + # some freezed classes to be saved on storage where toJson() is required. + # We should not allow the entire `/entities` folder here, as entities don't need fromJson(), toJson(), etc. + # Specifying exact file will help build runner to run fast. + - lib/domain/entities/user.dart + + # Data Classes, Cloning + freezed:freezed: + generate_for: + include: + # data layer: + - lib/data/model/**.dart + - lib/data/response_objects/**.dart + + # domain layer: + - lib/domain/common/**.dart + - lib/domain/entities/**.dart + + # presentation layer: + - lib/presentation/features/**_state.dart + + # Dependency Injection + injectable_generator:injectable_config_builder: + generate_for: + include: + - lib/injection/injector.dart + + injectable_generator:injectable_builder: + generate_for: + include: + - lib/injection/injector.dart + + # data + - lib/data/**_config.dart + - lib/data/services/**_service_impl.dart + - lib/data/services/**_remapper.dart + - lib/data/preferences/**_preferences.dart + + # domain + - lib/domain/use_cases/**_use_case.dart + + # presentation + - lib/presentation/features/**_cubit.dart + + # Routing + auto_route_generator: + generate_for: + include: + - lib/presentation/routes/router.dart + - lib/presentation/features/**_screen.dart + - lib/presentation/features/**_page.dart diff --git a/lib/data/interceptor/auth_interceptor.dart b/lib/data/interceptor/auth_interceptor.dart index 5e6e46f..8b63278 100644 --- a/lib/data/interceptor/auth_interceptor.dart +++ b/lib/data/interceptor/auth_interceptor.dart @@ -3,10 +3,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_template/data/interceptor/meta_interceptor.dart'; import 'package:flutter_template/data/model/auth/auth_tokens.dart'; import 'package:flutter_template/data/preferences/auth_preferences.dart'; +import 'package:flutter_template/data/response_objects/response_error.dart'; import 'package:flutter_template/data/services/http_client/dio_http_client.dart'; import 'package:flutter_template/data/services/http_client/http_client.dart'; -import 'package:flutter_template/data/services/response_error.dart'; -import 'package:flutter_template/data/services/response_objects/tokens_response.dart'; +import 'package:flutter_template/data/response_objects/tokens_response.dart'; import 'package:flutter_template/domain/preferences/user_preferences.dart'; class AuthInterceptor extends InterceptorsWrapper { diff --git a/lib/data/services/response_objects/error_response.dart b/lib/data/response_objects/error_response.dart similarity index 96% rename from lib/data/services/response_objects/error_response.dart rename to lib/data/response_objects/error_response.dart index c449c67..4a64676 100644 --- a/lib/data/services/response_objects/error_response.dart +++ b/lib/data/response_objects/error_response.dart @@ -1,4 +1,4 @@ -import 'package:flutter_template/data/services/response_error.dart'; +import 'package:flutter_template/data/response_objects/response_error.dart'; import 'package:flutter_template/nstack/nstack.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/lib/data/services/response_error.dart b/lib/data/response_objects/response_error.dart similarity index 97% rename from lib/data/services/response_error.dart rename to lib/data/response_objects/response_error.dart index 2299593..81ace3b 100644 --- a/lib/data/services/response_error.dart +++ b/lib/data/response_objects/response_error.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_template/data/services/response_objects/error_response.dart'; +import 'package:flutter_template/data/response_objects/error_response.dart'; import 'package:flutter_template/nstack/nstack.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -13,7 +13,7 @@ part 'response_error.freezed.dart'; /// /// We return those errors to get localized messages to display to the user. @freezed -class ResponseError with _$ResponseError implements Exception { +sealed class ResponseError with _$ResponseError implements Exception { const ResponseError._(); const factory ResponseError.noInternetConnection() = _NoInternetConnection; diff --git a/lib/data/services/response_objects/tokens_response.dart b/lib/data/response_objects/tokens_response.dart similarity index 100% rename from lib/data/services/response_objects/tokens_response.dart rename to lib/data/response_objects/tokens_response.dart diff --git a/lib/data/services/http_client/dio_http_client.dart b/lib/data/services/http_client/dio_http_client.dart index acbd98e..2d11633 100644 --- a/lib/data/services/http_client/dio_http_client.dart +++ b/lib/data/services/http_client/dio_http_client.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; +import 'package:flutter_template/data/response_objects/response_error.dart'; import 'package:flutter_template/data/services/http_client/http_client.dart'; -import 'package:flutter_template/data/services/response_error.dart'; import 'package:injectable/injectable.dart'; /// Abstraction of the Dio http client class. diff --git a/lib/data/services/http_profile_service.dart b/lib/data/services/http_profile_service.dart deleted file mode 100644 index 53447b4..0000000 --- a/lib/data/services/http_profile_service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_template/domain/services/profile_service.dart'; -import 'package:injectable/injectable.dart'; - -@LazySingleton(as: ProfileService) -class HttpProfileService extends ProfileService { - HttpProfileService(); - - @override - Future getProfileName() async { - // TODO: implement getProfileName - await Future.delayed(const Duration(milliseconds: 800)); - return 'Michael Laudrup'; - } -} diff --git a/lib/data/services/profile/profile_service_impl.dart b/lib/data/services/profile/profile_service_impl.dart new file mode 100644 index 0000000..b7e3f04 --- /dev/null +++ b/lib/data/services/profile/profile_service_impl.dart @@ -0,0 +1,12 @@ +import 'package:flutter_template/domain/services/profile/profile_service.dart'; +import 'package:injectable/injectable.dart'; + +@Injectable(as: ProfileService) +class ProfileServiceImpl implements ProfileService { + @override + Future getProfileName() async { + // TODO: implement getProfileName + await Future.delayed(const Duration(milliseconds: 800)); + return 'John Doe'; + } +} diff --git a/lib/domain/common/base_status/base_status.dart b/lib/domain/common/base_status/base_status.dart new file mode 100644 index 0000000..c996cff --- /dev/null +++ b/lib/domain/common/base_status/base_status.dart @@ -0,0 +1,31 @@ +import 'package:flutter_template/data/response_objects/response_error.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'base_status.freezed.dart'; + +@freezed +sealed class BaseStatus with _$BaseStatus { + const BaseStatus._(); + + const factory BaseStatus.initial() = Initial; + + const factory BaseStatus.loading() = Loading; + + const factory BaseStatus.lazyLoading() = LazyLoading; + + const factory BaseStatus.success() = Success; + + const factory BaseStatus.valid() = Valid; + + const factory BaseStatus.invalid() = Invalid; + + const factory BaseStatus.failure(ResponseError error) = Failure; + + bool get isInitial => this is Initial; + bool get isLoading => this is Loading; + bool get isLazyLoading => this is LazyLoading; + bool get isSuccess => this is Success; + bool get isValid => this is Valid; + bool get isInvalid => this is Invalid; + bool get isFailure => this is Failure; +} diff --git a/lib/domain/common/use_case.dart b/lib/domain/common/use_case.dart deleted file mode 100644 index 785c33c..0000000 --- a/lib/domain/common/use_case.dart +++ /dev/null @@ -1,173 +0,0 @@ -/// Future output use case that would take in an [Input] and return [Output]. -/// -/// Used when we need to consider any inputs like filtering parameters etc. -/// -/// Example: -/// // import tuple -/// typedef Input = Tuple, Query>; -/// -/// ```dart -/// class QueryStoriesUseCase extends UseCase> { -/// @override -/// List run(Input input) { -/// final stories = input.item1; -/// final query = input.item2; -/// return stories.where((story) => story.name.contains(query.name)); -/// } -/// } -/// -/// class Query { -/// final int age; -/// final String name; -/// final String author; -/// -/// Query(this.age, this.name, this.author); -/// } -/// ``` -/// -abstract class UseCase { - Output run(Input input); -} - -/// Output use case that would return [Output]. -/// -/// Used when we don't need to consider any inputs like filtering parameters etc. -/// -/// Example: -/// -/// ```dart -/// class AgeValidationUseCase extends OutputUseCase { -/// AgeValidationUseCase({ -/// required this._user, -/// }); -/// -/// final User _user; -/// -/// @override -/// bool run() { -/// return _user.age >= 18; -/// } -/// } -/// ``` -/// -abstract class OutputUseCase { - Output run(); -} - -/// Stream use case that would return [Output] as a [Stream]. -/// -/// Used when we want to get an async sequence of data. -/// -/// -/// Example: -/// -/// ```dart -/// class ObserveStoryPlayerStateUseCase extends StreamOutputUseCase { -/// ObserveStoryPlayerStateUseCase({ -/// required this._storyPlayerPreferences, -/// }); -/// -/// final StoryPlayerPreferences _storyPlayerPreferences; -/// -/// @override -/// Stream run() { -/// return _storyPlayerPreferences -/// .getStoryPlayerStateStream() -/// .where((storyPlayerState) => storyPlayerState.hasStoryForPlayback); -/// } -/// } -/// ``` -/// -abstract class StreamOutputUseCase - extends OutputUseCase> {} - -/// Stream output use case that would take in an [Input] and return [Output] as a [Stream]. -/// -/// Used when we want to get an async sequence of data. -/// -/// -/// Example: -/// -/// ```dart -/// class ObserveStoryPlayerStateFromStoryIdUseCase extends StreamUseCase { -/// ObserveStoryPlayerStateFromStoryIdUseCase( -/// this._storyPlayerPreferences, -/// ); -/// -/// final StoryPlayerPreferences _storyPlayerPreferences; -/// -/// @override -/// Stream run(Query input) { -/// return _storyPlayerPreferences -/// .getStoryPlayerStateStreamFromStoryID(input.storyID) -/// .where((storyPlayerState) => storyPlayerState.hasStoryForPlayback); -/// } -/// } -/// -/// class Query { -/// final String name; -/// final String author; -/// final int storyID; -/// -/// Query(this.name, this.author, this.storyID); -/// } -/// ``` -/// -abstract class StreamUseCase - extends UseCase> {} - -/// Future output use case that would take in an [Input] and return [Output] as a [Future]. -/// -/// Used when we need to consider any inputs like filtering parameters etc. -/// -/// Example: -/// -/// ```dart -/// class QueryStoriesUseCase extends FutureUseCase> { -/// QueryStoriesUseCase( -/// this._storiesService, -/// ); -/// -/// final StoriesService _storiesService; -/// -/// @override -/// Future> run(Query input) async { -/// return _storiesService.queryStories(input); -/// } -/// } -/// -/// class Query { -/// final int age; -/// final String name; -/// final String author; -/// -/// Query(this.age, this.name, this.author); -/// } -/// ``` -/// -abstract class FutureUseCase - extends UseCase> {} - -/// Future output use case that would return [Output] as a [Future]. -/// -/// Used when we don't need to consider any inputs like filtering parameters etc. -/// -/// Example: -/// -/// ```dart -/// class GetAllStoriesUseCase extends FutureOutputUseCase> { -/// GetAllStoriesUseCase( -/// this._storiesService, -/// ); -/// -/// final StoriesService _storiesService; -/// -/// @override -/// Future> run() async { -/// return _storiesService.fetchAllStories(); -/// } -/// } -/// ``` -/// -abstract class FutureOutputUseCase - extends OutputUseCase> {} diff --git a/lib/domain/services/profile/profile_service.dart b/lib/domain/services/profile/profile_service.dart new file mode 100644 index 0000000..9997d57 --- /dev/null +++ b/lib/domain/services/profile/profile_service.dart @@ -0,0 +1,3 @@ +abstract interface class ProfileService { + Future getProfileName(); +} diff --git a/lib/domain/services/profile_service.dart b/lib/domain/services/profile_service.dart deleted file mode 100644 index b869f18..0000000 --- a/lib/domain/services/profile_service.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class ProfileService { - Future getProfileName(); -} diff --git a/lib/domain/use_cases/profile/get_profile_use_case.dart b/lib/domain/use_cases/profile/get_profile_use_case.dart new file mode 100644 index 0000000..1ba9510 --- /dev/null +++ b/lib/domain/use_cases/profile/get_profile_use_case.dart @@ -0,0 +1,13 @@ +import 'package:flutter_template/domain/services/profile/profile_service.dart'; +import 'package:injectable/injectable.dart'; + +@injectable +final class GetProfileUseCase { + GetProfileUseCase({required this.profileService}); + + final ProfileService profileService; + + Future call() { + return profileService.getProfileName(); + } +} diff --git a/lib/extensions/future_extensions.dart b/lib/extensions/future_extensions.dart index 0451c4d..786ab0e 100644 --- a/lib/extensions/future_extensions.dart +++ b/lib/extensions/future_extensions.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter_template/data/services/response_error.dart'; +import 'package:flutter_template/data/response_objects/response_error.dart'; extension FutureExtensions on Future { Future catchPrintError(Function onError) { diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index a2a587d..695ce06 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -11,6 +11,7 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { final appRouter = injector.get(); + return MaterialApp.router( debugShowCheckedModeBanner: false, theme: getAppTheme(Brightness.light), diff --git a/lib/presentation/feature/home/home_screen.dart b/lib/presentation/feature/home/home_screen.dart deleted file mode 100644 index 003d4d8..0000000 --- a/lib/presentation/feature/home/home_screen.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_template/presentation/feature/home/home_screen_tab.dart'; -import 'package:flutter_template/presentation/feature/news/news_page.dart'; -import 'package:flutter_template/presentation/feature/profile/profile_page.dart'; -import 'package:flutter_template/presentation/resources/resources.dart'; - -@RoutePage() -class HomeScreen extends StatefulWidget { - const HomeScreen({ - Key? key, - this.tab = HomeScreenTab.news, - }) : super(key: key); - - final HomeScreenTab tab; - - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - HomeScreenTab? _tabSelection; - - @override - void initState() { - super.initState(); - _tabSelection = widget.tab; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: context.colors.background, - body: _getSelectedPage(_tabSelection), - bottomNavigationBar: BottomNavigationBar( - backgroundColor: context.colors.surface, - unselectedItemColor: context.colors.text, - items: _getBottomNavigationBarItems(context), - currentIndex: _tabSelection!.index, - type: BottomNavigationBarType.fixed, - onTap: (index) => setState(() { - _tabSelection = HomeScreenTab.values[index]; - }), - ), - ); - } - - Widget _getSelectedPage(HomeScreenTab? tab) { - switch (tab) { - case HomeScreenTab.news: - return const NewsPage(); - case HomeScreenTab.profile: - return const ProfilePage(); - default: - throw ('Unknown HomeScreenTab'); - } - } - - List _getBottomNavigationBarItems( - BuildContext context, - ) { - return [ - const BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'News', - ), - const BottomNavigationBarItem( - icon: Icon(Icons.person_rounded), - label: 'Profile', - ), - ]; - } -} diff --git a/lib/presentation/feature/home/home_screen_tab.dart b/lib/presentation/feature/home/home_screen_tab.dart deleted file mode 100644 index 78c43f3..0000000 --- a/lib/presentation/feature/home/home_screen_tab.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum HomeScreenTab { - news, - profile, -} diff --git a/lib/presentation/feature/profile/profile_cubit.dart b/lib/presentation/feature/profile/profile_cubit.dart deleted file mode 100644 index 70f0329..0000000 --- a/lib/presentation/feature/profile/profile_cubit.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_template/domain/services/profile_service.dart'; -import 'package:flutter_template/extensions/extensions.dart'; -import 'package:flutter_template/presentation/feature/profile/profile_state.dart'; -import 'package:injectable/injectable.dart'; - -@Injectable() -class ProfileCubit extends Cubit { - final ProfileService profileService; - - ProfileCubit({ - required this.profileService, - @factoryParam ProfileState? state, - }) : super(state ?? ProfileState.initial()); - - Future load() async { - if (state.isLoading) return; - - emit(state.copyWith(isLoading: true)); - - return profileService - .getProfileName() - .then( - (value) => emit( - state.copyWith(isLoading: false, name: value ?? ''), - ), - ) - .catchPrintError((e, s) { - emit( - state.copyWith( - isLoading: false, - name: '', - ), - ); - }); - } -} diff --git a/lib/presentation/feature/profile/profile_page.dart b/lib/presentation/feature/profile/profile_page.dart deleted file mode 100644 index 45edaa1..0000000 --- a/lib/presentation/feature/profile/profile_page.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_template/injection/injector.dart'; -import 'package:flutter_template/presentation/feature/profile/profile_cubit.dart'; -import 'package:flutter_template/presentation/feature/profile/profile_state.dart'; -import 'package:flutter_template/presentation/resources/resources.dart'; - -class ProfilePage extends StatefulWidget { - const ProfilePage({Key? key}) : super(key: key); - - @override - State createState() => _ProfilePageState(); -} - -class _ProfilePageState extends State { - final _profilePresenter = injector.get(); - - @override - void initState() { - super.initState(); - _profilePresenter.load(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: context.colors.background, - appBar: AppBar( - backgroundColor: context.colors.accent, - title: BlocBuilder( - bloc: _profilePresenter, - builder: (context, state) { - return Text(state.isLoading ? 'Profile' : 'Profile: ${state.name}'); - }, - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Center( - child: Column( - children: [ - BlocBuilder( - bloc: _profilePresenter, - builder: (context, state) { - if (state.isLoading) { - return const CircularProgressIndicator(); - } else { - return Text('Hi ${state.name}!'); - } - }, - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/presentation/features/_playground/playground_screen.dart b/lib/presentation/features/_playground/playground_screen.dart new file mode 100644 index 0000000..a687cd1 --- /dev/null +++ b/lib/presentation/features/_playground/playground_screen.dart @@ -0,0 +1,44 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/routes/router.gr.dart'; + +@RoutePage() +class PlaygroundScreen extends StatelessWidget { + const PlaygroundScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Playground'), + ), + body: const SingleChildScrollView( + child: Column( + children: [], + ), + ), + ); + } +} + +class PlaygroundScreenOpenerButton extends StatelessWidget { + const PlaygroundScreenOpenerButton({super.key}); + + @override + Widget build(BuildContext context) { + if (kReleaseMode) { + return const SizedBox.shrink(); + } + + return TextButton( + onPressed: () { + context.pushRoute(const PlaygroundRoute()); + }, + child: const Text( + 'Open Playground Screen', + ), + ); + } +} diff --git a/lib/presentation/features/home/home_page.dart b/lib/presentation/features/home/home_page.dart new file mode 100644 index 0000000..7307695 --- /dev/null +++ b/lib/presentation/features/home/home_page.dart @@ -0,0 +1,21 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/features/home/ui/home_body.dart'; +import 'package:flutter_template/presentation/resources/resources.dart'; + +@RoutePage() +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: context.colors.background, + appBar: AppBar( + backgroundColor: context.colors.accent, + title: const Text('Home'), + ), + body: const HomeBody(), + ); + } +} diff --git a/lib/presentation/features/home/ui/home_body.dart b/lib/presentation/features/home/ui/home_body.dart new file mode 100644 index 0000000..c8240c5 --- /dev/null +++ b/lib/presentation/features/home/ui/home_body.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HomeBody extends StatelessWidget { + const HomeBody({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} diff --git a/lib/presentation/features/main/main_screen.dart b/lib/presentation/features/main/main_screen.dart new file mode 100644 index 0000000..833fe4f --- /dev/null +++ b/lib/presentation/features/main/main_screen.dart @@ -0,0 +1,32 @@ +import 'package:auto_route/auto_route.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/features/main/ui/app_bottom_navigation_bar.dart'; +import 'package:flutter_template/presentation/routes/router.gr.dart'; + +@RoutePage() +class MainScreen extends StatelessWidget { + const MainScreen({super.key}); + + @override + Widget build(BuildContext context) { + return AutoTabsScaffold( + routes: const [ + HomeRoute(), + NewsRoute(), + ProfileRoute(), + ], + animationDuration: Duration.zero, + bottomNavigationBuilder: (_, tabsRouter) { + return AppBottomNavigationBar( + tabsRouter: tabsRouter, + bottomNavigationBarItemList: const [ + BottomNavigationBarItem(label: 'Home', icon: Icon(Icons.home)), + BottomNavigationBarItem(label: 'News', icon: Icon(Icons.newspaper)), + BottomNavigationBarItem(label: 'Profile', icon: Icon(Icons.person)), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/features/main/ui/app_bottom_navigation_bar.dart b/lib/presentation/features/main/ui/app_bottom_navigation_bar.dart new file mode 100644 index 0000000..0ce9bbf --- /dev/null +++ b/lib/presentation/features/main/ui/app_bottom_navigation_bar.dart @@ -0,0 +1,25 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/resources.dart'; + +class AppBottomNavigationBar extends StatelessWidget { + const AppBottomNavigationBar({ + super.key, + required this.tabsRouter, + required this.bottomNavigationBarItemList, + }); + + final TabsRouter tabsRouter; + final List bottomNavigationBarItemList; + + @override + Widget build(BuildContext context) { + return BottomNavigationBar( + backgroundColor: context.colors.background, + type: BottomNavigationBarType.fixed, + currentIndex: tabsRouter.activeIndex, + onTap: tabsRouter.setActiveIndex, + items: bottomNavigationBarItemList, + ); + } +} diff --git a/lib/presentation/feature/news/news_page.dart b/lib/presentation/features/news/news_page.dart similarity index 72% rename from lib/presentation/feature/news/news_page.dart rename to lib/presentation/features/news/news_page.dart index 5a2ade7..c1277e8 100644 --- a/lib/presentation/feature/news/news_page.dart +++ b/lib/presentation/features/news/news_page.dart @@ -1,6 +1,9 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/features/news/ui/news_body.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; +@RoutePage() class NewsPage extends StatelessWidget { const NewsPage({Key? key}) : super(key: key); @@ -12,6 +15,7 @@ class NewsPage extends StatelessWidget { backgroundColor: context.colors.accent, title: const Text('News'), ), + body: const NewsBody(), ); } } diff --git a/lib/presentation/features/news/ui/news_body.dart b/lib/presentation/features/news/ui/news_body.dart new file mode 100644 index 0000000..c802d82 --- /dev/null +++ b/lib/presentation/features/news/ui/news_body.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/features/_playground/playground_screen.dart'; + +class NewsBody extends StatelessWidget { + const NewsBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: PlaygroundScreenOpenerButton(), + ); + } +} diff --git a/lib/presentation/features/profile/cubit/profile_cubit.dart b/lib/presentation/features/profile/cubit/profile_cubit.dart new file mode 100644 index 0000000..93a9001 --- /dev/null +++ b/lib/presentation/features/profile/cubit/profile_cubit.dart @@ -0,0 +1,43 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_template/data/response_objects/response_error.dart'; +import 'package:flutter_template/domain/common/base_status/base_status.dart'; +import 'package:flutter_template/domain/use_cases/profile/get_profile_use_case.dart'; + +import 'package:flutter_template/presentation/features/profile/cubit/profile_state.dart'; +import 'package:injectable/injectable.dart'; + +@injectable +final class ProfileCubit extends Cubit { + ProfileCubit(this._getProfileUseCase) : super(ProfileState.initial()); + + final GetProfileUseCase _getProfileUseCase; + + Future init() async { + if (state.initializationStatus.isLoading) { + return; + } + + emit( + state.copyWith(initializationStatus: const BaseStatus.loading()), + ); + + try { + final profileName = await _getProfileUseCase(); + + return emit( + state.copyWith( + initializationStatus: const BaseStatus.success(), + name: profileName ?? 'N/A', + ), + ); + } catch (e) { + final responseError = ResponseError.from(e); + + return emit( + state.copyWith( + initializationStatus: BaseStatus.failure(responseError), + ), + ); + } + } +} diff --git a/lib/presentation/feature/profile/profile_state.dart b/lib/presentation/features/profile/cubit/profile_state.dart similarity index 56% rename from lib/presentation/feature/profile/profile_state.dart rename to lib/presentation/features/profile/cubit/profile_state.dart index 6900e78..63908b3 100644 --- a/lib/presentation/feature/profile/profile_state.dart +++ b/lib/presentation/features/profile/cubit/profile_state.dart @@ -1,3 +1,4 @@ +import 'package:flutter_template/domain/common/base_status/base_status.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'profile_state.freezed.dart'; @@ -7,14 +8,11 @@ class ProfileState with _$ProfileState { const ProfileState._(); factory ProfileState({ - required bool isLoading, - required String name, + @Default(BaseStatus.initial()) BaseStatus initializationStatus, + @Default('N/A') String name, }) = _ProfileState; factory ProfileState.initial() { - return ProfileState( - isLoading: false, - name: '', - ); + return ProfileState(); } } diff --git a/lib/presentation/features/profile/profile_page.dart b/lib/presentation/features/profile/profile_page.dart new file mode 100644 index 0000000..411ea66 --- /dev/null +++ b/lib/presentation/features/profile/profile_page.dart @@ -0,0 +1,27 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_template/injection/injector.dart'; +import 'package:flutter_template/presentation/features/profile/cubit/profile_cubit.dart'; +import 'package:flutter_template/presentation/features/profile/ui/profile_body.dart'; +import 'package:flutter_template/presentation/resources/resources.dart'; + +@RoutePage() +class ProfilePage extends StatelessWidget { + const ProfilePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => injector()..init(), + child: Scaffold( + backgroundColor: context.colors.background, + appBar: AppBar( + backgroundColor: context.colors.accent, + title: const Text('Profile'), + ), + body: const ProfileBody(), + ), + ); + } +} diff --git a/lib/presentation/features/profile/ui/profile_body.dart b/lib/presentation/features/profile/ui/profile_body.dart new file mode 100644 index 0000000..b1cb6df --- /dev/null +++ b/lib/presentation/features/profile/ui/profile_body.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_template/presentation/features/profile/cubit/profile_cubit.dart'; +import 'package:flutter_template/presentation/widgets/loading_indicator/app_loading_indicator.dart'; + +class ProfileBody extends StatelessWidget { + const ProfileBody({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final profileCubit = context.read(); + + final status = context.select( + (ProfileCubit cubit) => cubit.state.initializationStatus, + ); + + final name = context.select( + (ProfileCubit cubit) => cubit.state.name, + ); + + final child = Column( + children: [ + Padding( + padding: const EdgeInsets.all(32.0), + child: status.maybeWhen( + loading: () => AppLoadingIndicator.small(), + success: () => Text( + name, + textAlign: TextAlign.center, + ), + orElse: () { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Something went wrong.', + ), + TextButton( + onPressed: profileCubit.init, + child: const Text('Try again.'), + ), + ], + ); + }, + ), + ), + ], + ); + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: child, + ); + } +} diff --git a/lib/presentation/feature/splash/splash_screen.dart b/lib/presentation/features/splash/splash_screen.dart similarity index 95% rename from lib/presentation/feature/splash/splash_screen.dart rename to lib/presentation/features/splash/splash_screen.dart index 3e8d3e2..2bb5a74 100644 --- a/lib/presentation/feature/splash/splash_screen.dart +++ b/lib/presentation/features/splash/splash_screen.dart @@ -20,7 +20,7 @@ class _SplashScreenState extends State { void initState() { super.initState(); _timer = Timer(const Duration(seconds: 2), () { - context.router.replace(HomeRoute()); + context.router.replace(const MainRoute()); }); } diff --git a/lib/presentation/routes/router.dart b/lib/presentation/routes/router.dart index 737714d..a040b5c 100644 --- a/lib/presentation/routes/router.dart +++ b/lib/presentation/routes/router.dart @@ -1,6 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter_template/presentation/routes/router.gr.dart'; +/// 💡 Tip: Both `~Screen`, `~Page` will be treated as `~Route` by [AutoRoute]. +/// +/// Consider using `~Screen` as screen naming, and `~Page` if it is part of screen view, +/// for example, Bottom navigation child views, tab views, etc. +/// @AutoRouterConfig() class AppRouter extends $AppRouter { @override @@ -11,10 +16,33 @@ class AppRouter extends $AppRouter { AutoRoute( page: SplashRoute.page, path: '/', + initial: true, ), AutoRoute( - page: HomeRoute.page, - path: '/home', + page: MainRoute.page, + path: '/main', + children: [ + AutoRoute( + page: HomeRoute.page, + path: 'news', + initial: true, + ), + AutoRoute( + page: NewsRoute.page, + path: 'news', + ), + AutoRoute( + page: ProfileRoute.page, + path: 'profile', + ), + ], + ), + + // Playground Screen: Keep it as the last item of this list. + AutoRoute( + page: PlaygroundRoute.page, + path: '/playground', + fullscreenDialog: true, ), ]; } diff --git a/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart b/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart new file mode 100644 index 0000000..5b3b54a --- /dev/null +++ b/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + /// Determines both height and width of the [AppLoadingIndicator] + static const defaultSize = 24.0; + + /// Determines both height and width of the [AppLoadingIndicator.small] + static const defaultSmallSize = 16.0; + + const AppLoadingIndicator({ + super.key, + this.size = defaultSize, + this.strokeWidth = 4.0, + }); + + /// Determines both height and width of the loader. + final double size; + + /// The width of the material loader stroke. + final double strokeWidth; + + factory AppLoadingIndicator.small() { + return const AppLoadingIndicator( + size: defaultSmallSize, + strokeWidth: 2.0, + ); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: size, + width: size, + child: CircularProgressIndicator.adaptive( + strokeWidth: strokeWidth, + ), + ); + } +} From 75e9207bf4a37a79266fc870675d9f04d1d220b4 Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Wed, 24 Jan 2024 20:59:48 +0600 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20Add=20theme=20and=20basic=20UI=20?= =?UTF-8?q?widgets=20=F0=9F=8E=AD=20(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/injection/injector.dart | 1 - lib/presentation/app.dart | 6 +- .../extensions/color_extensions.dart | 18 + .../extensions/presentation_extensions.dart | 2 + .../scroll_controller_extensions.dart | 19 + .../_playground/playground_screen.dart | 253 +++++++++++- lib/presentation/features/home/home_page.dart | 12 +- lib/presentation/features/news/news_page.dart | 6 +- .../features/news/ui/news_body.dart | 8 +- .../features/profile/cubit/profile_cubit.dart | 4 + .../features/profile/profile_page.dart | 6 +- .../features/profile/ui/profile_body.dart | 11 +- .../features/splash/splash_screen.dart | 10 +- .../resources/app_color_palette.dart | 29 ++ lib/presentation/resources/app_colors.dart | 269 +++++++++++-- lib/presentation/resources/app_fonts.dart | 6 +- .../resources/app_text_styles.dart | 151 ++++++- lib/presentation/resources/app_theme.dart | 64 ++- .../resources/app_ui_constants.dart | 49 +++ .../widgets/app_bar/top_app_bar.dart | 41 ++ .../widgets/button/app_button.dart | 247 ++++++++++++ .../disable_widget/app_disable_widget.dart | 52 +++ .../app_loading_indicator.dart | 14 +- .../widgets/spacers/safe_area_spacer.dart | 20 + lib/presentation/widgets/text/app_text.dart | 381 ++++++++++++++++++ ...ismissible.dart => app_screen_config.dart} | 36 +- .../widgets/utilities/top_app_bar_config.dart | 32 ++ pubspec.yaml | 24 +- 28 files changed, 1661 insertions(+), 110 deletions(-) create mode 100644 lib/presentation/extensions/color_extensions.dart create mode 100644 lib/presentation/extensions/scroll_controller_extensions.dart create mode 100644 lib/presentation/resources/app_color_palette.dart create mode 100644 lib/presentation/resources/app_ui_constants.dart create mode 100644 lib/presentation/widgets/app_bar/top_app_bar.dart create mode 100644 lib/presentation/widgets/button/app_button.dart create mode 100644 lib/presentation/widgets/disable_widget/app_disable_widget.dart create mode 100644 lib/presentation/widgets/spacers/safe_area_spacer.dart create mode 100644 lib/presentation/widgets/text/app_text.dart rename lib/presentation/widgets/utilities/{focus_scope_dismissible.dart => app_screen_config.dart} (53%) create mode 100644 lib/presentation/widgets/utilities/top_app_bar_config.dart diff --git a/lib/injection/injector.dart b/lib/injection/injector.dart index dc5032f..3a1030b 100644 --- a/lib/injection/injector.dart +++ b/lib/injection/injector.dart @@ -10,4 +10,3 @@ GetIt injector = GetIt.instance; asExtension: false, // default ) Future configureDependencies() async => $initGetIt(injector); - diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index 695ce06..51957e9 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -14,8 +14,10 @@ class App extends StatelessWidget { return MaterialApp.router( debugShowCheckedModeBanner: false, - theme: getAppTheme(Brightness.light), - darkTheme: getAppTheme(Brightness.dark), + theme: AppTheme.fromBrightness(Brightness.light), + darkTheme: AppTheme.fromBrightness(Brightness.dark), + // TODO: Set to [ThemeMode.light] if your app only supports light mode + themeMode: ThemeMode.system, title: 'Project Name', builder: (c, widget) { if (widget == null) return const SizedBox(); diff --git a/lib/presentation/extensions/color_extensions.dart b/lib/presentation/extensions/color_extensions.dart new file mode 100644 index 0000000..c22ab22 --- /dev/null +++ b/lib/presentation/extensions/color_extensions.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +extension ColorExtensions on Color { + /// Returns a new color with reduced opacity. + /// + /// The resulting color is a copy of the original color with an opacity + /// level of 0.44, making it suitable for creating a visually muted or + /// disabled appearance. + /// + /// Example: + /// ```dart + /// final myColor = Colors.blue; + /// final mutedColor = myColor.lowOpacity(); + /// ``` + Color lowOpacity() { + return withOpacity(0.44); + } +} diff --git a/lib/presentation/extensions/presentation_extensions.dart b/lib/presentation/extensions/presentation_extensions.dart index 15b5cd9..6e8590c 100644 --- a/lib/presentation/extensions/presentation_extensions.dart +++ b/lib/presentation/extensions/presentation_extensions.dart @@ -1,4 +1,6 @@ // A set of extensions that can be used on the presentation layer. library presentation_extensions; +export 'color_extensions.dart'; +export 'scroll_controller_extensions.dart'; export 'string_presentation_extensions.dart'; diff --git a/lib/presentation/extensions/scroll_controller_extensions.dart b/lib/presentation/extensions/scroll_controller_extensions.dart new file mode 100644 index 0000000..5f83402 --- /dev/null +++ b/lib/presentation/extensions/scroll_controller_extensions.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/app_ui_constants.dart'; + +extension ScrollControllerExtensions on ScrollController { + Future appAnimateTo( + double offset, { + Duration duration = AppUiConstants.animationDuration, + Curve curve = AppUiConstants.transitionCurve, + }) { + return animateTo(offset, duration: duration, curve: curve); + } + + Future animateToTop({ + Duration duration = AppUiConstants.animationDuration, + Curve curve = Curves.fastEaseInToSlowEaseOut, + }) { + return appAnimateTo(0, duration: duration, curve: curve); + } +} diff --git a/lib/presentation/features/_playground/playground_screen.dart b/lib/presentation/features/_playground/playground_screen.dart index a687cd1..bf33d09 100644 --- a/lib/presentation/features/_playground/playground_screen.dart +++ b/lib/presentation/features/_playground/playground_screen.dart @@ -2,21 +2,252 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:flutter_template/presentation/resources/app_colors.dart'; +import 'package:flutter_template/presentation/resources/app_ui_constants.dart'; import 'package:flutter_template/presentation/routes/router.gr.dart'; +import 'package:flutter_template/presentation/widgets/app_bar/top_app_bar.dart'; +import 'package:flutter_template/presentation/widgets/button/app_button.dart'; +import 'package:flutter_template/presentation/widgets/spacers/safe_area_spacer.dart'; +import 'package:flutter_template/presentation/widgets/text/app_text.dart'; +import 'package:flutter_template/presentation/widgets/utilities/app_screen_config.dart'; +/// A playground screen designed for showcasing reusable widgets. +/// +/// This screen is intended for presenting and testing reusable widgets within +/// the context of a Flutter application. +/// +/// If you need to extract some small widgets only for this playground screen, +/// consider using the private modifier for widget classes. @RoutePage() class PlaygroundScreen extends StatelessWidget { const PlaygroundScreen({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Playground'), + const verticalGap = SizedBox(height: 12); + const sliverGap = SliverToBoxAdapter( + child: verticalGap, + ); + + return AppScreenConfig( + child: Scaffold( + appBar: const TopAppBar( + label: 'Playground', + ), + body: CustomScrollView( + slivers: [ + _PlaygroundStickyHeader( + label: 'Color Boxes', + child: Wrap( + children: [ + _PlaygroundColorBox( + label: 'Primary', + color: context.colors.primary, + labelColor: context.colors.foregroundOnPrimary, + ), + _PlaygroundColorBox( + label: 'Secondary', + color: context.colors.secondary, + labelColor: context.colors.foregroundOnSecondary, + ), + _PlaygroundColorBox( + label: 'Background', + color: context.colors.background, + labelColor: context.colors.foregroundOnBackground, + ), + _PlaygroundColorBox( + label: 'Danger', + color: context.colors.danger, + labelColor: context.colors.foregroundOnDanger, + ), + _PlaygroundColorBox( + label: 'Outline', + color: context.colors.outline, + labelColor: context.colors.foregroundOnPrimary, + ), + _PlaygroundColorBox( + label: 'Primary Variant', + color: context.colors.primaryVariant, + labelColor: context.colors.foregroundOnPrimary, + ), + _PlaygroundColorBox( + label: 'Secondary Variant', + color: context.colors.secondaryVariant, + labelColor: context.colors.foregroundOnSecondary, + ), + _PlaygroundColorBox( + label: 'Splash', + color: context.colors.splashColor, + labelColor: context.colors.foregroundOnBackground, + ), + ], + ), + ), + sliverGap, + _PlaygroundStickyHeader( + label: 'Buttons', + child: Column( + children: [ + AppButton.primary( + label: 'Primary Button', + onPressed: () {}, + ), + verticalGap, + AppButton.secondary( + label: 'Secondary Button', + onPressed: () {}, + ), + verticalGap, + AppButton.outlined( + label: 'Outlined Button', + onPressed: () {}, + ), + verticalGap, + AppButton.destructive( + label: 'Destructive Button', + onPressed: () {}, + ), + verticalGap, + AppButton.text( + label: 'Text Button', + onPressed: () {}, + ), + ], + ), + ), + sliverGap, + _PlaygroundStickyHeader( + label: 'Small Buttons', + child: Column( + children: [ + Row( + children: [ + Expanded( + child: AppButton.primary( + label: 'Small Primary Button', + isSmall: true, + onPressed: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppButton.secondary( + label: 'Small Secondary Button', + isSmall: true, + onPressed: () {}, + ), + ), + ], + ), + ], + ), + ), + sliverGap, + _PlaygroundStickyHeader( + label: 'Texts', + child: Column( + children: [ + AppText.header1('Header 1'), + verticalGap, + AppText.header2('Header 2'), + verticalGap, + AppText.header3('Header 3'), + verticalGap, + AppText.bodySmall('This is Body (small).'), + verticalGap, + AppText.body('This is Body (regular).'), + verticalGap, + AppText.bodyLarge('This is Body (large).'), + verticalGap, + AppText.buttonLabel('This is Button label.'), + verticalGap, + AppText.underlineText('This is underlined text.'), + verticalGap, + AppText.custom( + 'This is custom', + fontWeight: FontWeight.w700, + ), + ], + ), + ), + const SliverToBoxAdapter( + child: SizedBox( + height: 72, + ), + ), + const SliverToBoxAdapter( + child: SafeAreaSpacer(), + ), + ], + ), ), - body: const SingleChildScrollView( - child: Column( - children: [], + ); + } +} + +class _PlaygroundStickyHeader extends StatelessWidget { + const _PlaygroundStickyHeader({ + required this.label, + required this.child, + }); + + final String label; + final Widget child; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: ColoredBox( + color: context.colors.background, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 12.0, + ).copyWith(bottom: 12.0), + child: AppText.header3(label), + ), + ), + sliver: SliverToBoxAdapter( + child: Padding( + padding: AppUiConstants.defaultScreenHorizontalPadding, + child: child, + ), + ), + ); + } +} + +class _PlaygroundColorBox extends StatelessWidget { + const _PlaygroundColorBox({ + required this.label, + required this.color, + required this.labelColor, + }); + + final String label; + final Color color; + final Color labelColor; + + final boxSize = 100.0; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + width: boxSize, + height: boxSize, + color: color, + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AppText.body( + label, + textAlign: TextAlign.center, + color: labelColor, + ), + ), ), ), ); @@ -32,12 +263,10 @@ class PlaygroundScreenOpenerButton extends StatelessWidget { return const SizedBox.shrink(); } - return TextButton( - onPressed: () { - context.pushRoute(const PlaygroundRoute()); - }, - child: const Text( - 'Open Playground Screen', + return AppButton.text( + label: 'Open Playground Screen', + onPressed: () => context.pushRoute( + const PlaygroundRoute(), ), ); } diff --git a/lib/presentation/features/home/home_page.dart b/lib/presentation/features/home/home_page.dart index 7307695..9714552 100644 --- a/lib/presentation/features/home/home_page.dart +++ b/lib/presentation/features/home/home_page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/features/home/ui/home_body.dart'; -import 'package:flutter_template/presentation/resources/resources.dart'; +import 'package:flutter_template/presentation/widgets/app_bar/top_app_bar.dart'; @RoutePage() class HomePage extends StatelessWidget { @@ -9,13 +9,11 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: context.colors.background, - appBar: AppBar( - backgroundColor: context.colors.accent, - title: const Text('Home'), + return const Scaffold( + appBar: TopAppBar( + label: 'Home', ), - body: const HomeBody(), + body: HomeBody(), ); } } diff --git a/lib/presentation/features/news/news_page.dart b/lib/presentation/features/news/news_page.dart index c1277e8..b0dfdf1 100644 --- a/lib/presentation/features/news/news_page.dart +++ b/lib/presentation/features/news/news_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/features/news/ui/news_body.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; +import 'package:flutter_template/presentation/widgets/app_bar/top_app_bar.dart'; @RoutePage() class NewsPage extends StatelessWidget { @@ -11,9 +12,8 @@ class NewsPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: context.colors.background, - appBar: AppBar( - backgroundColor: context.colors.accent, - title: const Text('News'), + appBar: const TopAppBar( + label: 'News', ), body: const NewsBody(), ); diff --git a/lib/presentation/features/news/ui/news_body.dart b/lib/presentation/features/news/ui/news_body.dart index c802d82..a96ee5f 100644 --- a/lib/presentation/features/news/ui/news_body.dart +++ b/lib/presentation/features/news/ui/news_body.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/features/_playground/playground_screen.dart'; +import 'package:flutter_template/presentation/resources/app_ui_constants.dart'; class NewsBody extends StatelessWidget { const NewsBody({super.key}); @override Widget build(BuildContext context) { - return const Center( - child: PlaygroundScreenOpenerButton(), + return const Padding( + padding: AppUiConstants.defaultScreenHorizontalPadding, + child: Center( + child: PlaygroundScreenOpenerButton(), + ), ); } } diff --git a/lib/presentation/features/profile/cubit/profile_cubit.dart b/lib/presentation/features/profile/cubit/profile_cubit.dart index 93a9001..bfd5e19 100644 --- a/lib/presentation/features/profile/cubit/profile_cubit.dart +++ b/lib/presentation/features/profile/cubit/profile_cubit.dart @@ -24,6 +24,8 @@ final class ProfileCubit extends Cubit { try { final profileName = await _getProfileUseCase(); + if (isClosed) return; + return emit( state.copyWith( initializationStatus: const BaseStatus.success(), @@ -33,6 +35,8 @@ final class ProfileCubit extends Cubit { } catch (e) { final responseError = ResponseError.from(e); + if (isClosed) return; + return emit( state.copyWith( initializationStatus: BaseStatus.failure(responseError), diff --git a/lib/presentation/features/profile/profile_page.dart b/lib/presentation/features/profile/profile_page.dart index 411ea66..0f4a857 100644 --- a/lib/presentation/features/profile/profile_page.dart +++ b/lib/presentation/features/profile/profile_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_template/injection/injector.dart'; import 'package:flutter_template/presentation/features/profile/cubit/profile_cubit.dart'; import 'package:flutter_template/presentation/features/profile/ui/profile_body.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; +import 'package:flutter_template/presentation/widgets/app_bar/top_app_bar.dart'; @RoutePage() class ProfilePage extends StatelessWidget { @@ -16,9 +17,8 @@ class ProfilePage extends StatelessWidget { create: (context) => injector()..init(), child: Scaffold( backgroundColor: context.colors.background, - appBar: AppBar( - backgroundColor: context.colors.accent, - title: const Text('Profile'), + appBar: const TopAppBar( + label: 'Profile', ), body: const ProfileBody(), ), diff --git a/lib/presentation/features/profile/ui/profile_body.dart b/lib/presentation/features/profile/ui/profile_body.dart index b1cb6df..61ae055 100644 --- a/lib/presentation/features/profile/ui/profile_body.dart +++ b/lib/presentation/features/profile/ui/profile_body.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_template/presentation/features/profile/cubit/profile_cubit.dart'; +import 'package:flutter_template/presentation/widgets/button/app_button.dart'; import 'package:flutter_template/presentation/widgets/loading_indicator/app_loading_indicator.dart'; +import 'package:flutter_template/presentation/widgets/text/app_text.dart'; class ProfileBody extends StatelessWidget { const ProfileBody({ @@ -26,7 +28,7 @@ class ProfileBody extends StatelessWidget { padding: const EdgeInsets.all(32.0), child: status.maybeWhen( loading: () => AppLoadingIndicator.small(), - success: () => Text( + success: () => AppText.body( name, textAlign: TextAlign.center, ), @@ -34,12 +36,13 @@ class ProfileBody extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( + AppText.body( 'Something went wrong.', ), - TextButton( + AppButton.text( + label: 'Try again', + isSmall: true, onPressed: profileCubit.init, - child: const Text('Try again.'), ), ], ); diff --git a/lib/presentation/features/splash/splash_screen.dart b/lib/presentation/features/splash/splash_screen.dart index 2bb5a74..37d863e 100644 --- a/lib/presentation/features/splash/splash_screen.dart +++ b/lib/presentation/features/splash/splash_screen.dart @@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; import 'package:flutter_template/presentation/routes/router.gr.dart'; +import 'package:flutter_template/presentation/widgets/text/app_text.dart'; @RoutePage() class SplashScreen extends StatefulWidget { @@ -33,10 +34,13 @@ class _SplashScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: context.colors.accent, - body: const SafeArea( + backgroundColor: context.colors.appBarBackground, + body: SafeArea( child: Center( - child: Text('SplashScreen'), + child: AppText.body( + 'SplashScreen', + color: context.colors.foregroundOnAppBar, + ), ), ), ); diff --git a/lib/presentation/resources/app_color_palette.dart b/lib/presentation/resources/app_color_palette.dart new file mode 100644 index 0000000..d988326 --- /dev/null +++ b/lib/presentation/resources/app_color_palette.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +/// Defines color palette for the application. +/// +/// Consider using the color name that is mentioned in Figma. +sealed class AppColorPalette { + const AppColorPalette._(); + + static const alpha = Colors.transparent; + + static const americanOrange = Color(0xFFFF8800); + static const black = Color(0xFF000000); + static const blackLead = Color(0xFF202020); + static const cantaloupe = Color(0xFFFFE600); + static const darkGrey = Color(0xFF171717); + static const foggyGrey = Color(0xFF363636); + static const mutedGrey = Color(0xFF444444); + static const graphite = Color(0xFF444444); + static const keylime = Color(0xFFF0E68C); + static const lightLime = Color(0xFFF9F3C2); + static const lightSkyBlue = Color(0xFF4C9DFF); + static const oceanBlue = Color(0xFF1976D2); + static const red = Color(0xFFDD2C00); + static const silverPolish = Color(0xFFC6C6C6); + static const royalPurple = Color(0xFF4444DA); + static const ultraviolet = Color(0xFFB2AEF3); + static const white = Color(0xFFFFFFFF); + static const whisperingBlue = Color(0xFFD4E4FC); +} diff --git a/lib/presentation/resources/app_colors.dart b/lib/presentation/resources/app_colors.dart index 2cef8b9..8e651ac 100644 --- a/lib/presentation/resources/app_colors.dart +++ b/lib/presentation/resources/app_colors.dart @@ -1,43 +1,254 @@ import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/app_color_palette.dart'; -class AppColors { - final Color background; - final Color surface; - final Color accent; - final Color text; +class AppColors extends ThemeExtension { + static Color get black => AppColorPalette.black; + static Color get white => AppColorPalette.white; const AppColors({ + required this.primary, + required this.primaryVariant, + required this.secondary, + required this.secondaryVariant, required this.background, - required this.surface, - required this.accent, - required this.text, + required this.appBarBackground, + required this.danger, + required this.foregroundOnBackground, + required this.foregroundLightOnBackground, + required this.foregroundOnPrimary, + required this.foregroundOnSecondary, + required this.foregroundOnAppBar, + required this.foregroundOnDanger, + required this.outline, + required this.splashColor, + required this.disabledColor, + required this.transparant, }); -} -const colorsLight = AppColors( - background: Colors.white, - surface: Colors.white, - accent: Colors.blueAccent, - text: Colors.black, -); + // Core colors + final Color primary; + final Color primaryVariant; + final Color secondary; + final Color secondaryVariant; + final Color background; + final Color appBarBackground; + final Color danger; + final Color foregroundOnBackground; + final Color foregroundLightOnBackground; + final Color foregroundOnPrimary; + final Color foregroundOnSecondary; + final Color foregroundOnAppBar; + final Color foregroundOnDanger; + final Color outline; + final Color transparant; -const colorsDark = AppColors( - background: Colors.black54, - surface: Color(0xFF222222), - accent: Colors.lightBlueAccent, - text: Colors.white, -); + // Other colors + final Color splashColor; + final Color disabledColor; + + factory AppColors.fromBrightness(Brightness brightness) => + switch (brightness) { + Brightness.light => AppColors.light(), + Brightness.dark => AppColors.dark(), + }; + + factory AppColors.light() { + return const AppColors( + primary: AppColorPalette.oceanBlue, + primaryVariant: AppColorPalette.lightSkyBlue, + secondary: AppColorPalette.ultraviolet, + secondaryVariant: AppColorPalette.lightLime, + background: AppColorPalette.white, + appBarBackground: AppColorPalette.oceanBlue, + danger: AppColorPalette.red, + foregroundOnBackground: AppColorPalette.black, + foregroundLightOnBackground: AppColorPalette.graphite, + foregroundOnPrimary: AppColorPalette.white, + foregroundOnSecondary: AppColorPalette.royalPurple, + foregroundOnAppBar: AppColorPalette.white, + foregroundOnDanger: AppColorPalette.white, + outline: AppColorPalette.oceanBlue, + transparant: AppColorPalette.alpha, + splashColor: AppColorPalette.whisperingBlue, + disabledColor: AppColorPalette.mutedGrey, + ); + } + + factory AppColors.dark() { + return const AppColors( + primary: AppColorPalette.americanOrange, + primaryVariant: AppColorPalette.cantaloupe, + secondary: AppColorPalette.keylime, + secondaryVariant: AppColorPalette.lightLime, + background: AppColorPalette.blackLead, + appBarBackground: AppColorPalette.darkGrey, + danger: AppColorPalette.red, + foregroundOnBackground: AppColorPalette.white, + foregroundLightOnBackground: AppColorPalette.foggyGrey, + foregroundOnPrimary: AppColorPalette.black, + foregroundOnSecondary: AppColorPalette.black, + foregroundOnAppBar: AppColorPalette.white, + foregroundOnDanger: AppColorPalette.white, + outline: AppColorPalette.americanOrange, + transparant: AppColorPalette.alpha, + splashColor: AppColorPalette.darkGrey, + disabledColor: AppColorPalette.silverPolish, + ); + } + + @override + ThemeExtension copyWith({ + Color? primary, + Color? primaryVariant, + Color? secondary, + Color? secondaryVariant, + Color? background, + Color? appBarBackground, + Color? danger, + Color? foregroundOnBackground, + Color? foregroundLightOnBackground, + Color? foregroundOnPrimary, + Color? foregroundOnSecondary, + Color? foregroundOnAppBar, + Color? foregroundOnDanger, + Color? transparant, + Color? outline, + Color? splashColor, + Color? disabledColor, + }) { + return AppColors( + primary: primary ?? this.primary, + primaryVariant: primaryVariant ?? this.primaryVariant, + secondary: secondary ?? this.secondary, + secondaryVariant: secondaryVariant ?? this.secondaryVariant, + background: background ?? this.background, + appBarBackground: appBarBackground ?? this.appBarBackground, + danger: danger ?? this.danger, + foregroundOnBackground: + foregroundOnBackground ?? this.foregroundOnBackground, + foregroundLightOnBackground: + foregroundLightOnBackground ?? this.foregroundLightOnBackground, + foregroundOnPrimary: foregroundOnPrimary ?? this.foregroundOnPrimary, + foregroundOnSecondary: + foregroundOnSecondary ?? this.foregroundOnSecondary, + foregroundOnAppBar: foregroundOnAppBar ?? this.foregroundOnAppBar, + foregroundOnDanger: foregroundOnDanger ?? this.foregroundOnDanger, + outline: outline ?? this.outline, + transparant: transparant ?? this.transparant, + splashColor: splashColor ?? this.splashColor, + disabledColor: disabledColor ?? this.disabledColor, + ); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! AppColors) { + return this; + } + + return AppColors( + primary: Color.lerp( + primary, + other.primary, + t, + )!, + primaryVariant: Color.lerp( + primaryVariant, + other.primaryVariant, + t, + )!, + secondary: Color.lerp( + secondary, + other.secondary, + t, + )!, + secondaryVariant: Color.lerp( + secondaryVariant, + other.secondaryVariant, + t, + )!, + background: Color.lerp( + background, + other.background, + t, + )!, + appBarBackground: Color.lerp( + appBarBackground, + other.appBarBackground, + t, + )!, + danger: Color.lerp( + danger, + other.danger, + t, + )!, + foregroundOnBackground: Color.lerp( + foregroundOnBackground, + other.foregroundOnBackground, + t, + )!, + foregroundLightOnBackground: Color.lerp( + foregroundLightOnBackground, + other.foregroundLightOnBackground, + t, + )!, + foregroundOnPrimary: Color.lerp( + foregroundOnPrimary, + other.foregroundOnPrimary, + t, + )!, + foregroundOnSecondary: Color.lerp( + foregroundOnSecondary, + other.foregroundOnSecondary, + t, + )!, + foregroundOnAppBar: Color.lerp( + foregroundOnAppBar, + other.foregroundOnAppBar, + t, + )!, + foregroundOnDanger: Color.lerp( + foregroundOnDanger, + other.foregroundOnDanger, + t, + )!, + outline: Color.lerp( + outline, + other.outline, + t, + )!, + transparant: Color.lerp( + transparant, + other.transparant, + t, + )!, + splashColor: Color.lerp( + splashColor, + other.splashColor, + t, + )!, + disabledColor: Color.lerp( + disabledColor, + other.disabledColor, + t, + )!, + ); + } +} extension AppColorsExtension on BuildContext { AppColors get colors { - final brightness = Theme.of(this).brightness; - switch (brightness) { - case Brightness.light: - return colorsLight; - case Brightness.dark: - return colorsDark; - default: - return colorsLight; + final appColors = Theme.of(this).extension(); + + if (appColors == null) { + throw Exception( + 'Could not find the ThemeData extension for colors.\n Make sure to pass AppColors as ThemeData extension.', + ); } + + return appColors; } } diff --git a/lib/presentation/resources/app_fonts.dart b/lib/presentation/resources/app_fonts.dart index c8b5d62..eadeaee 100644 --- a/lib/presentation/resources/app_fonts.dart +++ b/lib/presentation/resources/app_fonts.dart @@ -1,3 +1,7 @@ -class AppFonts { +sealed class AppFonts { + AppFonts._(); + static const roboto = 'Roboto'; + + static String get activeFontFamily => roboto; } diff --git a/lib/presentation/resources/app_text_styles.dart b/lib/presentation/resources/app_text_styles.dart index b5d1fb7..57da292 100644 --- a/lib/presentation/resources/app_text_styles.dart +++ b/lib/presentation/resources/app_text_styles.dart @@ -1,12 +1,145 @@ import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/app_colors.dart'; +import 'package:flutter_template/presentation/resources/app_fonts.dart'; -class AppTextStyles { - static const headline1 = TextStyle( - fontWeight: FontWeight.w300, - fontSize: 34, - ); - static const body1 = TextStyle( - fontWeight: FontWeight.w300, - fontSize: 14, - ); +class AppTextStyles extends ThemeExtension { + AppTextStyles({ + required this.header1, + required this.header2, + required this.header3, + required this.appBarTitle, + required this.bodySmall, + required this.body, + required this.bodyLarge, + required this.buttonLabel, + required this.underlineText, + }); + + factory AppTextStyles.fromBrightness(Brightness brightness) { + final _appColors = AppColors.fromBrightness(brightness); + + return AppTextStyles( + header1: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + fontFamily: AppFonts.activeFontFamily, + ), + header2: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + fontFamily: AppFonts.activeFontFamily, + ), + header3: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: AppFonts.activeFontFamily, + ), + appBarTitle: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: _appColors.foregroundOnAppBar, + fontFamily: AppFonts.activeFontFamily, + ), + bodySmall: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: _appColors.foregroundOnBackground, + fontFamily: AppFonts.activeFontFamily, + ), + body: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: _appColors.foregroundOnBackground, + fontFamily: AppFonts.activeFontFamily, + ), + bodyLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: _appColors.foregroundOnBackground, + fontFamily: AppFonts.activeFontFamily, + ), + buttonLabel: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: AppFonts.activeFontFamily, + ), + underlineText: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: _appColors.foregroundOnBackground, + fontFamily: AppFonts.activeFontFamily, + decoration: TextDecoration.underline, + ), + ); + } + + final TextStyle header1; + final TextStyle header2; + final TextStyle header3; + final TextStyle appBarTitle; + final TextStyle bodySmall; + final TextStyle body; + final TextStyle bodyLarge; + final TextStyle buttonLabel; + final TextStyle underlineText; + + @override + ThemeExtension copyWith({ + TextStyle? header1, + TextStyle? header2, + TextStyle? header3, + TextStyle? appBarTitle, + TextStyle? bodySmall, + TextStyle? bodyLarge, + TextStyle? body, + TextStyle? buttonLabel, + TextStyle? underlineText, + }) { + return AppTextStyles( + header1: header1 ?? this.header1, + header2: header2 ?? this.header2, + header3: header3 ?? this.header3, + appBarTitle: appBarTitle ?? this.appBarTitle, + bodySmall: bodySmall ?? this.bodySmall, + body: body ?? this.body, + bodyLarge: bodyLarge ?? this.bodyLarge, + buttonLabel: buttonLabel ?? this.buttonLabel, + underlineText: underlineText ?? this.underlineText, + ); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! AppTextStyles) { + return this; + } + + return AppTextStyles( + header1: TextStyle.lerp(header1, other.header1, t)!, + header2: TextStyle.lerp(header2, other.header2, t)!, + header3: TextStyle.lerp(header3, other.header3, t)!, + appBarTitle: TextStyle.lerp(appBarTitle, other.appBarTitle, t)!, + bodySmall: TextStyle.lerp(bodySmall, other.bodySmall, t)!, + body: TextStyle.lerp(body, other.body, t)!, + bodyLarge: TextStyle.lerp(bodyLarge, other.bodyLarge, t)!, + buttonLabel: TextStyle.lerp(buttonLabel, other.buttonLabel, t)!, + underlineText: TextStyle.lerp(underlineText, other.underlineText, t)!, + ); + } +} + +extension AppTextStylesExtension on BuildContext { + AppTextStyles get textStyles { + final appTextStyles = Theme.of(this).extension(); + + if (appTextStyles == null) { + throw Exception( + 'Could not find the ThemeData extension for text styles.\n Make sure to pass AppTextStyles as ThemeData extension.', + ); + } + return appTextStyles; + } } diff --git a/lib/presentation/resources/app_theme.dart b/lib/presentation/resources/app_theme.dart index 0aec87e..4f0ad44 100644 --- a/lib/presentation/resources/app_theme.dart +++ b/lib/presentation/resources/app_theme.dart @@ -1,12 +1,62 @@ import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/extensions/color_extensions.dart'; import 'package:flutter_template/presentation/resources/resources.dart'; -ThemeData getAppTheme(Brightness brightness) { - final colors = brightness == Brightness.light ? colorsLight : colorsDark; +sealed class AppTheme { + const AppTheme._(); - return ThemeData( - brightness: brightness, - fontFamily: AppFonts.roboto, - scaffoldBackgroundColor: colors.background, - ); + static ThemeData fromBrightness(Brightness brightness) { + final _appColors = AppColors.fromBrightness(brightness); + final _appTextStyles = AppTextStyles.fromBrightness(brightness); + + final _lightColorScheme = ColorScheme.light( + brightness: brightness, + primary: _appColors.primary, + onPrimary: _appColors.foregroundOnPrimary, + background: _appColors.primary, + secondary: _appColors.secondary, + onSecondary: _appColors.foregroundOnSecondary, + onBackground: _appColors.foregroundOnBackground, + outline: _appColors.outline, + error: _appColors.danger, + onError: _appColors.foregroundOnDanger, + ); + + final _darkColorScheme = ColorScheme.dark( + brightness: brightness, + primary: _appColors.primary, + onPrimary: _appColors.foregroundOnPrimary, + secondary: _appColors.secondary, + onSecondary: _appColors.foregroundOnSecondary, + background: _appColors.background, + onBackground: _appColors.foregroundOnBackground, + outline: _appColors.outline, + error: _appColors.danger, + onError: _appColors.foregroundOnDanger, + ); + + final _appColorScheme = switch (brightness) { + Brightness.light => _lightColorScheme, + Brightness.dark => _darkColorScheme, + }; + + return ThemeData( + brightness: brightness, + scaffoldBackgroundColor: _appColors.background, + splashFactory: InkRipple.splashFactory, + fontFamily: AppFonts.activeFontFamily, + colorScheme: _appColorScheme, + highlightColor: Colors.transparent, + splashColor: _appColors.splashColor, + disabledColor: _appColors.primary.lowOpacity(), + appBarTheme: AppBarTheme( + color: _appColors.appBarBackground, + foregroundColor: _appColors.foregroundOnAppBar, + ), + extensions: [ + _appColors, + _appTextStyles, + ], + ); + } } diff --git a/lib/presentation/resources/app_ui_constants.dart b/lib/presentation/resources/app_ui_constants.dart new file mode 100644 index 0000000..f1dbd8b --- /dev/null +++ b/lib/presentation/resources/app_ui_constants.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +/// Represents a collection of constants specific to the app's user interface (UI). +/// +/// This class contains non-sensitive information and is designed to maintain consistency +/// across the app's UI elements. +sealed class AppUiConstants { + AppUiConstants._(); + + // Animations + static const animationDuration = Duration(milliseconds: 250); + + // Curves + static const transitionCurve = Curves.fastEaseInToSlowEaseOut; + + // Paddings + static const defaultScreenHorizontalPadding = + EdgeInsets.symmetric(horizontal: 24.0); + + static const defaultSmallButtonContentPadding = EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ); + + static const defaultButtonContentPadding = EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 18.0, + ); + + // Text styles + static const defaultSmallButtonTextStyle = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ); + + static const defaultButtonTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ); + + // Border radius + /// Assign border radius value if you want to have radius on buttons, textfields, etc. + /// Don't assign any value if you want to have the default full rounded radius (Stadium border). + /// Example — + /// ```dart + /// static double? defaultBorderRadius; + /// ```` + static double? defaultBorderRadius = 12.0; +} diff --git a/lib/presentation/widgets/app_bar/top_app_bar.dart b/lib/presentation/widgets/app_bar/top_app_bar.dart new file mode 100644 index 0000000..cb39eb7 --- /dev/null +++ b/lib/presentation/widgets/app_bar/top_app_bar.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/app_colors.dart'; +import 'package:flutter_template/presentation/widgets/utilities/top_app_bar_config.dart'; +import 'package:flutter_template/presentation/widgets/text/app_text.dart'; + +// TODO: Replace the name with Apps-specific naming (XyzAppBar) +class TopAppBar extends StatelessWidget implements PreferredSizeWidget { + const TopAppBar({ + super.key, + this.label, + this.actions, + this.automaticallyImplyLeading = true, + this.centerTitle = true, + }); + + final String? label; + final List? actions; + final bool automaticallyImplyLeading; + final bool centerTitle; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return TopAppBarConfig( + child: AppBar( + title: label == null + ? null + : AppText.appBarTitle( + label!, + color: context.colors.foregroundOnAppBar, + ), + actions: actions, + automaticallyImplyLeading: automaticallyImplyLeading, + centerTitle: centerTitle, + surfaceTintColor: Colors.transparent, + ), + ); + } +} diff --git a/lib/presentation/widgets/button/app_button.dart b/lib/presentation/widgets/button/app_button.dart new file mode 100644 index 0000000..506f0c9 --- /dev/null +++ b/lib/presentation/widgets/button/app_button.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/extensions/color_extensions.dart'; +import 'package:flutter_template/presentation/resources/app_colors.dart'; +import 'package:flutter_template/presentation/resources/app_ui_constants.dart'; +import 'package:flutter_template/presentation/widgets/loading_indicator/app_loading_indicator.dart'; +import 'package:flutter_template/presentation/widgets/text/app_text.dart'; + +enum _AppButtonType { + primary, + secondary, + outlined, + text, + danger, +} + +class AppButton extends StatelessWidget { + const AppButton._({ + Key? key, + required this.buttonType, + required this.label, + this.onPressed, + this.isLoading = false, + this.isSmall = false, + this.isDisabled = false, + }) : super(key: key); + + factory AppButton.primary({ + required String label, + VoidCallback? onPressed, + bool isLoading = false, + bool isSmall = false, + bool isDisabled = false, + }) { + return AppButton._( + buttonType: _AppButtonType.primary, + label: label, + isLoading: isLoading, + isSmall: isSmall, + isDisabled: isDisabled, + onPressed: onPressed, + ); + } + + factory AppButton.secondary({ + required String label, + VoidCallback? onPressed, + bool isLoading = false, + bool isSmall = false, + bool isDisabled = false, + }) { + return AppButton._( + buttonType: _AppButtonType.secondary, + label: label, + isLoading: isLoading, + isSmall: isSmall, + isDisabled: isDisabled, + onPressed: onPressed, + ); + } + + factory AppButton.outlined({ + required String label, + VoidCallback? onPressed, + bool isLoading = false, + bool isSmall = false, + bool isDisabled = false, + }) { + return AppButton._( + buttonType: _AppButtonType.outlined, + label: label, + isLoading: isLoading, + isSmall: isSmall, + isDisabled: isDisabled, + onPressed: onPressed, + ); + } + + factory AppButton.text({ + required String label, + VoidCallback? onPressed, + bool isLoading = false, + bool isSmall = false, + bool isDisabled = false, + }) { + return AppButton._( + buttonType: _AppButtonType.text, + label: label, + isLoading: isLoading, + isSmall: isSmall, + isDisabled: isDisabled, + onPressed: onPressed, + ); + } + + factory AppButton.destructive({ + required String label, + VoidCallback? onPressed, + bool isLoading = false, + bool isSmall = false, + bool isDisabled = false, + }) { + return AppButton._( + buttonType: _AppButtonType.danger, + label: label, + isLoading: isLoading, + isSmall: isSmall, + isDisabled: isDisabled, + onPressed: onPressed, + ); + } + + final _AppButtonType buttonType; + final String label; + final VoidCallback? onPressed; + final bool isLoading; + final bool isSmall; + final bool isDisabled; + + @override + Widget build(BuildContext context) { + final borderRadius = AppUiConstants.defaultBorderRadius; + + final _colors = context.colors; + + final _padding = isSmall + ? AppUiConstants.defaultSmallButtonContentPadding + : AppUiConstants.defaultButtonContentPadding; + + final _textStyle = isSmall + ? AppUiConstants.defaultSmallButtonTextStyle + : AppUiConstants.defaultButtonTextStyle; + + final shape = borderRadius == null + ? null + : MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppUiConstants.defaultBorderRadius ?? .0, + ), + ), + ); + + final disableOnPressed = isLoading || isDisabled; + + return SizedBox( + width: double.infinity, + child: switch (buttonType) { + _AppButtonType.primary => TextButton( + style: TextButton.styleFrom( + padding: _padding, + textStyle: _textStyle, + backgroundColor: + isDisabled ? _colors.primary.lowOpacity() : _colors.primary, + foregroundColor: _colors.foregroundOnPrimary, + ).copyWith( + shape: shape, + ), + onPressed: disableOnPressed ? null : onPressed, + child: isLoading + ? AppLoadingIndicator.small( + indicatorColor: _colors.foregroundOnPrimary, + ) + : _AppButtonLabel(label), + ), + _AppButtonType.secondary => TextButton( + style: TextButton.styleFrom( + padding: _padding, + textStyle: _textStyle, + backgroundColor: isDisabled + ? _colors.secondary.lowOpacity() + : _colors.secondary, + foregroundColor: _colors.foregroundOnSecondary, + ).copyWith( + shape: shape, + ), + onPressed: disableOnPressed ? null : onPressed, + child: isLoading + ? AppLoadingIndicator.small( + indicatorColor: _colors.foregroundOnSecondary, + ) + : _AppButtonLabel(label), + ), + _AppButtonType.outlined => OutlinedButton( + style: OutlinedButton.styleFrom( + padding: _padding, + textStyle: _textStyle, + ).copyWith( + shape: shape, + ), + onPressed: disableOnPressed ? null : onPressed, + child: isLoading + ? AppLoadingIndicator.small( + indicatorColor: _colors.foregroundOnBackground, + ) + : _AppButtonLabel(label), + ), + _AppButtonType.text => TextButton( + style: TextButton.styleFrom( + padding: _padding, + textStyle: _textStyle, + ).copyWith( + shape: shape, + ), + onPressed: disableOnPressed ? null : onPressed, + child: isLoading + ? AppLoadingIndicator.small( + indicatorColor: _colors.foregroundOnBackground, + ) + : _AppButtonLabel(label), + ), + _AppButtonType.danger => TextButton( + style: TextButton.styleFrom( + padding: _padding, + textStyle: _textStyle, + backgroundColor: + isDisabled ? _colors.danger.lowOpacity() : _colors.danger, + foregroundColor: _colors.foregroundOnDanger, + ).copyWith( + shape: shape, + ), + onPressed: disableOnPressed ? null : onPressed, + child: isLoading + ? AppLoadingIndicator.small( + indicatorColor: _colors.foregroundOnDanger, + ) + : _AppButtonLabel(label), + ), + }, + ); + } +} + +class _AppButtonLabel extends StatelessWidget { + const _AppButtonLabel(this.label); + + final String label; + + @override + Widget build(BuildContext context) { + return AppText.buttonLabel( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ); + } +} diff --git a/lib/presentation/widgets/disable_widget/app_disable_widget.dart b/lib/presentation/widgets/disable_widget/app_disable_widget.dart new file mode 100644 index 0000000..2ea1d53 --- /dev/null +++ b/lib/presentation/widgets/disable_widget/app_disable_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/app_ui_constants.dart'; +import 'package:flutter_template/presentation/resources/resources.dart'; + +class AppDisableWidget extends StatelessWidget { + const AppDisableWidget({ + super.key, + this.disable = true, + this.applyBlackWhiteFilter = true, + this.opacity = 0.66, + this.animate = false, + required this.child, + }); + + final bool disable; + final double opacity; + final bool applyBlackWhiteFilter; + final bool animate; + final Widget child; + + @override + Widget build(BuildContext context) { + Widget widget = IgnorePointer( + ignoring: disable, + child: child, + ); + + if (applyBlackWhiteFilter) { + widget = ColorFiltered( + colorFilter: ColorFilter.mode( + disable ? context.colors.background : Colors.transparent, + disable ? BlendMode.saturation : BlendMode.overlay, + ), + child: widget, + ); + } + + final opacity = disable ? this.opacity : 1.0; + + return animate + ? AnimatedOpacity( + opacity: opacity, + duration: AppUiConstants.animationDuration, + curve: Curves.fastEaseInToSlowEaseOut, + child: widget, + ) + : Opacity( + opacity: opacity, + child: widget, + ); + } +} diff --git a/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart b/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart index 5b3b54a..fec8a98 100644 --- a/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart +++ b/lib/presentation/widgets/loading_indicator/app_loading_indicator.dart @@ -7,10 +7,16 @@ class AppLoadingIndicator extends StatelessWidget { /// Determines both height and width of the [AppLoadingIndicator.small] static const defaultSmallSize = 16.0; + /// Determines the color of the loading indicator. + /// + /// By default, It will follow the primary color of the current theme. + final Color? indicatorColor; + const AppLoadingIndicator({ super.key, this.size = defaultSize, this.strokeWidth = 4.0, + this.indicatorColor, }); /// Determines both height and width of the loader. @@ -19,10 +25,13 @@ class AppLoadingIndicator extends StatelessWidget { /// The width of the material loader stroke. final double strokeWidth; - factory AppLoadingIndicator.small() { - return const AppLoadingIndicator( + factory AppLoadingIndicator.small({ + Color? indicatorColor, + }) { + return AppLoadingIndicator( size: defaultSmallSize, strokeWidth: 2.0, + indicatorColor: indicatorColor, ); } @@ -33,6 +42,7 @@ class AppLoadingIndicator extends StatelessWidget { width: size, child: CircularProgressIndicator.adaptive( strokeWidth: strokeWidth, + backgroundColor: indicatorColor, ), ); } diff --git a/lib/presentation/widgets/spacers/safe_area_spacer.dart b/lib/presentation/widgets/spacers/safe_area_spacer.dart new file mode 100644 index 0000000..e4bae8e --- /dev/null +++ b/lib/presentation/widgets/spacers/safe_area_spacer.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Create safe area space. Useful for the beginning/end of the pages +/// to make sure the "padding" of it is above the safe area. +class SafeAreaSpacer extends StatelessWidget { + const SafeAreaSpacer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final padding = MediaQuery.maybePaddingOf(context); + + if (padding == null) { + return const SizedBox.shrink(); + } + + final insets = padding.bottom; + + return SizedBox(height: insets); + } +} diff --git a/lib/presentation/widgets/text/app_text.dart b/lib/presentation/widgets/text/app_text.dart new file mode 100644 index 0000000..ee26876 --- /dev/null +++ b/lib/presentation/widgets/text/app_text.dart @@ -0,0 +1,381 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/resources/resources.dart'; + +enum _AppTextType { + header1, + header2, + header3, + appBarTitle, + bodySmall, + body, + bodyLarge, + buttonLabel, + underlineText, +} + +class AppText extends StatelessWidget { + const AppText._( + this.text, { + Key? key, + required this.type, + this.color, + this.textAlign, + this.maxLines, + this.overflow, + this.letterSpacing, + this.fontFamily, + this.lineHeight, + this.fontWeight, + this.fontSize, + this.enableAutoTextSize = false, + }) : super(key: key); + + factory AppText.header1( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.header1, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.header2( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.header2, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.header3( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.header3, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.appBarTitle( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.appBarTitle, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.bodySmall( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.bodySmall, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.body( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.body, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.bodyLarge( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.bodyLarge, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.buttonLabel( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.buttonLabel, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.underlineText( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.underlineText, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + enableAutoTextSize: enableAutoTextSize, + ); + } + + factory AppText.custom( + String text, { + Key? key, + Color? color, + TextAlign? textAlign, + int? maxLines, + TextOverflow? overflow, + double? letterSpacing, + String? fontFamily, + double? lineHeight, + FontWeight? fontWeight, + double? fontSize, + bool enableAutoTextSize = false, + }) { + return AppText._( + text, + type: _AppTextType.body, + key: key, + color: color, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + letterSpacing: letterSpacing, + fontFamily: fontFamily, + lineHeight: lineHeight, + fontWeight: fontWeight, + fontSize: fontSize, + enableAutoTextSize: enableAutoTextSize, + ); + } + + final String text; + final _AppTextType type; + final Color? color; + final TextAlign? textAlign; + final int? maxLines; + final TextOverflow? overflow; + final double? letterSpacing; + final String? fontFamily; + final double? lineHeight; + final FontWeight? fontWeight; + final double? fontSize; + final bool enableAutoTextSize; + + @override + Widget build(BuildContext context) { + final _appTextStyles = context.textStyles; + + final textStyle = switch (type) { + _AppTextType.header1 => _appTextStyles.header1, + _AppTextType.header2 => _appTextStyles.header2, + _AppTextType.header3 => _appTextStyles.header3, + _AppTextType.appBarTitle => _appTextStyles.appBarTitle, + _AppTextType.bodySmall => _appTextStyles.bodySmall, + _AppTextType.body => _appTextStyles.body, + _AppTextType.bodyLarge => _appTextStyles.bodyLarge, + _AppTextType.buttonLabel => _appTextStyles.buttonLabel, + _AppTextType.underlineText => _appTextStyles.underlineText, + }; + + final customTextStyle = textStyle.copyWith( + color: color, + letterSpacing: letterSpacing, + fontSize: fontSize, + fontWeight: fontWeight, + ); + + if (enableAutoTextSize) { + return AutoSizeText( + text, + style: customTextStyle, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + minFontSize: 6, + ); + } + + return Text( + text, + style: customTextStyle, + textAlign: textAlign, + maxLines: maxLines, + overflow: overflow, + ); + } +} diff --git a/lib/presentation/widgets/utilities/focus_scope_dismissible.dart b/lib/presentation/widgets/utilities/app_screen_config.dart similarity index 53% rename from lib/presentation/widgets/utilities/focus_scope_dismissible.dart rename to lib/presentation/widgets/utilities/app_screen_config.dart index e3eab20..6d49dc5 100644 --- a/lib/presentation/widgets/utilities/focus_scope_dismissible.dart +++ b/lib/presentation/widgets/utilities/app_screen_config.dart @@ -1,34 +1,38 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -/// A widget for dismissing the keyboard on tap outside. +/// A configuration widget designed to manage actions when tapping on the scaffold, such as dismissing the keyboard. /// -/// Wrap a widget that contain text inputs with this widget -/// to enable tap-outside-to-dismiss-keyboard behaviour. +/// Wrap a widget containing text inputs with this widget to enable tap-outside-to-dismiss-keyboard behavior. /// -/// So instead of this: +/// Example Usage: +/// ```dart +/// AppScreenConfig( +/// child: MyScreen(), +/// ) +/// ``` /// +/// Instead of manually handling tap events to dismiss the keyboard: /// ```dart /// GestureDetector( /// onTap: () { -/// final focusScope = FocusScope.of(context); -/// if (focusScope.hasFocus) { -/// focusScope.unfocus(); -/// } -/// }, -/// child: MyPage(), +/// final focusScope = FocusScope.of(context); +/// if (focusScope.hasFocus) { +/// focusScope.unfocus(); +/// } +/// }, +/// child: MyScreen(), /// ), /// ``` /// -/// You could do this: -/// +/// You can simplify it using `AppScreenConfig`: /// ```dart -/// BackgroundFocusScopeDismisser( -/// child: MyPage(), +/// AppScreenConfig( +/// child: MyScreen(), /// ), /// ``` -class FocusScopeDismissible extends StatelessWidget { - const FocusScopeDismissible({ +class AppScreenConfig extends StatelessWidget { + const AppScreenConfig({ Key? key, this.excludeFromSemantics = false, this.dragStartBehavior = DragStartBehavior.start, diff --git a/lib/presentation/widgets/utilities/top_app_bar_config.dart b/lib/presentation/widgets/utilities/top_app_bar_config.dart new file mode 100644 index 0000000..64b9d27 --- /dev/null +++ b/lib/presentation/widgets/utilities/top_app_bar_config.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/presentation/extensions/scroll_controller_extensions.dart'; + +/// A configurable widget designed for defining actions triggered by tapping on the [Scaffold]'s top app bar. +/// +/// The primary use case for this widget is to facilitate scrolling to the top of the screen. +/// If the screen is already scrolled to the top, the widget will alternatively unfocus and +/// dismiss the keyboard if it is currently shown. +class TopAppBarConfig extends StatelessWidget { + const TopAppBarConfig({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + final controller = PrimaryScrollController.maybeOf(context); + + if (controller?.position.pixels == 0.0) { + FocusManager.instance.primaryFocus?.unfocus(); + } + + controller?.animateToTop(); + }, + child: child, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index dda55f8..aa6be71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,39 +24,45 @@ dependencies: flutter: sdk: flutter - # UI - cupertino_icons: ^1.0.6 - flutter_svg: ^2.0.9 + # Localization nstack: git: url: https://github.com/nstack-io/flutter-sdk.git ref: v0.5.1 - # Net + # UI + cupertino_icons: ^1.0.6 + flutter_svg: ^2.0.9 + flutter_sticky_header: ^0.6.5 + auto_size_text: ^3.0.0 + + # Data dio: ^5.4.0 + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 # Utility device_info_plus: ^9.1.1 package_info_plus: ^5.0.1 # Persistence + flutter_secure_storage: ^9.0.0 shared_preferences: ^2.2.2 - + # Architecture - freezed_annotation: ^2.4.1 - json_annotation: ^4.8.1 - injectable: ^2.3.2 get_it: ^7.6.6 + injectable: ^2.3.2 auto_route: ^7.8.4 flutter_bloc: ^8.1.3 dev_dependencies: flutter_test: sdk: flutter + build_runner: ^2.4.7 - injectable_generator: ^2.4.1 freezed: ^2.4.6 json_serializable: ^6.7.1 + injectable_generator: ^2.4.1 auto_route_generator: ^7.3.2 monstarlab_lints: ^1.0.2 From 7fc463b181694d7d7d4d9fef011e7eb5a2abe90c Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Wed, 7 Feb 2024 17:06:29 +0600 Subject: [PATCH 07/19] feat: Integrated theme tailor (#141) --- lib/data/interceptor/auth_interceptor.dart | 2 +- lib/injection/dependencies.dart | 2 +- lib/presentation/app.dart | 4 +- .../extensions/presentation_extensions.dart | 2 +- .../features/profile/cubit/profile_cubit.dart | 8 +- .../features/profile/ui/profile_body.dart | 2 +- lib/presentation/resources/app_colors.dart | 165 ++------------- .../resources/app_colors.tailor.dart | 188 ++++++++++++++++++ .../resources/app_text_styles.dart | 63 ++---- .../resources/app_text_styles.tailor.dart | 110 ++++++++++ lib/presentation/resources/resources.dart | 2 +- .../widgets/app_bar/top_app_bar.dart | 2 +- pubspec.yaml | 4 +- 13 files changed, 353 insertions(+), 201 deletions(-) create mode 100644 lib/presentation/resources/app_colors.tailor.dart create mode 100644 lib/presentation/resources/app_text_styles.tailor.dart diff --git a/lib/data/interceptor/auth_interceptor.dart b/lib/data/interceptor/auth_interceptor.dart index 8b63278..0f03f9f 100644 --- a/lib/data/interceptor/auth_interceptor.dart +++ b/lib/data/interceptor/auth_interceptor.dart @@ -4,9 +4,9 @@ import 'package:flutter_template/data/interceptor/meta_interceptor.dart'; import 'package:flutter_template/data/model/auth/auth_tokens.dart'; import 'package:flutter_template/data/preferences/auth_preferences.dart'; import 'package:flutter_template/data/response_objects/response_error.dart'; +import 'package:flutter_template/data/response_objects/tokens_response.dart'; import 'package:flutter_template/data/services/http_client/dio_http_client.dart'; import 'package:flutter_template/data/services/http_client/http_client.dart'; -import 'package:flutter_template/data/response_objects/tokens_response.dart'; import 'package:flutter_template/domain/preferences/user_preferences.dart'; class AuthInterceptor extends InterceptorsWrapper { diff --git a/lib/injection/dependencies.dart b/lib/injection/dependencies.dart index b8e045c..cd2361c 100644 --- a/lib/injection/dependencies.dart +++ b/lib/injection/dependencies.dart @@ -5,7 +5,7 @@ import 'package:flutter_template/presentation/routes/router.dart'; class DependencyManager { static Future inject(AppFlavor flavor) async { injector.registerLazySingleton(() => flavor); - injector.registerLazySingleton(() => AppRouter()); + injector.registerLazySingleton(AppRouter.new); await configureDependencies(); } diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index 51957e9..1b6f5c1 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -20,7 +20,9 @@ class App extends StatelessWidget { themeMode: ThemeMode.system, title: 'Project Name', builder: (c, widget) { - if (widget == null) return const SizedBox(); + if (widget == null) { + return const SizedBox(); + } return NStackWidget( child: widget, diff --git a/lib/presentation/extensions/presentation_extensions.dart b/lib/presentation/extensions/presentation_extensions.dart index 6e8590c..314a562 100644 --- a/lib/presentation/extensions/presentation_extensions.dart +++ b/lib/presentation/extensions/presentation_extensions.dart @@ -1,4 +1,4 @@ -// A set of extensions that can be used on the presentation layer. +/// A set of extensions that can be used on the presentation layer. library presentation_extensions; export 'color_extensions.dart'; diff --git a/lib/presentation/features/profile/cubit/profile_cubit.dart b/lib/presentation/features/profile/cubit/profile_cubit.dart index bfd5e19..e71c082 100644 --- a/lib/presentation/features/profile/cubit/profile_cubit.dart +++ b/lib/presentation/features/profile/cubit/profile_cubit.dart @@ -24,7 +24,9 @@ final class ProfileCubit extends Cubit { try { final profileName = await _getProfileUseCase(); - if (isClosed) return; + if (isClosed) { + return; + } return emit( state.copyWith( @@ -35,7 +37,9 @@ final class ProfileCubit extends Cubit { } catch (e) { final responseError = ResponseError.from(e); - if (isClosed) return; + if (isClosed) { + return; + } return emit( state.copyWith( diff --git a/lib/presentation/features/profile/ui/profile_body.dart b/lib/presentation/features/profile/ui/profile_body.dart index 61ae055..1c2ad52 100644 --- a/lib/presentation/features/profile/ui/profile_body.dart +++ b/lib/presentation/features/profile/ui/profile_body.dart @@ -27,7 +27,7 @@ class ProfileBody extends StatelessWidget { Padding( padding: const EdgeInsets.all(32.0), child: status.maybeWhen( - loading: () => AppLoadingIndicator.small(), + loading: AppLoadingIndicator.small, success: () => AppText.body( name, textAlign: TextAlign.center, diff --git a/lib/presentation/resources/app_colors.dart b/lib/presentation/resources/app_colors.dart index 8e651ac..9074b16 100644 --- a/lib/presentation/resources/app_colors.dart +++ b/lib/presentation/resources/app_colors.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/resources/app_color_palette.dart'; +import 'package:theme_tailor_annotation/theme_tailor_annotation.dart'; -class AppColors extends ThemeExtension { +part 'app_colors.tailor.dart'; + +@TailorMixin() +class AppColors extends ThemeExtension with _$AppColorsTailorMixin { static Color get black => AppColorPalette.black; static Color get white => AppColorPalette.white; @@ -26,24 +30,41 @@ class AppColors extends ThemeExtension { }); // Core colors + @override final Color primary; + @override final Color primaryVariant; + @override final Color secondary; + @override final Color secondaryVariant; + @override final Color background; + @override final Color appBarBackground; + @override final Color danger; + @override final Color foregroundOnBackground; + @override final Color foregroundLightOnBackground; + @override final Color foregroundOnPrimary; + @override final Color foregroundOnSecondary; + @override final Color foregroundOnAppBar; + @override final Color foregroundOnDanger; + @override final Color outline; + @override final Color transparant; // Other colors + @override final Color splashColor; + @override final Color disabledColor; factory AppColors.fromBrightness(Brightness brightness) => @@ -95,148 +116,6 @@ class AppColors extends ThemeExtension { disabledColor: AppColorPalette.silverPolish, ); } - - @override - ThemeExtension copyWith({ - Color? primary, - Color? primaryVariant, - Color? secondary, - Color? secondaryVariant, - Color? background, - Color? appBarBackground, - Color? danger, - Color? foregroundOnBackground, - Color? foregroundLightOnBackground, - Color? foregroundOnPrimary, - Color? foregroundOnSecondary, - Color? foregroundOnAppBar, - Color? foregroundOnDanger, - Color? transparant, - Color? outline, - Color? splashColor, - Color? disabledColor, - }) { - return AppColors( - primary: primary ?? this.primary, - primaryVariant: primaryVariant ?? this.primaryVariant, - secondary: secondary ?? this.secondary, - secondaryVariant: secondaryVariant ?? this.secondaryVariant, - background: background ?? this.background, - appBarBackground: appBarBackground ?? this.appBarBackground, - danger: danger ?? this.danger, - foregroundOnBackground: - foregroundOnBackground ?? this.foregroundOnBackground, - foregroundLightOnBackground: - foregroundLightOnBackground ?? this.foregroundLightOnBackground, - foregroundOnPrimary: foregroundOnPrimary ?? this.foregroundOnPrimary, - foregroundOnSecondary: - foregroundOnSecondary ?? this.foregroundOnSecondary, - foregroundOnAppBar: foregroundOnAppBar ?? this.foregroundOnAppBar, - foregroundOnDanger: foregroundOnDanger ?? this.foregroundOnDanger, - outline: outline ?? this.outline, - transparant: transparant ?? this.transparant, - splashColor: splashColor ?? this.splashColor, - disabledColor: disabledColor ?? this.disabledColor, - ); - } - - @override - ThemeExtension lerp( - covariant ThemeExtension? other, - double t, - ) { - if (other is! AppColors) { - return this; - } - - return AppColors( - primary: Color.lerp( - primary, - other.primary, - t, - )!, - primaryVariant: Color.lerp( - primaryVariant, - other.primaryVariant, - t, - )!, - secondary: Color.lerp( - secondary, - other.secondary, - t, - )!, - secondaryVariant: Color.lerp( - secondaryVariant, - other.secondaryVariant, - t, - )!, - background: Color.lerp( - background, - other.background, - t, - )!, - appBarBackground: Color.lerp( - appBarBackground, - other.appBarBackground, - t, - )!, - danger: Color.lerp( - danger, - other.danger, - t, - )!, - foregroundOnBackground: Color.lerp( - foregroundOnBackground, - other.foregroundOnBackground, - t, - )!, - foregroundLightOnBackground: Color.lerp( - foregroundLightOnBackground, - other.foregroundLightOnBackground, - t, - )!, - foregroundOnPrimary: Color.lerp( - foregroundOnPrimary, - other.foregroundOnPrimary, - t, - )!, - foregroundOnSecondary: Color.lerp( - foregroundOnSecondary, - other.foregroundOnSecondary, - t, - )!, - foregroundOnAppBar: Color.lerp( - foregroundOnAppBar, - other.foregroundOnAppBar, - t, - )!, - foregroundOnDanger: Color.lerp( - foregroundOnDanger, - other.foregroundOnDanger, - t, - )!, - outline: Color.lerp( - outline, - other.outline, - t, - )!, - transparant: Color.lerp( - transparant, - other.transparant, - t, - )!, - splashColor: Color.lerp( - splashColor, - other.splashColor, - t, - )!, - disabledColor: Color.lerp( - disabledColor, - other.disabledColor, - t, - )!, - ); - } } extension AppColorsExtension on BuildContext { diff --git a/lib/presentation/resources/app_colors.tailor.dart b/lib/presentation/resources/app_colors.tailor.dart new file mode 100644 index 0000000..30b827f --- /dev/null +++ b/lib/presentation/resources/app_colors.tailor.dart @@ -0,0 +1,188 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element, unnecessary_cast + +part of 'app_colors.dart'; + +// ************************************************************************** +// TailorAnnotationsGenerator +// ************************************************************************** + +mixin _$AppColorsTailorMixin on ThemeExtension { + Color get primary; + Color get primaryVariant; + Color get secondary; + Color get secondaryVariant; + Color get background; + Color get appBarBackground; + Color get danger; + Color get foregroundOnBackground; + Color get foregroundLightOnBackground; + Color get foregroundOnPrimary; + Color get foregroundOnSecondary; + Color get foregroundOnAppBar; + Color get foregroundOnDanger; + Color get outline; + Color get transparant; + Color get splashColor; + Color get disabledColor; + + @override + AppColors copyWith({ + Color? primary, + Color? primaryVariant, + Color? secondary, + Color? secondaryVariant, + Color? background, + Color? appBarBackground, + Color? danger, + Color? foregroundOnBackground, + Color? foregroundLightOnBackground, + Color? foregroundOnPrimary, + Color? foregroundOnSecondary, + Color? foregroundOnAppBar, + Color? foregroundOnDanger, + Color? outline, + Color? transparant, + Color? splashColor, + Color? disabledColor, + }) { + return AppColors( + primary: primary ?? this.primary, + primaryVariant: primaryVariant ?? this.primaryVariant, + secondary: secondary ?? this.secondary, + secondaryVariant: secondaryVariant ?? this.secondaryVariant, + background: background ?? this.background, + appBarBackground: appBarBackground ?? this.appBarBackground, + danger: danger ?? this.danger, + foregroundOnBackground: + foregroundOnBackground ?? this.foregroundOnBackground, + foregroundLightOnBackground: + foregroundLightOnBackground ?? this.foregroundLightOnBackground, + foregroundOnPrimary: foregroundOnPrimary ?? this.foregroundOnPrimary, + foregroundOnSecondary: + foregroundOnSecondary ?? this.foregroundOnSecondary, + foregroundOnAppBar: foregroundOnAppBar ?? this.foregroundOnAppBar, + foregroundOnDanger: foregroundOnDanger ?? this.foregroundOnDanger, + outline: outline ?? this.outline, + transparant: transparant ?? this.transparant, + splashColor: splashColor ?? this.splashColor, + disabledColor: disabledColor ?? this.disabledColor, + ); + } + + @override + AppColors lerp(covariant ThemeExtension? other, double t) { + if (other is! AppColors) return this as AppColors; + return AppColors( + primary: Color.lerp(primary, other.primary, t)!, + primaryVariant: Color.lerp(primaryVariant, other.primaryVariant, t)!, + secondary: Color.lerp(secondary, other.secondary, t)!, + secondaryVariant: + Color.lerp(secondaryVariant, other.secondaryVariant, t)!, + background: Color.lerp(background, other.background, t)!, + appBarBackground: + Color.lerp(appBarBackground, other.appBarBackground, t)!, + danger: Color.lerp(danger, other.danger, t)!, + foregroundOnBackground: + Color.lerp(foregroundOnBackground, other.foregroundOnBackground, t)!, + foregroundLightOnBackground: Color.lerp( + foregroundLightOnBackground, other.foregroundLightOnBackground, t)!, + foregroundOnPrimary: + Color.lerp(foregroundOnPrimary, other.foregroundOnPrimary, t)!, + foregroundOnSecondary: + Color.lerp(foregroundOnSecondary, other.foregroundOnSecondary, t)!, + foregroundOnAppBar: + Color.lerp(foregroundOnAppBar, other.foregroundOnAppBar, t)!, + foregroundOnDanger: + Color.lerp(foregroundOnDanger, other.foregroundOnDanger, t)!, + outline: Color.lerp(outline, other.outline, t)!, + transparant: Color.lerp(transparant, other.transparant, t)!, + splashColor: Color.lerp(splashColor, other.splashColor, t)!, + disabledColor: Color.lerp(disabledColor, other.disabledColor, t)!, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is AppColors && + const DeepCollectionEquality().equals(primary, other.primary) && + const DeepCollectionEquality() + .equals(primaryVariant, other.primaryVariant) && + const DeepCollectionEquality().equals(secondary, other.secondary) && + const DeepCollectionEquality() + .equals(secondaryVariant, other.secondaryVariant) && + const DeepCollectionEquality() + .equals(background, other.background) && + const DeepCollectionEquality() + .equals(appBarBackground, other.appBarBackground) && + const DeepCollectionEquality().equals(danger, other.danger) && + const DeepCollectionEquality() + .equals(foregroundOnBackground, other.foregroundOnBackground) && + const DeepCollectionEquality().equals(foregroundLightOnBackground, + other.foregroundLightOnBackground) && + const DeepCollectionEquality() + .equals(foregroundOnPrimary, other.foregroundOnPrimary) && + const DeepCollectionEquality() + .equals(foregroundOnSecondary, other.foregroundOnSecondary) && + const DeepCollectionEquality() + .equals(foregroundOnAppBar, other.foregroundOnAppBar) && + const DeepCollectionEquality() + .equals(foregroundOnDanger, other.foregroundOnDanger) && + const DeepCollectionEquality().equals(outline, other.outline) && + const DeepCollectionEquality() + .equals(transparant, other.transparant) && + const DeepCollectionEquality() + .equals(splashColor, other.splashColor) && + const DeepCollectionEquality() + .equals(disabledColor, other.disabledColor)); + } + + @override + int get hashCode { + return Object.hash( + runtimeType.hashCode, + const DeepCollectionEquality().hash(primary), + const DeepCollectionEquality().hash(primaryVariant), + const DeepCollectionEquality().hash(secondary), + const DeepCollectionEquality().hash(secondaryVariant), + const DeepCollectionEquality().hash(background), + const DeepCollectionEquality().hash(appBarBackground), + const DeepCollectionEquality().hash(danger), + const DeepCollectionEquality().hash(foregroundOnBackground), + const DeepCollectionEquality().hash(foregroundLightOnBackground), + const DeepCollectionEquality().hash(foregroundOnPrimary), + const DeepCollectionEquality().hash(foregroundOnSecondary), + const DeepCollectionEquality().hash(foregroundOnAppBar), + const DeepCollectionEquality().hash(foregroundOnDanger), + const DeepCollectionEquality().hash(outline), + const DeepCollectionEquality().hash(transparant), + const DeepCollectionEquality().hash(splashColor), + const DeepCollectionEquality().hash(disabledColor), + ); + } +} + +extension AppColorsBuildContextProps on BuildContext { + AppColors get appColors => Theme.of(this).extension()!; + Color get primary => appColors.primary; + Color get primaryVariant => appColors.primaryVariant; + Color get secondary => appColors.secondary; + Color get secondaryVariant => appColors.secondaryVariant; + Color get background => appColors.background; + Color get appBarBackground => appColors.appBarBackground; + Color get danger => appColors.danger; + Color get foregroundOnBackground => appColors.foregroundOnBackground; + Color get foregroundLightOnBackground => + appColors.foregroundLightOnBackground; + Color get foregroundOnPrimary => appColors.foregroundOnPrimary; + Color get foregroundOnSecondary => appColors.foregroundOnSecondary; + Color get foregroundOnAppBar => appColors.foregroundOnAppBar; + Color get foregroundOnDanger => appColors.foregroundOnDanger; + Color get outline => appColors.outline; + Color get transparant => appColors.transparant; + Color get splashColor => appColors.splashColor; + Color get disabledColor => appColors.disabledColor; +} diff --git a/lib/presentation/resources/app_text_styles.dart b/lib/presentation/resources/app_text_styles.dart index 57da292..2e0b0d6 100644 --- a/lib/presentation/resources/app_text_styles.dart +++ b/lib/presentation/resources/app_text_styles.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/resources/app_colors.dart'; import 'package:flutter_template/presentation/resources/app_fonts.dart'; +import 'package:theme_tailor_annotation/theme_tailor_annotation.dart'; -class AppTextStyles extends ThemeExtension { +part 'app_text_styles.tailor.dart'; + +@TailorMixin() +class AppTextStyles extends ThemeExtension + with _$AppTextStylesTailorMixin { AppTextStyles({ required this.header1, required this.header2, @@ -73,62 +78,24 @@ class AppTextStyles extends ThemeExtension { ); } + @override final TextStyle header1; + @override final TextStyle header2; + @override final TextStyle header3; + @override final TextStyle appBarTitle; + @override final TextStyle bodySmall; + @override final TextStyle body; + @override final TextStyle bodyLarge; - final TextStyle buttonLabel; - final TextStyle underlineText; - @override - ThemeExtension copyWith({ - TextStyle? header1, - TextStyle? header2, - TextStyle? header3, - TextStyle? appBarTitle, - TextStyle? bodySmall, - TextStyle? bodyLarge, - TextStyle? body, - TextStyle? buttonLabel, - TextStyle? underlineText, - }) { - return AppTextStyles( - header1: header1 ?? this.header1, - header2: header2 ?? this.header2, - header3: header3 ?? this.header3, - appBarTitle: appBarTitle ?? this.appBarTitle, - bodySmall: bodySmall ?? this.bodySmall, - body: body ?? this.body, - bodyLarge: bodyLarge ?? this.bodyLarge, - buttonLabel: buttonLabel ?? this.buttonLabel, - underlineText: underlineText ?? this.underlineText, - ); - } - + final TextStyle buttonLabel; @override - ThemeExtension lerp( - covariant ThemeExtension? other, - double t, - ) { - if (other is! AppTextStyles) { - return this; - } - - return AppTextStyles( - header1: TextStyle.lerp(header1, other.header1, t)!, - header2: TextStyle.lerp(header2, other.header2, t)!, - header3: TextStyle.lerp(header3, other.header3, t)!, - appBarTitle: TextStyle.lerp(appBarTitle, other.appBarTitle, t)!, - bodySmall: TextStyle.lerp(bodySmall, other.bodySmall, t)!, - body: TextStyle.lerp(body, other.body, t)!, - bodyLarge: TextStyle.lerp(bodyLarge, other.bodyLarge, t)!, - buttonLabel: TextStyle.lerp(buttonLabel, other.buttonLabel, t)!, - underlineText: TextStyle.lerp(underlineText, other.underlineText, t)!, - ); - } + final TextStyle underlineText; } extension AppTextStylesExtension on BuildContext { diff --git a/lib/presentation/resources/app_text_styles.tailor.dart b/lib/presentation/resources/app_text_styles.tailor.dart new file mode 100644 index 0000000..f550cf4 --- /dev/null +++ b/lib/presentation/resources/app_text_styles.tailor.dart @@ -0,0 +1,110 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element, unnecessary_cast + +part of 'app_text_styles.dart'; + +// ************************************************************************** +// TailorAnnotationsGenerator +// ************************************************************************** + +mixin _$AppTextStylesTailorMixin on ThemeExtension { + TextStyle get header1; + TextStyle get header2; + TextStyle get header3; + TextStyle get appBarTitle; + TextStyle get bodySmall; + TextStyle get body; + TextStyle get bodyLarge; + TextStyle get buttonLabel; + TextStyle get underlineText; + + @override + AppTextStyles copyWith({ + TextStyle? header1, + TextStyle? header2, + TextStyle? header3, + TextStyle? appBarTitle, + TextStyle? bodySmall, + TextStyle? body, + TextStyle? bodyLarge, + TextStyle? buttonLabel, + TextStyle? underlineText, + }) { + return AppTextStyles( + header1: header1 ?? this.header1, + header2: header2 ?? this.header2, + header3: header3 ?? this.header3, + appBarTitle: appBarTitle ?? this.appBarTitle, + bodySmall: bodySmall ?? this.bodySmall, + body: body ?? this.body, + bodyLarge: bodyLarge ?? this.bodyLarge, + buttonLabel: buttonLabel ?? this.buttonLabel, + underlineText: underlineText ?? this.underlineText, + ); + } + + @override + AppTextStyles lerp(covariant ThemeExtension? other, double t) { + if (other is! AppTextStyles) return this as AppTextStyles; + return AppTextStyles( + header1: TextStyle.lerp(header1, other.header1, t)!, + header2: TextStyle.lerp(header2, other.header2, t)!, + header3: TextStyle.lerp(header3, other.header3, t)!, + appBarTitle: TextStyle.lerp(appBarTitle, other.appBarTitle, t)!, + bodySmall: TextStyle.lerp(bodySmall, other.bodySmall, t)!, + body: TextStyle.lerp(body, other.body, t)!, + bodyLarge: TextStyle.lerp(bodyLarge, other.bodyLarge, t)!, + buttonLabel: TextStyle.lerp(buttonLabel, other.buttonLabel, t)!, + underlineText: TextStyle.lerp(underlineText, other.underlineText, t)!, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is AppTextStyles && + const DeepCollectionEquality().equals(header1, other.header1) && + const DeepCollectionEquality().equals(header2, other.header2) && + const DeepCollectionEquality().equals(header3, other.header3) && + const DeepCollectionEquality() + .equals(appBarTitle, other.appBarTitle) && + const DeepCollectionEquality().equals(bodySmall, other.bodySmall) && + const DeepCollectionEquality().equals(body, other.body) && + const DeepCollectionEquality().equals(bodyLarge, other.bodyLarge) && + const DeepCollectionEquality() + .equals(buttonLabel, other.buttonLabel) && + const DeepCollectionEquality() + .equals(underlineText, other.underlineText)); + } + + @override + int get hashCode { + return Object.hash( + runtimeType.hashCode, + const DeepCollectionEquality().hash(header1), + const DeepCollectionEquality().hash(header2), + const DeepCollectionEquality().hash(header3), + const DeepCollectionEquality().hash(appBarTitle), + const DeepCollectionEquality().hash(bodySmall), + const DeepCollectionEquality().hash(body), + const DeepCollectionEquality().hash(bodyLarge), + const DeepCollectionEquality().hash(buttonLabel), + const DeepCollectionEquality().hash(underlineText), + ); + } +} + +extension AppTextStylesBuildContextProps on BuildContext { + AppTextStyles get appTextStyles => Theme.of(this).extension()!; + TextStyle get header1 => appTextStyles.header1; + TextStyle get header2 => appTextStyles.header2; + TextStyle get header3 => appTextStyles.header3; + TextStyle get appBarTitle => appTextStyles.appBarTitle; + TextStyle get bodySmall => appTextStyles.bodySmall; + TextStyle get body => appTextStyles.body; + TextStyle get bodyLarge => appTextStyles.bodyLarge; + TextStyle get buttonLabel => appTextStyles.buttonLabel; + TextStyle get underlineText => appTextStyles.underlineText; +} diff --git a/lib/presentation/resources/resources.dart b/lib/presentation/resources/resources.dart index 1c9be79..41cc261 100644 --- a/lib/presentation/resources/resources.dart +++ b/lib/presentation/resources/resources.dart @@ -1,6 +1,6 @@ export 'package:flutter_template/presentation/resources/app_colors.dart'; +export 'package:flutter_template/presentation/resources/app_fonts.dart'; export 'package:flutter_template/presentation/resources/app_icons.dart'; export 'package:flutter_template/presentation/resources/app_images.dart'; export 'package:flutter_template/presentation/resources/app_text_styles.dart'; -export 'package:flutter_template/presentation/resources/app_fonts.dart'; export 'package:flutter_template/presentation/resources/app_theme.dart'; diff --git a/lib/presentation/widgets/app_bar/top_app_bar.dart b/lib/presentation/widgets/app_bar/top_app_bar.dart index cb39eb7..9939ac6 100644 --- a/lib/presentation/widgets/app_bar/top_app_bar.dart +++ b/lib/presentation/widgets/app_bar/top_app_bar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_template/presentation/resources/app_colors.dart'; -import 'package:flutter_template/presentation/widgets/utilities/top_app_bar_config.dart'; import 'package:flutter_template/presentation/widgets/text/app_text.dart'; +import 'package:flutter_template/presentation/widgets/utilities/top_app_bar_config.dart'; // TODO: Replace the name with Apps-specific naming (XyzAppBar) class TopAppBar extends StatelessWidget implements PreferredSizeWidget { diff --git a/pubspec.yaml b/pubspec.yaml index aa6be71..4edd242 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: flutter_svg: ^2.0.9 flutter_sticky_header: ^0.6.5 auto_size_text: ^3.0.0 + theme_tailor_annotation: ^2.1.0 # Data dio: ^5.4.0 @@ -60,13 +61,14 @@ dev_dependencies: sdk: flutter build_runner: ^2.4.7 + theme_tailor: ^2.1.0 freezed: ^2.4.6 json_serializable: ^6.7.1 injectable_generator: ^2.4.1 auto_route_generator: ^7.3.2 monstarlab_lints: ^1.0.2 - # Simplifed work with assets + # Simplified work with assets flutter_gen_runner: ^5.4.0 flutter_gen: From f3ae5d36ffb75fed474c53f106fed370e34ea955 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 18:48:41 +0000 Subject: [PATCH 08/19] chore(deps): bump theme_tailor_annotation and theme_tailor (#142) --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4edd242..e1b1ecc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: flutter_svg: ^2.0.9 flutter_sticky_header: ^0.6.5 auto_size_text: ^3.0.0 - theme_tailor_annotation: ^2.1.0 + theme_tailor_annotation: ^3.0.1 # Data dio: ^5.4.0 @@ -61,7 +61,7 @@ dev_dependencies: sdk: flutter build_runner: ^2.4.7 - theme_tailor: ^2.1.0 + theme_tailor: ^3.0.1 freezed: ^2.4.6 json_serializable: ^6.7.1 injectable_generator: ^2.4.1 From be73078e9c72d9c78df3185584c6ebc72c7213e8 Mon Sep 17 00:00:00 2001 From: Hassan Saleh <130444909+hassan-saleh-ml@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:24:04 +0200 Subject: [PATCH 09/19] chore: Migrated deprectations after flutter upgrade (#148) --- lib/presentation/resources/app_theme.dart | 8 ++++---- lib/presentation/widgets/button/app_button.dart | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/presentation/resources/app_theme.dart b/lib/presentation/resources/app_theme.dart index 4f0ad44..1cbc88b 100644 --- a/lib/presentation/resources/app_theme.dart +++ b/lib/presentation/resources/app_theme.dart @@ -13,10 +13,10 @@ sealed class AppTheme { brightness: brightness, primary: _appColors.primary, onPrimary: _appColors.foregroundOnPrimary, - background: _appColors.primary, + surface: _appColors.primary, secondary: _appColors.secondary, onSecondary: _appColors.foregroundOnSecondary, - onBackground: _appColors.foregroundOnBackground, + onSurface: _appColors.foregroundOnBackground, outline: _appColors.outline, error: _appColors.danger, onError: _appColors.foregroundOnDanger, @@ -28,8 +28,8 @@ sealed class AppTheme { onPrimary: _appColors.foregroundOnPrimary, secondary: _appColors.secondary, onSecondary: _appColors.foregroundOnSecondary, - background: _appColors.background, - onBackground: _appColors.foregroundOnBackground, + surface: _appColors.background, + onSurface: _appColors.foregroundOnBackground, outline: _appColors.outline, error: _appColors.danger, onError: _appColors.foregroundOnDanger, diff --git a/lib/presentation/widgets/button/app_button.dart b/lib/presentation/widgets/button/app_button.dart index 506f0c9..8fa5c26 100644 --- a/lib/presentation/widgets/button/app_button.dart +++ b/lib/presentation/widgets/button/app_button.dart @@ -132,7 +132,7 @@ class AppButton extends StatelessWidget { final shape = borderRadius == null ? null - : MaterialStateProperty.all( + : WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular( AppUiConstants.defaultBorderRadius ?? .0, From f42bd7f25c0f47096a6ea011a0e7b2089b04c5a0 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Fri, 14 Jun 2024 16:02:51 +0300 Subject: [PATCH 10/19] chore: Add recommended VS Code extensions (#147) --- .vscode/extensions.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5fa9c14 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "dart-code.dart-code", + "dart-code.flutter", + "usernamehw.errorlens", + "felixangelov.mason", + "felixangelov.bloc", + "nivisi.dart-build-runner-tools", + "tenraneko.pubspec-dependency-opener", + "jeroen-meijer.pubspec-assist", + "davidanson.vscode-markdownlint" + ] +} From 540a327b8260861420c25a29f134d541415b32db Mon Sep 17 00:00:00 2001 From: Utpal Barman Date: Fri, 4 Oct 2024 21:16:01 +0600 Subject: [PATCH 11/19] chore: Migrated to android gradle plugin (#145) --- .metadata | 24 +++- .run/development_debug.run.xml | 6 + .run/development_release.run.xml | 7 ++ .run/production_debug.run.xml | 6 - .run/production_release.run.xml | 7 -- .run/staging_debug.run.xml | 2 +- .run/staging_release.run.xml | 2 +- .vscode/launch.json | 69 ++++++----- android/app/build.gradle | 111 +++++++++--------- android/app/src/debug/AndroidManifest.xml | 6 +- android/app/src/main/AndroidManifest.xml | 34 ++++-- .../example/flutter_template/MainActivity.kt | 3 +- .../app/src/main/res/values-night/styles.xml | 2 +- android/app/src/main/res/values/styles.xml | 2 +- android/app/src/profile/AndroidManifest.xml | 6 +- android/build.gradle | 39 ++++-- android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- android/settings.gradle | 30 +++-- ios/.gitignore | 2 + ios/Runner.xcodeproj/project.pbxproj | 14 +-- ios/Runner/AppDelegate.swift | 2 +- .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 1418 bytes 36 files changed, 218 insertions(+), 161 deletions(-) create mode 100644 .run/development_debug.run.xml create mode 100644 .run/development_release.run.xml delete mode 100644 .run/production_debug.run.xml delete mode 100644 .run/production_release.run.xml diff --git a/.metadata b/.metadata index cd984dd..1038231 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 - channel: stable + revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: ios + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.run/development_debug.run.xml b/.run/development_debug.run.xml new file mode 100644 index 0000000..c644514 --- /dev/null +++ b/.run/development_debug.run.xml @@ -0,0 +1,6 @@ + + + + diff --git a/.run/development_release.run.xml b/.run/development_release.run.xml new file mode 100644 index 0000000..f67e86e --- /dev/null +++ b/.run/development_release.run.xml @@ -0,0 +1,7 @@ + + + + diff --git a/.run/production_debug.run.xml b/.run/production_debug.run.xml deleted file mode 100644 index 8cce292..0000000 --- a/.run/production_debug.run.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.run/production_release.run.xml b/.run/production_release.run.xml deleted file mode 100644 index 1181e15..0000000 --- a/.run/production_release.run.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.run/staging_debug.run.xml b/.run/staging_debug.run.xml index deb8136..883761f 100644 --- a/.run/staging_debug.run.xml +++ b/.run/staging_debug.run.xml @@ -3,4 +3,4 @@