DEV3 Technologies home

Rating bottom sheet

Modal bottom sheet with tappable stars, optional quick tags and a comment box that returns a typed result to the caller.

FlutterFeedback and empty statesflutterratingbottomsheetreview

Code

import 'package:flutter/material.dart';

class RatingResult {
  const RatingResult(this.stars, this.tags, this.comment);
  final int stars;
  final Set<String> tags;
  final String comment;
}

/// Returns null when the user dismisses the sheet.
Future<RatingResult?> showRatingSheet(BuildContext context,
    {List<String> tags = const ['On time', 'Polite', 'Great quality']}) {
  return showModalBottomSheet<RatingResult>(
    context: context,
    isScrollControlled: true, // lets the sheet rise above the keyboard
    showDragHandle: true,
    builder: (_) => _RatingSheet(tags: tags),
  );
}

class _RatingSheet extends StatefulWidget {
  const _RatingSheet({required this.tags});
  final List<String> tags;
  @override
  State<_RatingSheet> createState() => _RatingSheetState();
}

class _RatingSheetState extends State<_RatingSheet> {
  int _stars = 0;
  final _picked = <String>{};
  final _comment = TextEditingController();

  @override
  void dispose() {
    _comment.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final bottom = MediaQuery.of(context).viewInsets.bottom;
    return Padding(
      padding: EdgeInsets.fromLTRB(20, 0, 20, 20 + bottom),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Text('How was it?', style: Theme.of(context).textTheme.titleLarge),
        Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          for (var i = 1; i <= 5; i++)
            IconButton(
              tooltip: '$i stars',
              onPressed: () => setState(() => _stars = i),
              icon: Icon(i <= _stars ? Icons.star : Icons.star_border, size: 36, color: Colors.amber),
            ),
        ]),
        Wrap(spacing: 8, children: [
          for (final t in widget.tags)
            FilterChip(
              label: Text(t),
              selected: _picked.contains(t),
              onSelected: (on) => setState(() => on ? _picked.add(t) : _picked.remove(t)),
            ),
        ]),
        const SizedBox(height: 12),
        TextField(
          controller: _comment,
          maxLines: 3,
          maxLength: 300,
          decoration: const InputDecoration(hintText: 'Add a comment (optional)', border: OutlineInputBorder()),
        ),
        FilledButton(
          onPressed: _stars == 0
              ? null
              : () => Navigator.pop(context, RatingResult(_stars, _picked, _comment.text.trim())),
          child: const Text('Submit'),
        ),
      ]),
    );
  }
}

How it works

What it solves

After a job finishes you want a rating without sending people to a new page. A bottom sheet keeps context, and returning a typed value keeps the calling code simple.

How it works

showRatingSheet wraps showModalBottomSheet and resolves to a RatingResult or null on dismissal. The sheet holds the star count, the selected tags and the comment in local state. The submit button stays disabled until at least one star is chosen, so empty ratings are impossible.

isScrollControlled plus bottom padding from viewInsets pushes the sheet above the keyboard when the comment field gains focus. Each star is an IconButton with a tooltip, which gives tap targets and screen reader labels for free.

Adapting it

Pass tags that fit the context, for example different options for low and high scores by rebuilding the chip list from _stars. Post the result to your API in the caller, and show a thank-you snackbar. For half stars, track a double and render Icons.star_half.

Pitfalls

  • Return only on submit; swiping the sheet away yields null, so never treat null as a zero rating.
  • Without isScrollControlled the comment field is hidden behind the keyboard.
  • Limit comment length on the server too.

How to use it

Copy the file to lib/feedback/. No packages.

final r = await showRatingSheet(context);
if (r != null) await api.sendRating(r.stars, r.tags.toList(), r.comment);

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets