import 'package:flutter/material.dart'; /// Reusable loading indicator widget /// /// Provides different loading indicator variants: /// - Circular (default) /// - Linear /// - Overlay (full screen with backdrop) /// - With message /// /// Usage: /// ```dart /// // Simple circular indicator /// LoadingIndicator() /// /// // With custom size and color /// LoadingIndicator( /// size: 50, /// color: Colors.blue, /// ) /// /// // Linear indicator /// LoadingIndicator.linear() /// /// // Full screen overlay /// LoadingIndicator.overlay( /// message: 'Loading data...', /// ) /// /// // Centered with message /// LoadingIndicator.withMessage( /// message: 'Please wait...', /// ) /// ``` class LoadingIndicator extends StatelessWidget { /// Size of the loading indicator final double? size; /// Color of the loading indicator final Color? color; /// Stroke width for circular indicator final double strokeWidth; /// Whether to use linear progress indicator final bool isLinear; /// Optional loading message final String? message; /// Text style for the message final TextStyle? messageStyle; /// Spacing between indicator and message final double messageSpacing; const LoadingIndicator({ super.key, this.size, this.color, this.strokeWidth = 4.0, this.message, this.messageStyle, this.messageSpacing = 16.0, }) : isLinear = false; /// Create a linear loading indicator const LoadingIndicator.linear({ super.key, this.color, this.message, this.messageStyle, this.messageSpacing = 16.0, }) : isLinear = true, size = null, strokeWidth = 4.0; /// Create a full-screen loading overlay static Widget overlay({ String? message, Color? backgroundColor, Color? indicatorColor, TextStyle? messageStyle, }) { return _LoadingOverlay( message: message, backgroundColor: backgroundColor, indicatorColor: indicatorColor, messageStyle: messageStyle, ); } /// Create a loading indicator with a message below it static Widget withMessage({ required String message, double size = 40, Color? color, TextStyle? messageStyle, double spacing = 16.0, }) { return LoadingIndicator( size: size, color: color, message: message, messageStyle: messageStyle, messageSpacing: spacing, ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final indicatorColor = color ?? theme.colorScheme.primary; Widget indicator; if (isLinear) { indicator = LinearProgressIndicator( color: indicatorColor, backgroundColor: indicatorColor.withOpacity(0.1), ); } else { indicator = SizedBox( width: size ?? 40, height: size ?? 40, child: CircularProgressIndicator( color: indicatorColor, strokeWidth: strokeWidth, ), ); } // If there's no message, return just the indicator if (message == null) { return indicator; } // If there's a message, wrap in column return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ indicator, SizedBox(height: messageSpacing), Text( message!, style: messageStyle ?? theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ); } } /// Full-screen loading overlay class _LoadingOverlay extends StatelessWidget { final String? message; final Color? backgroundColor; final Color? indicatorColor; final TextStyle? messageStyle; const _LoadingOverlay({ this.message, this.backgroundColor, this.indicatorColor, this.messageStyle, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( color: backgroundColor ?? Colors.black.withOpacity(0.5), child: Center( child: Card( elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 50, height: 50, child: CircularProgressIndicator( color: indicatorColor ?? theme.colorScheme.primary, strokeWidth: 4, ), ), if (message != null) ...[ const SizedBox(height: 24), SizedBox( width: 200, child: Text( message!, style: messageStyle ?? theme.textTheme.bodyLarge, textAlign: TextAlign.center, ), ), ], ], ), ), ), ), ); } } /// Shimmer loading effect for list items class ShimmerLoading extends StatefulWidget { /// Width of the shimmer container final double? width; /// Height of the shimmer container final double height; /// Border radius final double borderRadius; /// Base color final Color? baseColor; /// Highlight color final Color? highlightColor; const ShimmerLoading({ super.key, this.width, this.height = 16, this.borderRadius = 4, this.baseColor, this.highlightColor, }); @override State createState() => _ShimmerLoadingState(); } class _ShimmerLoadingState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); _animation = Tween(begin: -1.0, end: 2.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final baseColor = widget.baseColor ?? theme.colorScheme.surfaceVariant; final highlightColor = widget.highlightColor ?? theme.colorScheme.surface; return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( width: widget.width, height: widget.height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ baseColor, highlightColor, baseColor, ], stops: [ 0.0, _animation.value, 1.0, ], ), ), ); }, ); } } /// Loading state for list items class ListLoadingIndicator extends StatelessWidget { /// Number of shimmer items to show final int itemCount; /// Height of each item final double itemHeight; /// Spacing between items final double spacing; const ListLoadingIndicator({ super.key, this.itemCount = 5, this.itemHeight = 80, this.spacing = 12, }); @override Widget build(BuildContext context) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: itemCount, separatorBuilder: (context, index) => SizedBox(height: spacing), itemBuilder: (context, index) => ShimmerLoading( height: itemHeight, width: double.infinity, borderRadius: 8, ), ); } }