LCOV - code coverage report
Current view: top level - buttons - single_press_button.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 43 44 97.7 %
Date: 2024-11-26 10:38:40 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:flutter/material.dart';
       2             : 
       3             : import 'custom_action_button.dart';
       4             : 
       5             : /// A customizable button that ensures the [onPressed] callback is invoked
       6             : /// only once per press. It prevents multiple invocations during a single press,
       7             : /// making it ideal for handling actions that shouldn't be executed multiple times
       8             : /// concurrently, such as network requests.
       9             : ///
      10             : /// The [SinglePressButton] provides options to display a loading indicator
      11             : /// while processing, customize its appearance, and handle processing states
      12             : /// with callbacks.
      13             : ///
      14             : /// Example usage:
      15             : /// ```dart
      16             : /// SinglePressButton(
      17             : ///   onPressed: () async {
      18             : ///     await performNetworkRequest();
      19             : ///   },
      20             : ///   child: Text('Submit'),
      21             : ///   showLoadingIndicator: true,
      22             : ///   backgroundColor: Colors.blue,
      23             : ///   disabledColor: Colors.blueAccent,
      24             : ///   borderRadius: 12.0,
      25             : ///   padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      26             : ///   width: 200,
      27             : ///   height: 50,
      28             : ///   onStartProcessing: () {
      29             : ///     // Optional: Actions to perform when processing starts.
      30             : ///   },
      31             : ///   onFinishProcessing: () {
      32             : ///     // Optional: Actions to perform when processing finishes.
      33             : ///   },
      34             : ///   onError: (error) {
      35             : ///     // Optional: Handle errors during processing.
      36             : ///   },
      37             : /// );
      38             : /// ```
      39             : class SinglePressButton extends StatefulWidget {
      40             :   /// The widget below this widget in the tree.
      41             :   ///
      42             :   /// Typically, this is the content of the button, such as text or an icon.
      43             :   final Widget child;
      44             : 
      45             :   /// The callback that is called when the button is tapped.
      46             :   ///
      47             :   /// This callback can be asynchronous and is invoked only once per press.
      48             :   /// It is responsible for handling the primary action of the button.
      49             :   final Future<void> Function() onPressed;
      50             : 
      51             :   /// The amount of space to surround the child inside the button.
      52             :   ///
      53             :   /// If not specified, default padding is applied.
      54             :   final EdgeInsetsGeometry? padding;
      55             : 
      56             :   /// The external margin around the button.
      57             :   ///
      58             :   /// This margin wraps the button, providing space between it and other widgets.
      59             :   final EdgeInsetsGeometry? margin;
      60             : 
      61             :   /// The background color of the button when enabled.
      62             :   ///
      63             :   /// If not specified, the button uses the theme's primary color.
      64             :   final Color? backgroundColor;
      65             : 
      66             :   /// The background color of the button when disabled.
      67             :   ///
      68             :   /// This color is displayed when the button is in a processing state.
      69             :   /// If not specified, it defaults to the theme's disabled color.
      70             :   final Color? disabledColor;
      71             : 
      72             :   /// The border radius of the button.
      73             :   ///
      74             :   /// Controls the roundness of the button's corners.
      75             :   /// Defaults to 8.0.
      76             :   final double borderRadius;
      77             : 
      78             :   /// The text style for the button's label.
      79             :   ///
      80             :   /// If not specified, it inherits the theme's text style.
      81             :   final TextStyle? textStyle;
      82             : 
      83             :   /// The elevation of the button.
      84             :   ///
      85             :   /// Controls the shadow depth of the button.
      86             :   /// If not specified, it defaults to the theme's elevated button elevation.
      87             :   final double? elevation;
      88             : 
      89             :   /// The shape of the button's material.
      90             :   ///
      91             :   /// Allows for customizing the button's outline and borders.
      92             :   /// If not specified, a rounded rectangle is used.
      93             :   final OutlinedBorder? shape;
      94             : 
      95             :   /// Whether to show a loading indicator while processing.
      96             :   ///
      97             :   /// If set to `true`, a [CircularProgressIndicator] is displayed on top of the button's child.
      98             :   /// Defaults to `false`.
      99             :   final bool showLoadingIndicator;
     100             : 
     101             :   /// The color of the loading indicator.
     102             :   ///
     103             :   /// If not specified, it defaults to the theme's [ColorScheme.onPrimary].
     104             :   final Color? loadingIndicatorColor;
     105             : 
     106             :   /// The width of the button.
     107             :   ///
     108             :   /// If not specified, the button will size itself based on its child's size constraints.
     109             :   final double? width;
     110             : 
     111             :   /// The height of the button.
     112             :   ///
     113             :   /// If not specified, the button will size itself based on its child's size constraints.
     114             :   final double? height;
     115             : 
     116             :   /// Callback invoked when the button starts processing.
     117             :   ///
     118             :   /// Useful for triggering actions like disabling other UI elements.
     119             :   final VoidCallback? onStartProcessing;
     120             : 
     121             :   /// Callback invoked when the button finishes processing.
     122             :   ///
     123             :   /// Useful for resetting states or triggering subsequent actions.
     124             :   final VoidCallback? onFinishProcessing;
     125             : 
     126             :   /// Callback invoked when an error occurs during processing.
     127             :   ///
     128             :   /// Provides a way to handle exceptions thrown by the [onPressed] callback.
     129             :   final void Function(Object error)? onError;
     130             : 
     131             :   /// Creates a [SinglePressButton].
     132             :   ///
     133             :   /// The [child] and [onPressed] parameters are required.
     134             :   /// The [borderRadius] defaults to 8.0, and [showLoadingIndicator] defaults to `false`.
     135           1 :   const SinglePressButton({
     136             :     super.key,
     137             :     required this.child,
     138             :     required this.onPressed,
     139             :     this.padding,
     140             :     this.margin,
     141             :     this.backgroundColor,
     142             :     this.disabledColor,
     143             :     this.borderRadius = 8.0,
     144             :     this.textStyle,
     145             :     this.elevation,
     146             :     this.shape,
     147             :     this.showLoadingIndicator = false,
     148             :     this.loadingIndicatorColor,
     149             :     this.width,
     150             :     this.height,
     151             :     this.onStartProcessing,
     152             :     this.onFinishProcessing,
     153             :     this.onError,
     154             :   });
     155             : 
     156           1 :   @override
     157           1 :   State<SinglePressButton> createState() => _SinglePressButtonState();
     158             : }
     159             : 
     160             : class _SinglePressButtonState extends State<SinglePressButton> {
     161             :   /// Indicates whether the button is currently processing an action.
     162             :   ///
     163             :   /// When `true`, the button is disabled, and a loading indicator is shown if enabled.
     164             :   bool _isProcessing = false;
     165             : 
     166             :   /// Handles the button press by invoking [widget.onPressed].
     167             :   ///
     168             :   /// Ensures that the callback is invoked only once per press.
     169             :   /// Manages the processing state and handles optional callbacks for processing events.
     170           1 :   Future<void> _handlePress() async {
     171           1 :     if (_isProcessing) return;
     172             : 
     173           2 :     setState(() {
     174           1 :       _isProcessing = true;
     175             :     });
     176             : 
     177             :     // Invoke onStartProcessing callback if provided.
     178           3 :     widget.onStartProcessing?.call();
     179             : 
     180             :     try {
     181           3 :       await widget.onPressed();
     182             :     } catch (error) {
     183             :       // Invoke onError callback if provided.
     184           2 :       if (widget.onError != null) {
     185           3 :         widget.onError!(error);
     186             :       } else {
     187             :         // If no onError is provided, rethrow the exception.
     188             :         rethrow;
     189             :       }
     190             :     } finally {
     191           1 :       if (mounted) {
     192           2 :         setState(() {
     193           1 :           _isProcessing = false;
     194             :         });
     195             : 
     196             :         // Invoke onFinishProcessing callback if provided.
     197           3 :         widget.onFinishProcessing?.call();
     198             :       }
     199             :     }
     200             :   }
     201             : 
     202           1 :   @override
     203             :   Widget build(BuildContext context) {
     204             :     // Determine the button's background color based on its state.
     205           1 :     final Color backgroundColor = _isProcessing
     206           4 :         ? (widget.disabledColor ?? Theme.of(context).disabledColor)
     207           4 :         : (widget.backgroundColor ?? Theme.of(context).primaryColor);
     208             : 
     209             :     // Determine the text style, merging with provided [textStyle] if any.
     210           2 :     final TextStyle effectiveTextStyle = widget.textStyle ??
     211           4 :         Theme.of(context).textTheme.labelLarge!.copyWith(
     212           2 :               color: widget.backgroundColor != null
     213           0 :                   ? Theme.of(context).colorScheme.onPrimary
     214           4 :                   : Theme.of(context).textTheme.labelLarge!.color,
     215             :             );
     216             : 
     217           1 :     return Container(
     218           2 :       margin: widget.margin, // Apply the external margin here
     219           1 :       child: CustomActionButton(
     220           2 :         onPressed: _isProcessing ? null : _handlePress,
     221           2 :         padding: widget.padding,
     222             :         backgroundColor: backgroundColor,
     223           2 :         borderRadius: widget.borderRadius,
     224           2 :         elevation: widget.elevation,
     225           2 :         shape: widget.shape,
     226           2 :         width: widget.width,
     227           2 :         height: widget.height,
     228           1 :         child: Stack(
     229             :           alignment: Alignment.center,
     230           1 :           children: [
     231             :             // Original child
     232           1 :             DefaultTextStyle(
     233             :               style: effectiveTextStyle,
     234           2 :               child: widget.child,
     235             :             ),
     236             : 
     237             :             // Loading indicator overlay
     238           3 :             if (_isProcessing && widget.showLoadingIndicator)
     239           1 :               SizedBox(
     240             :                 width: 20,
     241             :                 height: 20,
     242           1 :                 child: CircularProgressIndicator(
     243           1 :                   valueColor: AlwaysStoppedAnimation<Color>(
     244           2 :                     widget.loadingIndicatorColor ??
     245           3 :                         Theme.of(context).colorScheme.onPrimary,
     246             :                   ),
     247             :                   strokeWidth: 2.5,
     248             :                 ),
     249             :               ),
     250             :           ],
     251             :         ),
     252             :       ),
     253             :     );
     254             :   }
     255             : }

Generated by: LCOV version 1.14