Implementing Shimmer Effect Animation on List Items in Flutter
When an application fetches data, especially for lists or feeds, users often experience a brief waiting period. Displaying a static loading indicator can feel unresponsive. A "Shimmer Effect" provides a much more engaging and visually appealing alternative. It's a subtle animation that gives the impression of content being loaded, making the user experience smoother and more pleasant. This article will guide you through implementing a professional Shimmer effect animation on list items in your Flutter application.
Why Use a Shimmer Effect?
The Shimmer effect simulates the layout of the content that is about to appear, typically by displaying a gradient animation over placeholder shapes. This technique offers several benefits:
- Improved User Experience: It makes the app feel faster and more responsive, as the UI isn't completely static during loading.
- Visual Engagement: The animation keeps the user engaged rather than staring at a blank screen or a simple spinner.
- Contextual Loading: Users can anticipate the structure of the incoming content, which is more informative than a generic loader.
Getting Started: Adding the Shimmer Package
For an efficient and ready-to-use Shimmer effect, we'll leverage the popular
shimmer package from pub.dev. First, add it to your
pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
shimmer: ^3.0.0 # Use the latest stable version
After adding the dependency, run
flutter pub get in your terminal to fetch the package.
Basic Shimmer Usage
The
shimmer package provides a
Shimmer.fromColors widget that animates a child widget by overlaying a gradient. You define a
baseColor and a
highlightColor for the shimmering effect.
Here's a simple example applying shimmer to a text widget:
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class BasicShimmerExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Basic Shimmer')),
body: Center(
child: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Text(
'Loading Text...',
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}
Creating a Shimmer Placeholder Widget for List Items
To apply the shimmer effect effectively to list items, you need to create a placeholder widget that mimics the visual structure of your actual list item. This placeholder will be wrapped by
Shimmer.fromColors.
Let's create a
ShimmerListItemPlaceholder that resembles a typical list item with an avatar and some text lines.
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ShimmerListItemPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(
color: Colors.white, // Placeholder color for shimmer effect
shape: BoxShape.circle,
),
),
const SizedBox(width: 12.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 16.0,
color: Colors.white,
),
const SizedBox(height: 8.0),
Container(
width: MediaQuery.of(context).size.width * 0.7, // Shorter line
height: 16.0,
color: Colors.white,
),
],
),
),
],
),
);
}
}
Notice that the
color property for the placeholder elements is set to
Colors.white. This is because
Shimmer.fromColors will paint its gradient over the child's
baseColor and
highlightColor, making the white child elements appear to shimmer.
Integrating Shimmer into a List View
Now, let's combine the
ShimmerListItemPlaceholder with a
ListView.builder to display the shimmer effect while data is loading. We'll use a simple
Future.delayed to simulate network fetching.
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
// Start of ShimmerListItemPlaceholder definition
class ShimmerListItemPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60.0,
height: 60.0,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 16.0,
color: Colors.white,
),
const SizedBox(height: 8.0),
Container(
width: MediaQuery.of(context).size.width * 0.7,
height: 16.0,
color: Colors.white,
),
],
),
),
],
),
);
}
}
// End of ShimmerListItemPlaceholder definition
// Start of MyListItem definition
class MyListItem {
final String title;
final String subtitle;
final String imageUrl;
MyListItem({required this.title, required this.subtitle, required this.imageUrl});
}
// End of MyListItem definition
// Start of ShimmerListScreen definition
class ShimmerListScreen extends StatefulWidget {
@override
_ShimmerListScreenState createState() => _ShimmerListScreenState();
}
class _ShimmerListScreenState extends State {
bool _isLoading = true;
List _items = [];
@override
void initState() {
super.initState();
_loadData();
}
Future _loadData() async {
setState(() {
_isLoading = true;
});
// Simulate network delay
await Future.delayed(Duration(seconds: 3));
_items = List.generate(
10,
(index) => MyListItem(
title: 'Item Title $index',
subtitle: 'This is the subtitle for item $index.',
imageUrl: 'https://via.placeholder.com/150/0000FF/FFFFFF?text=Item+$index', // Example image URL
),
);
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Shimmer List Example')),
body: _isLoading
? ListView.builder(
itemCount: 8, // Number of shimmer items to show
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: ShimmerListItemPlaceholder(),
);
},
)
: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final item = _items[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(item.imageUrl),
),
const SizedBox(width: 12.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4.0),
Text(
item.subtitle,
style: TextStyle(
fontSize: 14.0,
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
},
),
);
}
}
// End of ShimmerListScreen definition
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Shimmer Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ShimmerListScreen(),
);
}
}
In this example:
- We define a
_isLoading boolean state variable.
- While
_isLoading is true, ListView.builder renders multiple instances of Shimmer.fromColors wrapping ShimmerListItemPlaceholder.
- Once the simulated data fetching (
_loadData) completes, _isLoading becomes false, and the ListView.builder then renders the actual Card widgets with the fetched data.
Customization Options
The
Shimmer.fromColors widget offers several properties for customization:
baseColor and highlightColor: Control the primary and secondary colors of the gradient.
period: Determines the duration of one complete shimmer animation cycle (default is Duration(milliseconds: 1500)).
direction: Specifies the direction of the shimmer effect (e.g., ShimmerDirection.ltr for left-to-right, ShimmerDirection.rtl for right-to-left, ShimmerDirection.ttb for top-to-bottom, ShimmerDirection.btt for bottom-to-top).
loop: Number of times to loop the animation. Setting it to 0 will make it loop infinitely (default).
shimmer: You can also provide a custom Shimmer object for advanced control.
Conclusion
Implementing a Shimmer effect animation in Flutter is an effective way to improve the perceived performance and overall user experience of your application. By using the
shimmer package and creating custom placeholder widgets that mirror your actual content structure, you can provide users with an engaging and professional loading indicator for list items and other data-intensive UI components. This technique transforms dull waiting times into a visually rich and informative experience.