image

06 Jan 2026

9K

35K

Building a Rating Bar Widget with Gesture Interaction in Flutter

Introduction

Rating bars are a ubiquitous UI component in modern applications, allowing users to provide feedback easily and intuitively. While many libraries offer pre-built rating widgets, understanding how to construct one from scratch in Flutter, especially with gesture interaction, provides invaluable insight into Flutter's core principles like state management and gesture detection. This article will guide you through building a customizable rating bar widget that responds to tap and drag gestures, offering a rich user experience.

Core Concepts

StatefulWidget for Dynamic UI

A rating bar needs to change its appearance (e.g., number of filled stars) based on user interaction. For this dynamic behavior, we must use a StatefulWidget. A StatefulWidget is a widget that has mutable state, meaning it can be rebuilt to reflect changes in its internal data. The state is managed by a corresponding State object.

GestureDetector for Interactive Elements

Flutter provides the GestureDetector widget, a powerful tool for detecting various common gestures such as taps, drags, and long presses. By wrapping our rating bar elements (or the entire row of stars) with a GestureDetector, we can capture user input and update the rating accordingly. For a rating bar, we'll primarily leverage onTapDown (or onTapUp) for precise taps and onHorizontalDragUpdate for a smooth dragging experience.

Designing the Rating Bar Widget

Widget Definition

First, let's define our RatingBar widget. It will extend StatefulWidget and take some parameters for customization, such as the total number of stars, their size, and color.


import 'package:flutter/material.dart';

class RatingBar extends StatefulWidget {
  final int itemCount;
  final double initialRating;
  final double itemSize;
  final Color color;
  final Color unratedColor;
  final ValueChanged<double> onRatingUpdate;

  const RatingBar({
    Key? key,
    this.itemCount = 5,
    this.initialRating = 0.0,
    this.itemSize = 30.0,
    this.color = Colors.amber,
    this.unratedColor = Colors.grey,
    required this.onRatingUpdate,
  }) : assert(itemCount > 0),
       assert(initialRating >= 0.0 && initialRating <= itemCount),
       super(key: key);

  @override
  _RatingBarState createState() => _RatingBarState();
}

State Management

Our _RatingBarState will hold the current rating as a double value, allowing for half-star precision. We'll initialize it with the initialRating provided by the widget and update it using setState.


class _RatingBarState extends State<RatingBar> {
  late double _currentRating;

  @override
  void initState() {
    super.initState();
    _currentRating = widget.initialRating;
  }

  @override
  void didUpdateWidget(covariant RatingBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initialRating != widget.initialRating) {
      _currentRating = widget.initialRating;
    }
  }

  // ... build method and helper functions will go here
}

Building Individual Stars

We need a helper method to return the correct icon for each star based on the current rating. This method will determine if a star should be fully filled, half-filled, or empty.


  Widget _buildStar(int index) {
    IconData iconData;
    Color iconColor;

    if (index >= _currentRating) {
      // Empty star
      iconData = Icons.star_border;
      iconColor = widget.unratedColor;
    } else if (index + 1 == _currentRating.ceil() && _currentRating % 1 != 0) {
      // Half-filled star
      iconData = Icons.star_half;
      iconColor = widget.color;
    } else {
      // Fully filled star
      iconData = Icons.star;
      iconColor = widget.color;
    }

    return Icon(
      iconData,
      color: iconColor,
      size: widget.itemSize,
    );
  }

Implementing Gesture Interaction

To capture gestures across the entire row of stars, we'll wrap the Row with a GestureDetector. We'll use onHorizontalDragUpdate to allow users to drag their finger across the stars, and onTapDown for single taps. The key is to calculate which star the gesture corresponds to based on the local position within the GestureDetector.


  void _updateRating(Offset localPosition) {
    final double rating = (localPosition.dx / widget.itemSize).clamp(0.0, widget.itemCount.toDouble());
    setState(() {
      _currentRating = rating;
    });
    widget.onRatingUpdate(_currentRating);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onHorizontalDragUpdate: (details) => _updateRating(details.localPosition),
      onTapDown: (details) => _updateRating(details.localPosition),
      child: Row(
        mainAxisSize: MainAxisSize.min, // Essential to constrain width
        children: List.generate(widget.itemCount, (index) => _buildStar(index)),
      ),
    );
  }

