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
Name | Type | Description | Default |
---|---|---|---|
color | Color? | Color of the circular progress indicator | Theme's primary color |
thickness | double? | Stroke width of the circular indicator | 4.0 |
size | double? | Diameter of the circular indicator | 20.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
- Appropriate Sizing: Choose sizes that fit the context (small for buttons, large for full-screen loading)
- Color Contrast: Ensure sufficient contrast against background
- Consistent Theming: Use theme colors for consistency across the app
- Loading Context: Show loaders only when necessary and hide them promptly
- Accessibility: Provide semantic labels for screen readers
- 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
- LinearProgressIndicatorAppi - For linear progress indicators
- ButtonAppi - For buttons with loading statesspinning circle for loading states