From 9f347558407282e1f7c4ae57f3813750afe6ac24 Mon Sep 17 00:00:00 2001 From: Erick Ortega Date: Mon, 11 Mar 2024 09:39:55 -0400 Subject: [PATCH 1/3] feat: :sparkles: Custom Tag --- example/ios/Podfile.lock | 4 +- example/lib/main.dart | 323 ++++++------ .../pages/custom_tag/custom_tags_page.dart | 49 ++ packages/avilatek_ui/lib/avilatek_ui.dart | 4 +- .../lib/src/ui/avila_custom_theme.dart | 6 + .../src/ui/custom_tag/custom_tag_style.dart | 81 +++ .../ui/title_wrapper/title_wrapper_style.dart | 18 +- .../lib/src/widgets/custom_tags.dart | 463 +++++++++++------- 8 files changed, 609 insertions(+), 339 deletions(-) create mode 100644 example/lib/pages/custom_tag/custom_tags_page.dart create mode 100644 packages/avilatek_ui/lib/src/ui/custom_tag/custom_tag_style.dart diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a546496..4d19b46 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -65,11 +65,11 @@ SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f PODFILE CHECKSUM: d080214342236ffc72197a414adc2a47f117f9dc -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.2 diff --git a/example/lib/main.dart b/example/lib/main.dart index f3fb14f..26ac953 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'package:avilatek_ui/avilatek_ui.dart'; import 'package:example/pages/adaptive_dialog/adaptive_dialog_example.dart'; import 'package:example/pages/avila_snackbar/avila_snackar_example_page.dart'; import 'package:example/pages/constants_showcase/constants_showcase_page.dart'; +import 'package:example/pages/custom_tag/custom_tags_page.dart'; import 'package:example/pages/developed_by_logo/developed_by_logo_example.dart'; import 'package:example/pages/field_with_title/title_wrapper_example_page.dart'; import 'package:example/pages/file_uploader/file_uploader_page.dart'; @@ -26,6 +27,12 @@ class MyApp extends StatelessWidget { titleWrapperStyle: TitleWrapperStyle(), selectorButtonStyle: SelectorButtonStyle(), avilaSnackBarTheme: AvilaSnackBarTheme(), + customTagStyle: CustomTagStyle( + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + ), ); return MaterialApp( @@ -95,155 +102,173 @@ class _MyHomePageState extends State { body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PermissionHandlerExamplePage(), - ), - ); - }, - child: const Text('Permission Handler Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RemoteDataFetchExamplePage(), - ), - ); - }, - child: const Text('Remote Data Fetch Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const TitleWrapperExample(), - ), - ); - }, - child: const Text('Title Wrapper Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AdaptiveDialogExample(), - ), - ); - }, - child: const Text('Adaptive dialog'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DevelopedByLogoExample(), - ), - ); - }, - child: const Text('Developed By Logo'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AvilaSnackbarExamplePage(), - ), - ); - }, - child: const Text('Avila Snackbar Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const PendingNotificationsExamplePage(), - ), - ); - }, - child: const Text('Pending Notifications Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SelectorSheetExamplePage(), - ), - ); - }, - child: const Text('Selector Sheet Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ConstantsShowcasePage(), - ), - ); - }, - child: const Text('Constants Showcase'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RainbowPage(), - ), - ); - }, - child: const Text('Paged Remote Data Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const FileUploaderPage(), - ), - ); - }, - child: const Text('Upload File Bloc Example'), - ), - ], + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 20, + ), + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + // + // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" + // action in the IDE, or press "p" in the console), to see the + // wireframe for each widget. + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const PermissionHandlerExamplePage(), + ), + ); + }, + child: const Text('Permission Handler Bloc Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RemoteDataFetchExamplePage(), + ), + ); + }, + child: const Text('Remote Data Fetch Bloc Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TitleWrapperExample(), + ), + ); + }, + child: const Text('Title Wrapper Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AdaptiveDialogExample(), + ), + ); + }, + child: const Text('Adaptive dialog'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DevelopedByLogoExample(), + ), + ); + }, + child: const Text('Developed By Logo'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AvilaSnackbarExamplePage(), + ), + ); + }, + child: const Text('Avila Snackbar Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const PendingNotificationsExamplePage(), + ), + ); + }, + child: const Text('Pending Notifications Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SelectorSheetExamplePage(), + ), + ); + }, + child: const Text('Selector Sheet Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ConstantsShowcasePage(), + ), + ); + }, + child: const Text('Constants Showcase'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RainbowPage(), + ), + ); + }, + child: const Text('Paged Remote Data Bloc Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const FileUploaderPage(), + ), + ); + }, + child: const Text('Upload File Bloc Example'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CustomTagsPage(), + ), + ); + }, + child: const Text('Custom Tags Example'), + ), + ], + ), ), ), ); diff --git a/example/lib/pages/custom_tag/custom_tags_page.dart b/example/lib/pages/custom_tag/custom_tags_page.dart new file mode 100644 index 0000000..89a956b --- /dev/null +++ b/example/lib/pages/custom_tag/custom_tags_page.dart @@ -0,0 +1,49 @@ +import 'package:avilatek_ui/avilatek_ui.dart'; +import 'package:flutter/material.dart'; + +class CustomTagsPage extends StatelessWidget { + const CustomTagsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Custom Tags'), + ), + body: Align( + alignment: Alignment.center, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + CustomTag.primaryOne( + context, + child: const Text('Primary color tag'), + ), + const SizedBox(height: 16), + CustomTag.green( + context, + child: const Text('Green tag'), + ), + const SizedBox(height: 16), + CustomTag.yellow( + context, + child: const Text('Yellow tag'), + ), + const SizedBox(height: 16), + CustomTag.red( + context, + child: const Text('Red tag'), + ), + const SizedBox(height: 16), + CustomTag.grey( + context, + child: const Text('Neutral tag'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/avilatek_ui/lib/avilatek_ui.dart b/packages/avilatek_ui/lib/avilatek_ui.dart index fbab4e1..35ab071 100644 --- a/packages/avilatek_ui/lib/avilatek_ui.dart +++ b/packages/avilatek_ui/lib/avilatek_ui.dart @@ -3,12 +3,14 @@ library avilatek_ui; export 'src/avilatek_ui.dart'; export 'src/ui/avila_custom_theme.dart'; -export 'src/ui/avila_snackbar/avila_snackbar.dart'; +export 'src/ui/avila_snackbar/avila_snackbar_theme.dart'; +export 'src/ui/custom_tag/custom_tag_style.dart'; export 'src/ui/selector_button/selector_button_style.dart'; export 'src/ui/selector_sheet/selector_sheet_theme.dart'; export 'src/ui/title_wrapper/title_wrapper_style.dart'; export 'src/widgets/adaptive_dialog.dart'; export 'src/widgets/avila_snackbar.dart'; +export 'src/widgets/custom_tags.dart'; export 'src/widgets/developed_by.dart'; export 'src/widgets/selector_button.dart'; export 'src/widgets/selector_sheet.dart'; diff --git a/packages/avilatek_ui/lib/src/ui/avila_custom_theme.dart b/packages/avilatek_ui/lib/src/ui/avila_custom_theme.dart index 616d8fe..aee6da4 100644 --- a/packages/avilatek_ui/lib/src/ui/avila_custom_theme.dart +++ b/packages/avilatek_ui/lib/src/ui/avila_custom_theme.dart @@ -11,6 +11,7 @@ class AvilaCustomTheme { this.titleWrapperStyle, this.selectorButtonStyle, this.avilaSnackBarTheme, + this.customTagStyle, }); /// `titleWrapperStyle` is being used to handle the theme of @@ -25,11 +26,16 @@ class AvilaCustomTheme { /// [AvilaSnackBar] widget final AvilaSnackBarTheme? avilaSnackBarTheme; + /// `customTagStyle` is being used to handle the theme of + /// [CustomTag] widget + final CustomTagStyle? customTagStyle; + /// `extensions` is used to get all AvilaTek theme extensions, to then /// define in the `MaterialApp` theme Iterable> get extensions => { titleWrapperStyle ?? const TitleWrapperStyle(), selectorButtonStyle ?? const SelectorButtonStyle(), avilaSnackBarTheme ?? const AvilaSnackBarTheme(), + customTagStyle ?? const CustomTagStyle(), }; } diff --git a/packages/avilatek_ui/lib/src/ui/custom_tag/custom_tag_style.dart b/packages/avilatek_ui/lib/src/ui/custom_tag/custom_tag_style.dart new file mode 100644 index 0000000..3814d64 --- /dev/null +++ b/packages/avilatek_ui/lib/src/ui/custom_tag/custom_tag_style.dart @@ -0,0 +1,81 @@ +import 'dart:ui'; + +import 'package:avilatek_ui/src/widgets/custom_tags.dart'; +import 'package:flutter/material.dart'; + +/// The theme data for the [CustomTag] widget. +class CustomTagStyle extends ThemeExtension { + /// Creates a [CustomTagStyle]. + const CustomTagStyle({ + this.backgroundColor, + this.foregroundColor, + this.border, + this.padding, + this.iconColor, + this.iconSize, + this.textStyle, + }); + + /// The [backgroundColor] parameter is the background color of the tag. + final Color? backgroundColor; + + /// The [foregroundColor] parameter changes the color of the child widget. + final Color? foregroundColor; + + /// The [border] parameter is the border of the tag. It defaults to `null`. + final BoxBorder? border; + + /// The optional [padding] parameter overrides the default padding + /// for [CustomTag]. + final EdgeInsets? padding; + + /// The [iconColor] parameter changes the color of the [Icon] widget. + final Color? iconColor; + + /// The [iconSize] parameter changes the size of the [Icon] widget. + final double? iconSize; + + /// The [textStyle] parameter changes the TextStyle of the widget. + final TextStyle? textStyle; + + @override + ThemeExtension copyWith({ + Color? backgroundColor, + Color? foregroundColor, + TextStyle? textStyle, + BoxBorder? border, + EdgeInsets? padding, + Color? iconColor, + double? iconSize, + }) { + return CustomTagStyle( + backgroundColor: backgroundColor ?? this.backgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + border: border ?? this.border, + padding: padding ?? this.padding, + iconColor: iconColor ?? this.iconColor, + iconSize: iconSize ?? this.iconSize, + textStyle: textStyle ?? this.textStyle, + ); + } + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! CustomTagStyle) { + return this; + } + + return CustomTagStyle( + backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t), + foregroundColor: Color.lerp(foregroundColor, other.foregroundColor, t), + border: BoxBorder.lerp(border, other.border, t), + padding: EdgeInsets.lerp(padding, other.padding, t), + iconColor: Color.lerp(iconColor, other.iconColor, t), + iconSize: lerpDouble(iconSize, other.iconSize, t), + textStyle: TextStyle.lerp(textStyle, other.textStyle, t), + ); + } +} diff --git a/packages/avilatek_ui/lib/src/ui/title_wrapper/title_wrapper_style.dart b/packages/avilatek_ui/lib/src/ui/title_wrapper/title_wrapper_style.dart index 87a019e..ed225af 100644 --- a/packages/avilatek_ui/lib/src/ui/title_wrapper/title_wrapper_style.dart +++ b/packages/avilatek_ui/lib/src/ui/title_wrapper/title_wrapper_style.dart @@ -1,8 +1,14 @@ import 'dart:ui'; +import 'package:avilatek_ui/avilatek_ui.dart'; import 'package:flutter/material.dart'; +/// The theme data for the [TitleWrapper] widget. +/// +/// This class is used by [TitleWrapper] to configure the default +/// appearance of the widget. class TitleWrapperStyle extends ThemeExtension { + /// Creates a [TitleWrapperStyle]. const TitleWrapperStyle({ this.titleSpacing, this.footerSpacing, @@ -10,18 +16,18 @@ class TitleWrapperStyle extends ThemeExtension { this.footerStyle, }); -// /// Defines the default space between title and child of the -// /// [TitleWrapper] widget. + /// Defines the default space between title and child of the + // / [TitleWrapper] widget. final double? titleSpacing; -// /// Defines the default spacing between child and footer of the -// /// [TitleWrapper] widget. + /// Defines the default spacing between child and footer of the + /// [TitleWrapper] widget. final double? footerSpacing; -// /// Defines the default title TextStyle of the [TitleWrapper] widget. + /// Defines the default title TextStyle of the [TitleWrapper] widget. final TextStyle? titleStyle; -// /// Defines the default footer TextStyle of the [TitleWrapper] widget. + /// Defines the default footer TextStyle of the [TitleWrapper] widget. final TextStyle? footerStyle; @override diff --git a/packages/avilatek_ui/lib/src/widgets/custom_tags.dart b/packages/avilatek_ui/lib/src/widgets/custom_tags.dart index 9040966..6c2d533 100644 --- a/packages/avilatek_ui/lib/src/widgets/custom_tags.dart +++ b/packages/avilatek_ui/lib/src/widgets/custom_tags.dart @@ -1,181 +1,282 @@ -// import 'package:flutter/material.dart'; -// import 'package:google_fonts/google_fonts.dart'; -// import 'package:suni_wallet_app/src/constants.dart'; - -// /// {@template custom_tag} -// /// This widget displays a tag. Usually used for status indicators. -// /// -// /// This class has custom constructors for the most common colors. -// /// -// /// ![](https://i.imgur.com/hIW3akM.png) -// /// ##### *Example of a [CustomTag.green] widget with a row of [Icon] and [Text] as a child.* -// /// -// /// Parameters -// /// --- -// /// The [child] parameter is the main content of the tag. It is usually a [Text] -// /// widget. -// /// -// /// The [backgroundColor] parameter is the background color of the tag. -// /// -// /// The [foregroundColor] parameter changes the color of the [child] widget. -// /// -// /// The [border] parameter is the border of the tag. It defaults to `null`. -// /// -// /// The optional [padding] parameter overrides the default padding for [CustomTag]. -// /// -// /// The [iconColor] parameter changes the color of the [Icon] widget. -// /// -// /// {@endtemplate} -// class CustomTag extends StatelessWidget { -// /// {@macro custom_tag} -// const CustomTag({ -// super.key, -// required this.child, -// required this.backgroundColor, -// required this.foregroundColor, -// this.border, -// this.padding, -// this.iconColor, -// }); - -// final Widget child; -// final BoxBorder? border; -// final EdgeInsets? padding; -// final Color? iconColor; -// final Color backgroundColor; -// final Color foregroundColor; - -// factory CustomTag.primaryOne({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.primaryOne.shade200, -// width: 1, -// ), -// backgroundColor: Consts.primaryOne.shade50, -// foregroundColor: Consts.primaryOne.shade500, -// iconColor: Consts.primaryOne.shade500, -// padding: padding, -// child: child, -// ); -// } - -// factory CustomTag.primaryTwo({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.primaryTwo.shade200, -// width: 1, -// ), -// backgroundColor: Consts.primaryTwo.shade50, -// foregroundColor: Consts.primaryTwo.shade700, -// iconColor: Consts.primaryTwo.shade500, -// padding: padding, -// child: child, -// ); -// } - -// factory CustomTag.grey({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.neutral.shade200, -// width: 1, -// ), -// backgroundColor: Consts.neutral.shade50, -// foregroundColor: Consts.neutral.shade400, -// iconColor: Consts.neutral.shade500, -// padding: padding, -// child: child, -// ); -// } - -// factory CustomTag.green({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.success.shade200, -// width: 1, -// ), -// backgroundColor: Consts.success.shade50, -// foregroundColor: Consts.success.shade700, -// iconColor: Consts.success.shade500, -// padding: padding, -// child: child, -// ); -// } - -// factory CustomTag.yellow({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.warning.shade200, -// width: 1, -// ), -// backgroundColor: Consts.warning.shade50, -// foregroundColor: Consts.warning.shade700, -// iconColor: Consts.warning.shade500, -// padding: padding, -// child: child, -// ); -// } - -// factory CustomTag.red({ -// required Widget child, -// EdgeInsets? padding, -// }) { -// return CustomTag( -// border: Border.all( -// color: Consts.error.shade200, -// width: 1, -// ), -// backgroundColor: Consts.error.shade50, -// foregroundColor: Consts.error.shade700, -// iconColor: Consts.error.shade500, -// padding: padding, -// child: child, -// ); -// } - -// @override -// Widget build(BuildContext context) { -// return Theme( -// data: Theme.of(context).copyWith( -// iconTheme: IconThemeData( -// color: iconColor ?? foregroundColor, -// size: 15, -// ), -// ), -// child: Container( -// padding: padding ?? -// const EdgeInsets.symmetric( -// horizontal: Consts.padding * 1, -// vertical: Consts.padding * 0.5, -// ), -// decoration: BoxDecoration( -// color: backgroundColor, -// borderRadius: BorderRadius.circular(100), -// border: border, -// ), -// child: DefaultTextStyle( -// style: GoogleFonts.quicksand( -// color: foregroundColor, -// fontSize: 13, -// fontWeight: FontWeight.w700, -// ), -// child: child, -// ), -// ), -// ); -// } -// } +import 'package:avilatek_ui/src/ui/custom_tag/custom_tag_style.dart'; +import 'package:flutter/material.dart'; + +/// {@template custom_tag} +/// This widget displays a tag. Usually used for status indicators. +/// +/// This class has custom constructors for the most common colors. +/// +/// Parameters +/// --- +/// The [child] parameter is the main content of the tag. It is usually a [Text] +/// widget. +/// +/// {@endtemplate} +class CustomTag extends StatelessWidget { + /// {@macro custom_tag} + const CustomTag({ + required this.child, + required this.style, + super.key, + }); + + /// Custom constructor for a tag with the primary color of the app. + /// The [child] parameter is the main content of the tag. It is usually a + /// [Text] widget. + /// The [context] parameter is used to get the primary color of the app. + /// The [style] parameter is used if you want to override the default style + /// for this constructor. + factory CustomTag.primaryOne( + BuildContext context, { + required Widget child, + CustomTagStyle? style, + }) { + final themeStyle = Theme.of(context).extension(); + + final primaryColor = Theme.of(context).primaryColor; + + final backgroundColor = + style?.backgroundColor ?? primaryColor.withOpacity(0.3); + + final foregroundColor = style?.foregroundColor ?? primaryColor; + + final iconColor = style?.iconColor ?? primaryColor; + + final padding = style?.padding ?? themeStyle?.padding; + + final border = style?.border ?? themeStyle?.border; + + final textStyle = style?.textStyle ?? themeStyle?.textStyle; + + return CustomTag( + style: CustomTagStyle( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + padding: padding, + border: border, + iconColor: iconColor, + textStyle: textStyle, + ), + child: child, + ); + } + + /// Custom constructor for a tag with green color. Use this constructor + /// when you want to display a tag with a success color. + /// The [child] parameter is the main content of the tag. It is usually a + /// [Text] widget. + /// The [context] parameter is used to get the default CustomTagStyle. + /// The [style] parameter is used if you want to override the default style + /// for this constructor. + factory CustomTag.green( + BuildContext context, { + required Widget child, + CustomTagStyle? style, + }) { + final themeStyle = Theme.of(context).extension(); + + final green = Colors.green[800]; + + final backgroundColor = style?.backgroundColor ?? green?.withOpacity(0.3); + + final foregroundColor = style?.foregroundColor ?? green; + + final iconColor = style?.iconColor ?? green; + + final padding = style?.padding ?? themeStyle?.padding; + + final border = style?.border ?? themeStyle?.border; + + final textStyle = style?.textStyle ?? themeStyle?.textStyle; + + return CustomTag( + style: CustomTagStyle( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + padding: padding, + border: border, + iconColor: iconColor, + textStyle: textStyle, + ), + child: child, + ); + } + + /// Custom constructor for a tag with green color. Use this constructor + /// when you want to display a tag with a warning color. + /// The [child] parameter is the main content of the tag. It is usually a + /// [Text] widget. + /// The [context] parameter is used to get the default CustomTagStyle. + /// The [style] parameter is used if you want to override the default style + /// for this constructor. + factory CustomTag.yellow( + BuildContext context, { + required Widget child, + CustomTagStyle? style, + }) { + final themeStyle = Theme.of(context).extension(); + + final yellow = Colors.yellow[800]; + + final backgroundColor = style?.backgroundColor ?? yellow?.withOpacity(0.3); + + final foregroundColor = style?.foregroundColor ?? yellow; + + final iconColor = style?.iconColor ?? yellow; + + final padding = style?.padding ?? themeStyle?.padding; + + final border = style?.border ?? themeStyle?.border; + + final textStyle = style?.textStyle ?? themeStyle?.textStyle; + + return CustomTag( + style: CustomTagStyle( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + padding: padding, + border: border, + iconColor: iconColor, + textStyle: textStyle, + ), + child: child, + ); + } + + /// Custom constructor for a tag with green color. Use this constructor + /// when you want to display a tag with a danger color. + /// The [child] parameter is the main content of the tag. It is usually a + /// [Text] widget. + /// The [context] parameter is used to get the default CustomTagStyle. + /// The [style] parameter is used if you want to override the default style + /// for this constructor. + factory CustomTag.red( + BuildContext context, { + required Widget child, + CustomTagStyle? style, + }) { + final themeStyle = Theme.of(context).extension(); + + final red = Colors.red[800]; + + final backgroundColor = style?.backgroundColor ?? red?.withOpacity(0.3); + + final foregroundColor = style?.foregroundColor ?? red; + + final iconColor = style?.iconColor ?? red; + + final padding = style?.padding ?? themeStyle?.padding; + + final border = style?.border ?? themeStyle?.border; + + final textStyle = style?.textStyle ?? themeStyle?.textStyle; + + return CustomTag( + style: CustomTagStyle( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + padding: padding, + border: border, + iconColor: iconColor, + textStyle: textStyle, + ), + child: child, + ); + } + + /// Custom constructor for a tag with the grey color. Use this constructor + /// when you want to display a tag with a neutral color. + /// The [child] parameter is the main content of the tag. It is usually a + /// [Text] widget. + /// The [context] parameter is used to get the default CustomTagStyle. + /// The [style] parameter is used if you want to override the default style + /// for this constructor. + factory CustomTag.grey( + BuildContext context, { + required Widget child, + CustomTagStyle? style, + }) { + final themeStyle = Theme.of(context).extension(); + + final grey = Colors.grey.shade800; + + final backgroundColor = style?.backgroundColor ?? grey.withOpacity(0.3); + + final foregroundColor = style?.foregroundColor ?? grey; + + final iconColor = style?.iconColor ?? grey; + + final padding = style?.padding ?? themeStyle?.padding; + + final border = style?.border ?? themeStyle?.border; + + final textStyle = style?.textStyle ?? themeStyle?.textStyle; + + return CustomTag( + style: CustomTagStyle( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + padding: padding, + border: border, + iconColor: iconColor, + textStyle: textStyle, + ), + child: child, + ); + } + + /// + final Widget child; + + /// + final CustomTagStyle? style; + + @override + Widget build(BuildContext context) { + final defaultStyle = Theme.of(context).extension(); + + final defaultTheme = Theme.of(context); + + final textStyle = style?.textStyle ?? + defaultStyle?.textStyle ?? + defaultTheme.textTheme.labelMedium; + + final backgroundColor = + style?.backgroundColor ?? defaultStyle?.backgroundColor; + + final foregroundColor = + style?.foregroundColor ?? defaultStyle?.foregroundColor; + + final iconColor = + style?.iconColor ?? defaultStyle?.iconColor ?? foregroundColor; + + final iconSize = style?.iconSize ?? defaultStyle?.iconSize ?? 15.0; + + final padding = style?.padding ?? defaultStyle?.padding; + + final border = style?.border ?? defaultStyle?.border; + + return Theme( + data: Theme.of(context).copyWith( + iconTheme: IconThemeData( + color: iconColor ?? foregroundColor, + size: iconSize, + ), + ), + child: Container( + padding: padding, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(100), + border: border, + ), + child: DefaultTextStyle( + style: textStyle!.copyWith( + color: foregroundColor, + ), + child: child, + ), + ), + ); + } +} From e9e8e6562b91a9c0b0f855a5c947513d08c2ad6f Mon Sep 17 00:00:00 2001 From: Andres Eloy Date: Sun, 24 Mar 2024 13:20:25 -0400 Subject: [PATCH 2/3] feat: SendDataBloc (missing example --- packages/avilatek_bloc/README.md | 31 +++++++- .../lib/src/send_data/send_data_bloc.dart | 63 +++++++++++++++++ .../lib/src/send_data/send_data_event.dart | 30 ++++++++ .../lib/src/send_data/send_data_handler.dart | 43 ++++++++++++ .../lib/src/send_data/send_data_state.dart | 70 +++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart create mode 100644 packages/avilatek_bloc/lib/src/send_data/send_data_event.dart create mode 100644 packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart create mode 100644 packages/avilatek_bloc/lib/src/send_data/send_data_state.dart diff --git a/packages/avilatek_bloc/README.md b/packages/avilatek_bloc/README.md index 126b97f..ddd9ffa 100644 --- a/packages/avilatek_bloc/README.md +++ b/packages/avilatek_bloc/README.md @@ -28,7 +28,12 @@ flutter packages get ## RemoteDataBloc -This bloc is a generalized state machine for handling remote data. It abstracts the process of fetching data from the internet and provides a simple and consistent interface for handling the various states of the data fetching process. +This bloc is a generalized state machine for handling data fetching. It abstracts the process of fetching data from the internet and provides a simple and consistent interface for handling the various states of the data fetching process. + +It is useful for handling the process of fetching data from the internet, such as fetching a single item, a list of items, a file, etc. One of the advantages of this bloc, apart from its simplicity, is that it makes it possible to keep data in the UI while refetching, or to show the last fetched data in case of an error in a simple way. + + +Note: If you need to paginate the data, you may use the [`PagedRemoteDataBloc`](#PagedRemoteDataBloc) instead. The `RemoteDataBloc` State Machine is as follows: @@ -55,7 +60,31 @@ title: RemoteDataBloc State Machine linkStyle 0,1,5,7 stroke:#f05,stroke-width:2px,color:crimson; linkStyle 3,6,8 stroke:lightgreen,stroke-width:2px,color:lightgreen; ``` +## SendDataBloc + +This bloc is a generalized state machine to send data. It abstracts the process of sending payloads to a remote server and provides a simple and consistent interface for handling the various states of this process. + +This bloc is useful for handling the process of sending data to a remote server, such as sending a form, sending a message, or sending a file. It does not care about storing any reference to the data being sent, it only cares about the process of sending the data. If you need to store the data before, during or after being sent, you may combine this with anouther bloc that stores it. + + +The `SendDataBloc` State Machine is as follows: +```mermaid + +--- +title: SendDataBloc State Machine +--- + + graph LR; + A[SendDataReady] -- DataSent --> B[SendDataLoading]; + D -.-> A; + B -- "error" --> D[SendDataFailure]; + B -- "success" --> E[SendDataSuccess]; + E -.-> A + linkStyle 1,2 stroke:#f05,stroke-width:2px,color:crimson; + linkStyle 3,4 stroke:lightgreen,stroke-width:2px,color:lightgreen; +``` + ## PagedRemoteDataBloc diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart new file mode 100644 index 0000000..5e0187d --- /dev/null +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart @@ -0,0 +1,63 @@ +import 'package:avilatek_bloc/src/send_data/send_data_event.dart'; +import 'package:avilatek_bloc/src/send_data/send_data_handler.dart'; +import 'package:avilatek_bloc/src/send_data/send_data_state.dart'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; + +export 'package:avilatek_bloc/src/send_data/send_data_event.dart'; +export 'package:avilatek_bloc/src/send_data/send_data_state.dart'; + +/// Abstract Bloc simple "Send Data" blocs. +abstract class SendDataBloc extends Bloc { + /// + SendDataBloc() : super(SendDataReady()) { + _handler = SendDataEventHandler(); + on>(_mapDataSentToState); + } + late SendDataEventHandler _handler; + + /// Propagates the [DataSent] event down to the corresponding event + /// handler. + Future _mapDataSentToState( + DataSent event, + Emitter emit, + ) async { + return _handleStatesOnEvent( + isNoOp: state is SendDataLoading || + state is SendDataFailure || + state is SendDataSuccess, + onDataSent: () => _handler.mapDataSentToState( + event, + state as SendDataReady, + emit, + sendData, + ), + ); + } + + /// Helper function that can be used by [_mapDataSentToState] function + /// for cleaner propagation of the events to the corresponding event handler. + Future _handleStatesOnEvent({ + required bool isNoOp, + Future Function()? onDataSent, + }) async { + if (isNoOp) { + return; + } else if (state is SendDataReady && onDataSent != null) { + return onDataSent(); + } else { + throw UnimplementedError( + 'No handler implemented for combination: ${state.runtimeType}.', + ); + } + } + + /// Implements the code that calls the data sending function. + /// + /// The sent data must be returned by this function. + @visibleForTesting + Future sendData( + SendDataState oldState, + DataSent event, + ); +} diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart new file mode 100644 index 0000000..277cc12 --- /dev/null +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; + +/// {@template send_data_event} +/// Abstract class that represents the events that can be dispatched to the +/// [SendDataBloc]. +/// {@endtemplate} +abstract class SendDataEvent extends Equatable { + /// {@macro send_data_event} + const SendDataEvent(); + + @override + List get props => []; +} + +/// {@template data_sent} +/// Event that triggers the fetching of the send data. +/// {@endtemplate} +class DataSent extends SendDataEvent { + /// {@macro fetch_send_data} + const DataSent(this.data, {this.simulateError = false}); + + /// If true, the [SendDataBloc] will simulate an error. + final bool? simulateError; + + /// The data that is to be sent. + final T data; + + @override + List get props => [simulateError, data]; +} diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart new file mode 100644 index 0000000..b569747 --- /dev/null +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart @@ -0,0 +1,43 @@ +import 'package:avilatek_bloc/src/send_data/send_data_event.dart'; +import 'package:avilatek_bloc/src/send_data/send_data_state.dart'; +import 'package:bloc/bloc.dart'; + +/// +class SendDataEventHandler { + /// + const SendDataEventHandler(); + + /// Handler for [DataSent] + [SendDataReady] combination. + /// Handles initial fetch when the send data is not yet present. + /// + /// On success it emits: [SendDataLoading], [SendDataSuccess], [SendDataReady] + /// On failure it emits: [SendDataLoading], + /// [SendDataFailure], [SendDataReady]. + Future mapDataSentToState( + DataSent event, + SendDataReady state, + Emitter emit, + Future Function(SendDataState, DataSent) sendData, + ) async { + try { + emit(SendDataLoading()); + + if (event.simulateError ?? false) { + await _simulateError(); + } + + await sendData(state, event); + + emit(SendDataSuccess(event.data)); + emit(SendDataReady()); + } catch (e) { + emit(SendDataFailure(event.data, e)); + emit(SendDataReady()); + } + } + + Future _simulateError() { + Future.delayed(const Duration(seconds: 1)); + throw Exception('Simulated error'); + } +} diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_state.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_state.dart new file mode 100644 index 0000000..cbbbca3 --- /dev/null +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_state.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; + +/// +extension SendDataStateX on SendDataState { + /// Returns `true` if the state is [SendDataReady], ready to send data. + bool get isReady => this is SendDataReady; + + /// Returns `true` if the state is [SendDataLoading], waiting for the data to + /// be sent. + bool get isLoading => this is SendDataLoading; + + /// Returns `true` if the state is [SendDataFailure], the data transmission + /// failed. + bool get isFailure => this is SendDataFailure; + + /// Returns `true` if the state is [SendDataSuccess], the data was sent + /// successfully. + bool get isSuccess => this is SendDataSuccess; +} + +/// Base class for all states of the [SendDataBloc]. +abstract class SendDataState extends Equatable { + /// + const SendDataState(); + + @override + List get props => []; +} + +/// Initial state of the [SendDataBloc]. +class SendDataReady extends SendDataState {} + +/// State during which the bloc is waiting for the asynchonous task to +/// finish. +class SendDataLoading extends SendDataState {} + +/// {@template send_data_failure} +/// State emitted when the data transmission fails. +/// +/// This state is only emitted from the [SendDataLoading] state. +/// {@endtemplate} +class SendDataFailure extends SendDataState { + /// {@macro send_data_failure} + const SendDataFailure(this.data, this.error); + + /// The data that was just sent. + final T data; + + /// The error that caused the data transmission to fail. + final dynamic error; + + @override + List get props => [...super.props, data, error]; +} + +/// {@template send_data_success} +/// State emitted when the data is sent successfully. +/// +/// This state is only emitted from the [SendDataLoading] state. +/// {@endtemplate} +class SendDataSuccess extends SendDataState { + /// {@macro send_data_success} + const SendDataSuccess(this.data); + + /// The data that was just sent. + final T data; + + @override + List get props => [...super.props, data]; +} From 85ad28dda76cbfe389bec69098e122e0ffc02fdb Mon Sep 17 00:00:00 2001 From: Andres Eloy Date: Sun, 24 Mar 2024 14:05:36 -0400 Subject: [PATCH 3/3] feat: SendDataBloc with example --- example/lib/main.dart | 302 +++++++----------- .../lib/pages/send_data/send_data_bloc.dart | 17 + .../lib/pages/send_data/send_data_page.dart | 157 +++++++++ example/pubspec.yaml | 2 - packages/avilatek_bloc/lib/avilatek_bloc.dart | 1 + .../lib/src/send_data/send_data_bloc.dart | 8 +- .../lib/src/send_data/send_data_event.dart | 4 +- .../lib/src/send_data/send_data_handler.dart | 6 +- 8 files changed, 308 insertions(+), 189 deletions(-) create mode 100644 example/lib/pages/send_data/send_data_bloc.dart create mode 100644 example/lib/pages/send_data/send_data_page.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 05bfbbe..29064ae 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,8 +2,8 @@ import 'package:avilatek_ui/avilatek_ui.dart'; import 'package:example/pages/adaptive_dialog/adaptive_dialog_example.dart'; import 'package:example/pages/avila_snackbar/avila_snackar_example_page.dart'; import 'package:example/pages/constants_showcase/constants_showcase_page.dart'; -import 'package:example/pages/custom_tag/custom_tags_page.dart'; import 'package:example/pages/custom_loading_indicator/custom_loading_indicator_example.dart'; +import 'package:example/pages/custom_tag/custom_tags_page.dart'; import 'package:example/pages/developed_by_logo/developed_by_logo_example.dart'; import 'package:example/pages/field_with_title/title_wrapper_example_page.dart'; import 'package:example/pages/file_uploader/file_uploader_page.dart'; @@ -12,8 +12,29 @@ import 'package:example/pages/permission_handler_example_page.dart'; import 'package:example/pages/remote_data/remote_data_fetch_example_page.dart'; import 'package:example/pages/remote_data_paginated/view/rainbow_page.dart'; import 'package:example/pages/selector_sheet/selector_sheet_example_page.dart'; +import 'package:example/pages/send_data/send_data_page.dart'; import 'package:flutter/material.dart'; +class ExampleMenuItem { + const ExampleMenuItem({ + required this.title, + required this.child, + }); + + final String title; + final Widget child; +} + +class ExampleMenuCategory { + const ExampleMenuCategory({ + required this.title, + required this.children, + }); + + final String title; + final List children; +} + void main() { runApp(const MyApp()); } @@ -82,6 +103,75 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { + final widgetItems = const [ + ExampleMenuItem( + title: 'Title Wrapper Example', + child: TitleWrapperExample(), + ), + ExampleMenuItem( + title: 'Adaptive dialog', + child: AdaptiveDialogExample(), + ), + ExampleMenuItem( + title: 'Developed By Logo', + child: DevelopedByLogoExample(), + ), + ExampleMenuItem( + title: 'Avila Snackbar Example', + child: AvilaSnackbarExamplePage(), + ), + ExampleMenuItem( + title: 'Selector Sheet Example', + child: SelectorSheetExamplePage(), + ), + ExampleMenuItem( + title: 'Custom Loading Indicator Example', + child: CustomLoadingIndicatorExample(), + ), + ExampleMenuItem( + title: 'Custom Tags Example', + child: CustomTagsPage(), + ), + ]; + final blocItems = const [ + ExampleMenuItem( + title: 'Remote Data Fetch Bloc Example', + child: RemoteDataFetchExamplePage(), + ), + ExampleMenuItem( + title: 'Permission Handler Bloc Example', + child: PermissionHandlerExamplePage(), + ), + ExampleMenuItem( + title: 'Pending Notifications Bloc Example', + child: PendingNotificationsExamplePage(), + ), + ExampleMenuItem( + title: 'Paged Remote Data Bloc Example', + child: RainbowPage(), + ), + ExampleMenuItem( + title: 'Upload File Bloc Example', + child: FileUploaderPage(), + ), + ExampleMenuItem( + title: 'Send Data Bloc', + child: SendDataPage(), + ), + ]; + final otherItems = const [ + ExampleMenuItem( + title: 'Constants Showcase', + child: ConstantsShowcasePage(), + ), + ]; + + late final categories = [ + ExampleMenuCategory(title: 'Blocs', children: blocItems), + ExampleMenuCategory(title: 'Widgets', children: widgetItems), + ExampleMenuCategory(title: 'Others', children: otherItems), + ]; + @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done @@ -103,183 +193,39 @@ class _MyHomePageState extends State { body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. - child: SingleChildScrollView( - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const PermissionHandlerExamplePage(), - ), - ); - }, - child: const Text('Permission Handler Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RemoteDataFetchExamplePage(), - ), - ); - }, - child: const Text('Remote Data Fetch Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const TitleWrapperExample(), - ), - ); - }, - child: const Text('Title Wrapper Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AdaptiveDialogExample(), - ), - ); - }, - child: const Text('Adaptive dialog'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DevelopedByLogoExample(), - ), - ); - }, - child: const Text('Developed By Logo'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AvilaSnackbarExamplePage(), - ), - ); - }, - child: const Text('Avila Snackbar Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const PendingNotificationsExamplePage(), - ), - ); - }, - child: const Text('Pending Notifications Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SelectorSheetExamplePage(), - ), - ); - }, - child: const Text('Selector Sheet Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const CustomLoadingIndicatorExample(), - ), - ); - }, - child: const Text('Custom Loading Indicator Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ConstantsShowcasePage(), - ), - ); - }, - child: const Text('Constants Showcase'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const RainbowPage(), - ), - ); - }, - child: const Text('Paged Remote Data Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const FileUploaderPage(), - ), - ); - }, - child: const Text('Upload File Bloc Example'), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CustomTagsPage(), - ), - ); - }, - child: const Text('Custom Tags Example'), - ), - ], - ), + child: ListView.separated( + itemCount: categories.length, + padding: const EdgeInsets.all(24), + itemBuilder: (context, index) { + final category = categories[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.title, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + ListView.separated( + shrinkWrap: true, + itemCount: category.children.length, + itemBuilder: (context, index) => ListTile( + title: Text(category.children[index].title), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => category.children[index].child, + ), + ); + }, + ), + separatorBuilder: (_, __) => const Divider(height: 0), + ), + ], + ); + }, + separatorBuilder: (_, __) => const SizedBox(height: 24), ), ), ); diff --git a/example/lib/pages/send_data/send_data_bloc.dart b/example/lib/pages/send_data/send_data_bloc.dart new file mode 100644 index 0000000..cfed9f6 --- /dev/null +++ b/example/lib/pages/send_data/send_data_bloc.dart @@ -0,0 +1,17 @@ +import 'package:avilatek_bloc/avilatek_bloc.dart'; + +class ExampleSendDataBloc extends SendDataBloc { + ExampleSendDataBloc() : super(); + + @override + Future sendData(SendDataState oldState, DataSent event) async { + // Simulate a network request + // Here, you would normally call the function that sends the data to the + // server. + await Future.delayed(const Duration(seconds: 2)); + + /// This function must return the data sent in order to make it available + /// in the [SendDataLoading], [SendDataSuccess] state. + return event.data; + } +} diff --git a/example/lib/pages/send_data/send_data_page.dart b/example/lib/pages/send_data/send_data_page.dart new file mode 100644 index 0000000..10a0a22 --- /dev/null +++ b/example/lib/pages/send_data/send_data_page.dart @@ -0,0 +1,157 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:avilatek_ui/avilatek_ui.dart'; +import 'package:example/pages/file_uploader/file_uploader_bloc.dart'; +import 'package:example/pages/send_data/send_data_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FormTextInput extends StatelessWidget { + const FormTextInput({ + super.key, + this.enabled = true, + required this.title, + required this.controller, + }); + + final String title; + final bool enabled; + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextField( + controller: controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + enabled: enabled, + ), + ], + ); + } +} + +class FormModel { + FormModel({ + required this.name, + required this.email, + required this.phone, + }); + + final String name; + final String email; + final String phone; + + @override + String toString() => 'Name: $name\nEmail: $email\nPhone: $phone'; +} + +class SendDataPage extends StatelessWidget { + const SendDataPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ExampleSendDataBloc(), + child: Scaffold( + appBar: AppBar( + title: const Text('Send Data Bloc Example'), + ), + body: SendDataView(), + ), + ); + } +} + +class SendDataView extends StatelessWidget { + SendDataView({super.key}); + + final controllers = (List.generate(3, (index) => TextEditingController())); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.isLoading) { + AvilaSnackBar.info( + context: context, + content: const Text('Sending data...'), + ).show(context); + } + if (state.isFailure) { + state as SendDataFailure; + + AvilaSnackBar.failure( + context: context, + content: Text('Failed to send data: ${state.error}'), + ).show(context); + } + if (state.isSuccess) { + state as SendDataSuccess; + AvilaSnackBar.success( + context: context, + content: Column( + children: [ + const Text('Data sent successfully!'), + const SizedBox(height: 4), + Text(state.data.toString()), + ], + ), + ).show(context); + } + }, + builder: (context, state) { + return ListView( + padding: const EdgeInsets.all(24), + children: [ + FormTextInput( + title: 'Name', + enabled: !state.isLoading, + controller: controllers[0], + ), + const SizedBox(height: 24), + FormTextInput( + title: 'Email', + enabled: !state.isLoading, + controller: controllers[1], + ), + const SizedBox(height: 24), + FormTextInput( + title: 'Phone', + enabled: !state.isLoading, + controller: controllers[2], + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: state.isLoading + ? null + : () { + context.read().add( + DataSent( + FormModel( + name: controllers[0].text, + email: controllers[1].text, + phone: controllers[2].text, + ), + ), + ); + }, + child: const Text('Send Data'), + ), + ], + ); + }, + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 175f545..700f217 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -70,8 +70,6 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true - assets: - - packages/avilatek_ui/assets/avilatek_ui/developed_by/ # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/packages/avilatek_bloc/lib/avilatek_bloc.dart b/packages/avilatek_bloc/lib/avilatek_bloc.dart index 5738f93..476db28 100644 --- a/packages/avilatek_bloc/lib/avilatek_bloc.dart +++ b/packages/avilatek_bloc/lib/avilatek_bloc.dart @@ -7,3 +7,4 @@ export 'src/permission_handler/permission_handler_listener.dart'; export 'src/pick_and_upload_file/pick_and_upload_file_bloc.dart'; export 'src/remote_data/remote_data_bloc.dart'; export 'src/remote_data_paginated/paged_remote_data_bloc.dart'; +export 'src/send_data/send_data_bloc.dart'; diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart index 5e0187d..47df18f 100644 --- a/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_bloc.dart @@ -12,14 +12,14 @@ abstract class SendDataBloc extends Bloc { /// SendDataBloc() : super(SendDataReady()) { _handler = SendDataEventHandler(); - on>(_mapDataSentToState); + on(_mapDataSentToState); } late SendDataEventHandler _handler; /// Propagates the [DataSent] event down to the corresponding event /// handler. Future _mapDataSentToState( - DataSent event, + DataSent event, Emitter emit, ) async { return _handleStatesOnEvent( @@ -54,10 +54,10 @@ abstract class SendDataBloc extends Bloc { /// Implements the code that calls the data sending function. /// - /// The sent data must be returned by this function. + /// Sent data must be returned by this function. @visibleForTesting Future sendData( SendDataState oldState, - DataSent event, + DataSent event, ); } diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart index 277cc12..4f69233 100644 --- a/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_event.dart @@ -15,7 +15,7 @@ abstract class SendDataEvent extends Equatable { /// {@template data_sent} /// Event that triggers the fetching of the send data. /// {@endtemplate} -class DataSent extends SendDataEvent { +class DataSent extends SendDataEvent { /// {@macro fetch_send_data} const DataSent(this.data, {this.simulateError = false}); @@ -23,7 +23,7 @@ class DataSent extends SendDataEvent { final bool? simulateError; /// The data that is to be sent. - final T data; + final dynamic data; @override List get props => [simulateError, data]; diff --git a/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart b/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart index b569747..8ab10f5 100644 --- a/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart +++ b/packages/avilatek_bloc/lib/src/send_data/send_data_handler.dart @@ -14,10 +14,10 @@ class SendDataEventHandler { /// On failure it emits: [SendDataLoading], /// [SendDataFailure], [SendDataReady]. Future mapDataSentToState( - DataSent event, + DataSent event, SendDataReady state, Emitter emit, - Future Function(SendDataState, DataSent) sendData, + Future Function(SendDataState, DataSent) sendData, ) async { try { emit(SendDataLoading()); @@ -28,7 +28,7 @@ class SendDataEventHandler { await sendData(state, event); - emit(SendDataSuccess(event.data)); + emit(SendDataSuccess(event.data)); emit(SendDataReady()); } catch (e) { emit(SendDataFailure(event.data, e));