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
isScrollControlledthe 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.