Skip to main content

CircleLoaderAppi

A customizable circular loading indicator widget that provides smooth animations and flexible styling options for indicating loading states and progress.

Features

  • Smooth Animation: Continuous circular progress animation
  • Custom Colors: Configurable color schemes
  • Variable Thickness: Adjustable stroke width
  • Flexible Sizing: Customizable diameter
  • Material Design: Follows Material Design guidelines
  • Performance Optimized: Efficient rendering and animation
  • Accessibility: Screen reader compatible

Usage

Basic Circular Loader

CircleLoaderAppi()

Custom Color and Size

CircleLoaderAppi(
color: Colors.blue,
size: 50.0,
thickness: 4.0,
)

Large Loader with Custom Styling

CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 80.0,
thickness: 6.0,
)

Parameters

NameTypeDescriptionDefault
colorColor?Color of the circular progress indicatorTheme's primary color
thicknessdouble?Stroke width of the circular indicator4.0
sizedouble?Diameter of the circular indicator20.0

Behavior

Animation

  • Continuous rotation animation
  • Smooth transitions and easing
  • Consistent animation speed across different sizes
  • Automatically starts when widget is mounted

Visual Appearance

  • Circular Shape: Perfect circle with customizable stroke
  • Color Theming: Adapts to app theme or uses custom colors
  • Responsive: Maintains aspect ratio across different screen densities
  • Centered: Automatically centers within available space

Performance

  • Optimized for smooth 60fps animation
  • Minimal CPU and GPU usage
  • Efficient memory management
  • Hardware acceleration support

Best Practices

  1. Appropriate Sizing: Choose sizes that fit the context (small for buttons, large for full-screen loading)
  2. Color Contrast: Ensure sufficient contrast against background
  3. Consistent Theming: Use theme colors for consistency across the app
  4. Loading Context: Show loaders only when necessary and hide them promptly
  5. Accessibility: Provide semantic labels for screen readers
  6. Performance: Avoid multiple simultaneous loaders on the same screen

Examples

Button Loading State

class LoadingButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;

LoadingButton({required this.text, this.onPressed});


_LoadingButtonState createState() => _LoadingButtonState();
}

class _LoadingButtonState extends State<LoadingButton> {
bool isLoading = false;

void _handlePress() async {
if (widget.onPressed == null || isLoading) return;

setState(() => isLoading = true);

try {
await widget.onPressed!();
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}


Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : _handlePress,
child: isLoading
? Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleLoaderAppi(
color: Colors.white,
size: 16.0,
thickness: 2.0,
),
SizedBox(width: 8),
Text('Loading...'),
],
)
: Text(widget.text),
);
}
}

Full Screen Loading Overlay

class LoadingOverlay extends StatelessWidget {
final bool isLoading;
final Widget child;
final String? message;

LoadingOverlay({
required this.isLoading,
required this.child,
this.message,
});


Widget build(BuildContext context) {
return Stack(
children: [
child,
if (isLoading)
Container(
color: Colors.black54,
child: Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 48.0,
thickness: 4.0,
),
if (message != null) ...[
SizedBox(height: 16),
Text(
message!,
style: Theme.of(context).textTheme.bodyLarge,
),
],
],
),
),
),
),
),
],
);
}
}

// Usage
LoadingOverlay(
isLoading: _isProcessing,
message: 'Processing your request...',
child: YourMainContent(),
)

List Item Loading State

class ListItemWithLoading extends StatefulWidget {
final String title;
final String subtitle;
final VoidCallback? onTap;

ListItemWithLoading({
required this.title,
required this.subtitle,
this.onTap,
});


_ListItemWithLoadingState createState() => _ListItemWithLoadingState();
}

class _ListItemWithLoadingState extends State<ListItemWithLoading> {
bool isLoading = false;

void _handleTap() async {
if (widget.onTap == null || isLoading) return;

setState(() => isLoading = true);

try {
await widget.onTap!();
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}


Widget build(BuildContext context) {
return ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
trailing: isLoading
? CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 24.0,
thickness: 3.0,
)
: Icon(Icons.chevron_right),
onTap: _handleTap,
);
}
}

Card Loading State

class LoadingCard extends StatelessWidget {
final bool isLoading;
final Widget? child;
final String? loadingMessage;

LoadingCard({
required this.isLoading,
this.child,
this.loadingMessage,
});


Widget build(BuildContext context) {
return Card(
child: Container(
height: 200,
child: isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 40.0,
thickness: 4.0,
),
if (loadingMessage != null) ...[
SizedBox(height: 16),
Text(
loadingMessage!,
style: Theme.of(context).textTheme.bodyMedium,
),
],
],
),
)
: child,
),
);
}
}

Refresh Indicator

class CustomRefreshIndicator extends StatefulWidget {
final Widget child;
final Future<void> Function() onRefresh;

CustomRefreshIndicator({
required this.child,
required this.onRefresh,
});


_CustomRefreshIndicatorState createState() => _CustomRefreshIndicatorState();
}

class _CustomRefreshIndicatorState extends State<CustomRefreshIndicator> {
bool isRefreshing = false;

Future<void> _handleRefresh() async {
setState(() => isRefreshing = true);

try {
await widget.onRefresh();
} finally {
if (mounted) {
setState(() => isRefreshing = false);
}
}
}


Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: Stack(
children: [
widget.child,
if (isRefreshing)
Positioned(
top: 50,
left: 0,
right: 0,
child: Center(
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 24.0,
thickness: 3.0,
),
),
),
),
],
),
);
}
}

Multi-State Loading Widget

enum LoadingState { idle, loading, success, error }

class MultiStateWidget extends StatefulWidget {

_MultiStateWidgetState createState() => _MultiStateWidgetState();
}

class _MultiStateWidgetState extends State<MultiStateWidget> {
LoadingState currentState = LoadingState.idle;
String? errorMessage;

Future<void> _performAction() async {
setState(() {
currentState = LoadingState.loading;
errorMessage = null;
});

try {
// Simulate network request
await Future.delayed(Duration(seconds: 2));

// Simulate random success/failure
if (DateTime.now().millisecond % 2 == 0) {
setState(() => currentState = LoadingState.success);
} else {
throw Exception('Random error occurred');
}
} catch (e) {
setState(() {
currentState = LoadingState.error;
errorMessage = e.toString();
});
}
}


Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 200,
height: 200,
child: Card(
child: Center(
child: _buildStateWidget(),
),
),
),
SizedBox(height: 24),
ElevatedButton(
onPressed: currentState == LoadingState.loading ? null : _performAction,
child: Text('Start Action'),
),
],
),
);
}

Widget _buildStateWidget() {
switch (currentState) {
case LoadingState.idle:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow, size: 48),
SizedBox(height: 8),
Text('Ready to start'),
],
);

case LoadingState.loading:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleLoaderAppi(
color: Theme.of(context).primaryColor,
size: 48.0,
thickness: 4.0,
),
SizedBox(height: 16),
Text('Processing...'),
],
);

case LoadingState.success:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, color: Colors.green, size: 48),
SizedBox(height: 8),
Text('Success!'),
],
);

case LoadingState.error:
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 8),
Text('Error occurred'),
if (errorMessage != null)
Text(
errorMessage!,
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
],
);
}
}
}

See Also