The _updateRating function calculates the new rating by dividing the local X-position of the gesture by the size of a single star. clamp ensures the rating stays within the valid range (0 to itemCount).

Putting It All Together (Full Rating Bar Widget)

Here is the complete code for our customizable RatingBar widget:


import 'package:flutter/material.dart';

class RatingBar extends StatefulWidget {
  final int itemCount;
  final double initialRating;
  final double itemSize;
  final Color color;
  final Color unratedColor;
  final ValueChanged<double> onRatingUpdate;

  const RatingBar({
    Key? key,
    this.itemCount = 5,
    this.initialRating = 0.0,
    this.itemSize = 30.0,
    this.color = Colors.amber,
    this.unratedColor = Colors.grey,
    required this.onRatingUpdate,
  }) : assert(itemCount > 0),
       assert(initialRating >= 0.0 && initialRating <= itemCount),
       super(key: key);

  @override
  _RatingBarState createState() => _RatingBarState();
}

class _RatingBarState extends State<RatingBar> {
  late double _currentRating;

  @override
  void initState() {
    super.initState();
    _currentRating = widget.initialRating;
  }

  @override
  void didUpdateWidget(covariant RatingBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initialRating != widget.initialRating) {
      // Update internal state if the initialRating passed from parent changes
      _currentRating = widget.initialRating;
    }
  }

  // Helper method to determine which icon to display for each star
  Widget _buildStar(int index) {
    IconData iconData;
    Color iconColor;

    // Logic for fully filled, half-filled, or empty star
    if (index >= _currentRating) {
      // Empty star
      iconData = Icons.star_border;
      iconColor = widget.unratedColor;
    } else if (index + 1 == _currentRating.ceil() && _currentRating % 1 != 0) {
      // Half-filled star
      iconData = Icons.star_half;
      iconColor = widget.color;
    } else {
      // Fully filled star
      iconData = Icons.star;
      iconColor = widget.color;
    }

    return Icon(
      iconData,
      color: iconColor,
      size: widget.itemSize,
    );
  }

  // Method to update the rating based on gesture position
  void _updateRating(Offset localPosition) {
    // Calculate the new rating based on the local X position and itemSize
    final double rating = (localPosition.dx / widget.itemSize).clamp(0.0, widget.itemCount.toDouble());
    setState(() {
      _currentRating = rating; // Update the internal state
    });
    widget.onRatingUpdate(_currentRating); // Notify the parent widget of the change
  }

  @override
  Widget build(BuildContext context) {
    // Wrap the Row of stars with GestureDetector to detect interactions
    return GestureDetector(
      onHorizontalDragUpdate: (details) => _updateRating(details.localPosition),
      onTapDown: (details) => _updateRating(details.localPosition),
      child: Row(
        mainAxisSize: MainAxisSize.min, // Ensures the row only takes up as much space as its children
        children: List.generate(widget.itemCount, (index) => _buildStar(index)),
      ),
    );
  }
}

Usage Example

To use the RatingBar widget, simply place it in your widget tree and provide an onRatingUpdate callback to receive the new rating value.


import 'package:flutter/material.dart';
// Assuming your RatingBar widget is in 'rating_bar.dart'
import 'package:your_app_name/rating_bar.dart'; 

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: 'Rating Bar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Custom Rating Bar'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Rate this item:',
                style: TextStyle(fontSize: 20),
              ),
              const SizedBox(height: 20),
              RatingBar(
                initialRating: _userRating,
                itemCount: 5,
                itemSize: 40,
                color: Colors.deepPurple,
                unratedColor: Colors.deepPurple.shade100,
                onRatingUpdate: (rating) {
                  setState(() {
                    _userRating = rating;
                  });
                  print('New rating: $rating'); // For debugging
                },
              ),
              const SizedBox(height: 20),
              Text(
                'Your current rating: ${_userRating.toStringAsFixed(1)}',
                style: const TextStyle(fontSize: 18),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Conclusion

By combining Flutter's StatefulWidget for managing dynamic UI and GestureDetector for robust user interaction, we've successfully built a flexible and interactive rating bar widget. This approach provides fine-grained control over the widget's appearance and behavior, allowing for extensive customization without relying on external packages. Understanding these fundamental concepts empowers you to create custom, engaging user interfaces tailored precisely to your application's needs.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is