Building a Star Rating Widget in Flutter
Star rating widgets are a common and essential UI component for applications that involve user feedback, reviews, or product evaluation. They provide an intuitive visual representation of quality or satisfaction, allowing users to quickly rate items with a simple tap. In Flutter, creating such a widget is straightforward, leveraging its powerful declarative UI framework and state management capabilities.
This article will guide you through the process of building a reusable and customizable star rating widget in Flutter. We'll cover the core concepts, implementation details, and how to make it interactive and configurable.
Core Concepts
At its heart, a star rating widget consists of a row of star icons. The number of filled stars typically reflects the current rating. To make it interactive, we need to handle user taps on individual stars to update the rating. This requires managing the widget's internal state.
We'll use a StatefulWidget for this purpose, as the rating changes dynamically based on user interaction. Key elements will include:
- A
Rowwidget to arrange the stars horizontally. Iconwidgets to represent individual stars.GestureDetectororInkWellwidgets to detect taps on each star.- State variables to store the current rating.
- Callbacks to inform parent widgets when the rating changes.
1. Widget Skeleton
First, let's set up the basic structure of our StarRating widget as a StatefulWidget. This will include the constructor for initial parameters and the state class.
import 'package:flutter/material.dart';
class StarRating extends StatefulWidget {
final int starCount;
final double initialRating;
final double starSize;
final Color color;
final Color? borderColor;
final void Function(double rating)? onRatingUpdate;
const StarRating({
Key? key,
this.starCount = 5,
this.initialRating = 0.0,
this.starSize = 24.0,
this.color = Colors.amber,
this.borderColor,
this.onRatingUpdate,
}) : assert(initialRating >= 0 && initialRating <= starCount),
super(key: key);
@override
State<StarRating> createState() => _StarRatingState();
}
class _StarRatingState extends State<StarRating> {
late double _currentRating;
@override
void initState() {
super.initState();
_currentRating = widget.initialRating;
}
@override
void didUpdateWidget(covariant StarRating oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialRating != widget.initialRating) {
_currentRating = widget.initialRating;
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(widget.starCount, (index) {
return _buildStar(context, index);
}),
);
}
Widget _buildStar(BuildContext context, int index) {
// Implementation for building individual stars will go here
return Container(); // Placeholder
}
}
In this initial setup:
starCount: The total number of stars to display (e.g., 5).initialRating: The starting rating value.starSize: The size of each star icon.color: The color of the filled stars.borderColor: Optional color for outlined stars when not filled.onRatingUpdate: A callback function that gets triggered when the user changes the rating._currentRating: An internal state variable to hold the currently selected rating.initStateanddidUpdateWidget: To initialize and update_currentRatingbased oninitialRating.
2. Displaying Stars and Handling Interaction
Now, let's implement the _buildStar method to render each star and make it interactive. We'll use GestureDetector to capture taps on individual stars. The logic will determine whether a star should be filled, partially filled, or empty based on the _currentRating.
// ... (inside _StarRatingState class)
Widget _buildStar(BuildContext context, int index) {
IconData iconData;
Color iconColor;
if (index >= _currentRating) {
// Empty star
iconData = Icons.star_border;
iconColor = widget.borderColor ?? Colors.grey;
} else if (index + 1 > _currentRating && index < _currentRating) {
// Half-filled star
iconData = Icons.star_half;
iconColor = widget.color;
} else {
// Filled star
iconData = Icons.star;
iconColor = widget.color;
}
return GestureDetector(
onTap: () {
setState(() {
_currentRating = index + 1.0;
});
widget.onRatingUpdate?.call(_currentRating);
},
child: Icon(
iconData,
color: iconColor,
size: widget.starSize,
),
);
}
Explanation:
- The
indexparameter helps us determine the position of the star. - We use conditional logic to decide whether to display a
star_border(empty),star_half(half-filled), orstar(filled) icon. - The
GestureDetectorwraps eachIcon, making it tappable. - When a star is tapped,
setStateis called to update_currentRatingto the index of the tapped star plus one (since indices are 0-based). - Finally,
widget.onRatingUpdate?.call(_currentRating)invokes the callback, notifying the parent widget about the rating change.
3. Full Code Example
Here's the complete, runnable code for the StarRating widget and an example of how to use it in a simple Flutter application.
import 'package:flutter/material.dart';
class StarRating extends StatefulWidget {
final int starCount;
final double initialRating;
final double starSize;
final Color color;
final Color? borderColor;
final void Function(double rating)? onRatingUpdate;
const StarRating({
Key? key,
this.starCount = 5,
this.initialRating = 0.0,
this.starSize = 24.0,
this.color = Colors.amber,
this.borderColor,
this.onRatingUpdate,
}) : assert(initialRating >= 0 && initialRating <= starCount),
super(key: key);
@override
State<StarRating> createState() => _StarRatingState();
}
class _StarRatingState extends State<StarRating> {
late double _currentRating;
@override
void initState() {
super.initState();
_currentRating = widget.initialRating;
}
@override
void didUpdateWidget(covariant StarRating oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialRating != widget.initialRating) {
_currentRating = widget.initialRating;
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(widget.starCount, (index) {
return _buildStar(context, index);
}),
);
}
Widget _buildStar(BuildContext context, int index) {
IconData iconData;
Color iconColor;
if (index >= _currentRating) {
// Empty star
iconData = Icons.star_border;
iconColor = widget.borderColor ?? Colors.grey;
} else if (index + 1 > _currentRating && index < _currentRating) {
// Half-filled star
iconData = Icons.star_half;
iconColor = widget.color;
} else {
// Filled star
iconData = Icons.star;
iconColor = widget.color;
}
return GestureDetector(
onTap: () {
setState(() {
_currentRating = index + 1.0;
});
widget.onRatingUpdate?.call(_currentRating);
},
child: Icon(
iconData,
color: iconColor,
size: widget.starSize,
),
);
}
}
// --- Example Usage ---
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
double _userRating = 3.5; // Example initial rating
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Star Rating Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Star Rating Widget'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Rate this item:',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 20),
StarRating(
initialRating: _userRating,
starCount: 5,
starSize: 40.0,
color: Colors.deepOrange,
borderColor: Colors.deepOrange.withOpacity(0.5),
onRatingUpdate: (rating) {
setState(() {
_userRating = rating;
});
print('New rating: $rating');
},
),
const SizedBox(height: 20),
Text(
'Your current rating: ${_userRating.toStringAsFixed(1)}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 40),
const Text(
'Another example (read-only):',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 20),
StarRating(
initialRating: 4.2,
starCount: 5,
starSize: 30.0,
color: Colors.green,
// No onRatingUpdate means it's read-only
),
],
),
),
),
);
}
}
Conclusion
You have now successfully created a customizable and interactive star rating widget in Flutter! This widget can be easily integrated into various parts of your application, from product review screens to feedback forms. By understanding the core principles of StatefulWidget, GestureDetector, and conditional UI rendering, you can extend this widget further to include features like half-star selection on drag, different icon sets, or more complex animations.