Loading indicators are crucial for providing feedback to users during data fetching or processing tasks in mobile applications. While Flutter offers a default CircularProgressIndicator
and LinearProgressIndicator
, customizing these indicators can greatly enhance the user experience by aligning with the app’s design language and branding.
Why Customize Loading Indicators?
- Branding Consistency: Match the app’s visual style.
- Enhanced User Experience: Make loading states more engaging.
- Differentiate Your App: Stand out with unique animations.
Approaches to Creating Custom Loading Indicators in Flutter
Flutter provides several ways to create custom loading indicators:
- Using existing widgets and modifying their properties.
- Creating custom painters for intricate designs.
- Using animated widgets and transitions.
- Utilizing third-party libraries.
Method 1: Modifying Existing Widgets
You can customize the default CircularProgressIndicator
and LinearProgressIndicator
by adjusting their colors, stroke widths, and background properties.
Custom Circular Progress Indicator
import 'package:flutter/material.dart';
class CustomCircularProgressIndicator extends StatelessWidget {
final double? value; // Nullable for indeterminate state
final Color? color;
const CustomCircularProgressIndicator({Key? key, this.value, this.color}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 40, // Fixed size for visibility, adjust as needed
height: 40,
child: CircularProgressIndicator(
value: value,
strokeWidth: 5,
valueColor: AlwaysStoppedAnimation(color ?? Theme.of(context).primaryColor),
backgroundColor: Colors.grey[200],
),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Circular Progress Indicator')),
body: const Center(
child: CustomCircularProgressIndicator(color: Colors.orange), //Example Use
),
),
),
);
}
Usage example:
CustomCircularProgressIndicator(color: Colors.orange, value: 0.75),
Key modifications:
valueColor
: Change the indicator’s color.strokeWidth
: Adjust the thickness of the indicator’s stroke.backgroundColor
: Set a background color for the track.value
: A nullable value represents a percentage or null for an indeterminate state
Custom Linear Progress Indicator
import 'package:flutter/material.dart';
class CustomLinearProgressIndicator extends StatelessWidget {
final double? value;
final Color? color;
const CustomLinearProgressIndicator({Key? key, this.value, this.color}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200, // or desired size
child: LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(color ?? Theme.of(context).primaryColor),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Linear Progress Indicator')),
body: const Center(
child: CustomLinearProgressIndicator(color: Colors.green, value: 0.6), // Example Use
),
),
),
);
}
Usage example:
CustomLinearProgressIndicator(color: Colors.green, value: 0.6),
Method 2: Creating Custom Painters
For more intricate and unique designs, you can create custom loading indicators using Flutter’s CustomPainter
. This approach allows you to draw anything you can imagine.
Example: A Pulsating Circle Loader
import 'dart:math';
import 'package:flutter/material.dart';
class PulsatingCirclePainter extends CustomPainter {
final Animation animation;
PulsatingCirclePainter({required this.animation}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final scale = sin(animation.value * pi); // Sine wave for pulsing effect
final paint = Paint()
..color = Colors.blue.withOpacity(0.5 + 0.5 * scale)
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius * scale, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false; // Repaint handled by AnimationController
}
}
class PulsatingCircleLoadingIndicator extends StatefulWidget {
const PulsatingCircleLoadingIndicator({Key? key}) : super(key: key);
@override
_PulsatingCircleLoadingIndicatorState createState() => _PulsatingCircleLoadingIndicatorState();
}
class _PulsatingCircleLoadingIndicatorState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(); // Animate indefinitely
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: PulsatingCirclePainter(animation: _controller),
size: const Size(50, 50), // Size of the painted area
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Pulsating Circle Loading Indicator')),
body: const Center(
child: PulsatingCircleLoadingIndicator(), //Example Use
),
),
),
);
}
Key components:
PulsatingCirclePainter
: Draws a circle with a radius that pulses using a sine wave.AnimationController
: Controls the animation, causing the circle to pulsate continuously.
Method 3: Animated Widgets and Transitions
Flutter’s built-in animated widgets such as AnimatedOpacity
, AnimatedScale
, and AnimatedContainer
can be combined to create interesting loading indicators without needing custom painting.
Example: Fading Dots Loader
import 'package:flutter/material.dart';
class FadingDotsLoader extends StatefulWidget {
final Color color;
final double size;
const FadingDotsLoader({Key? key, this.color = Colors.blue, this.size = 10.0}) : super(key: key);
@override
_FadingDotsLoaderState createState() => _FadingDotsLoaderState();
}
class _FadingDotsLoaderState extends State with TickerProviderStateMixin {
late List _animationControllers;
late List> _animations;
@override
void initState() {
super.initState();
_animationControllers = List.generate(3, (index) => AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
));
_animations = _animationControllers.map((controller) =>
Tween(begin: 0.3, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
)).toList();
for (int i = 0; i < _animationControllers.length; i++) {
Future.delayed(Duration(milliseconds: 200 * i), () {
_animationControllers[i].repeat(reverse: true);
});
}
}
@override
void dispose() {
for (var controller in _animationControllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: AnimatedOpacity(
opacity: _animations[index].value,
duration: const Duration(milliseconds: 600),
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
),
),
),
)),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Custom Fading Dots Loader')),
body: const Center(
child: FadingDotsLoader(), //Example Use
),
),
),
);
}
Explanation:
- Three dots are created, each animated to fade in and out.
AnimatedOpacity
is used to control the opacity of each dot.AnimationController
s are used to control the fading animation for each dot.
Method 4: Utilizing Third-Party Libraries
Several Flutter packages provide a wide variety of pre-built, customizable loading indicators.
dependencies:
flutter:
sdk: flutter
flutter_spinkit: ^5.2.0
Example using flutter_spinkit
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class ThirdPartyLoadingIndicator extends StatelessWidget {
const ThirdPartyLoadingIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const SpinKitFadingCircle(
color: Colors.green,
size: 50.0,
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Third-Party Loading Indicator')),
body: const Center(
child: ThirdPartyLoadingIndicator(), //Example Use
),
),
),
);
}
Some popular libraries:
Conclusion
Custom loading indicators are an effective way to enhance the user experience and maintain brand consistency in Flutter applications. Whether by modifying existing widgets, creating custom painters, or utilizing third-party libraries, Flutter offers ample flexibility to create unique and engaging loading animations.