Shimmer Loading
Loading preview
Starting the Flutter app
Skeleton loading with animated gradient sweep
import 'dart:math';
import 'package:flutter/material.dart';
class ShimmerLoading extends StatefulWidget {
const ShimmerLoading({super.key});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = _controller.drive(CurveTween(curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(title: const Text('Shimmer Loading')),
body: ListView(
padding: const EdgeInsets.all(16),
children: List.generate(
5,
(i) => _ShimmerCard(
animation: _animation,
isDark: isDark,
),
),
),
);
}
}
class _ShimmerCard extends StatelessWidget {
final Animation<double> animation;
final bool isDark;
const _ShimmerCard({
required this.animation,
required this.isDark,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ShimmerBox(
width: double.infinity,
height: 120,
animation: animation,
isDark: isDark,
),
const SizedBox(height: 12),
_ShimmerBox(
width: 200,
height: 16,
animation: animation,
isDark: isDark,
),
const SizedBox(height: 8),
_ShimmerBox(
width: 150,
height: 14,
animation: animation,
isDark: isDark,
),
],
),
),
);
}
}
class _ShimmerBox extends StatelessWidget {
static const _bandWidth = 240.0;
final double width;
final double height;
final Animation<double> animation;
final bool isDark;
const _ShimmerBox({
required this.width,
required this.height,
required this.animation,
required this.isDark,
});
@override
Widget build(BuildContext context) {
final baseColor = isDark ? Colors.grey[800]! : Colors.grey[300]!;
final highlightColor = isDark ? Colors.grey[600]! : Colors.grey[100]!;
// The band is placed in pixels from the screen width. Every box shares a
// left edge, so one band crosses them all at the same speed.
final travel = MediaQuery.sizeOf(context).width + _bandWidth;
return LayoutBuilder(
builder: (context, constraints) {
final boxWidth = min(width, constraints.maxWidth);
// Converts a pixel offset within the box to an Alignment x value.
double alignX(double pixels) => pixels / boxWidth * 2 - 1;
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final bandStart = animation.value * travel - _bandWidth;
return Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: LinearGradient(
begin: Alignment(alignX(bandStart), 0),
end: Alignment(alignX(bandStart + _bandWidth), 0),
colors: [baseColor, highlightColor, baseColor],
),
),
);
},
);
},
);
}
}
Add it to your project
Copies the Dart source into your app, so you own it. Flutter SDK only, no extra packages.
First time? Run dart pub global activate fluttertemplates_dev, or read the CLI guide.
How it works
A 1500 ms repeating AnimationController drives _animation from 0 to 1, and each _ShimmerBox turns that into a highlight band placed in pixels, using the screen width and its own measured width. Every box shares a left edge, so one 240 px band crosses them all at the same speed, whatever their size. Only each box’s AnimatedBuilder rebuilds per frame.
Adapting it
- Show the
_ShimmerCards while your future is pending, since the demo never resolves to content. - Match
_ShimmerBoxsizes to your real layout, and takebaseColorandhighlightColorfrom the theme. - Boxes that don’t start at the same x need their offset added to
bandStartto stay in line.