Theory
Six tickets, one thumb
Group bookings arrive: up to 6 tickets per student. A TextField accepting "six" and "6six" is the old pain; the natural control is a slider the thumb drags from 1 to 6.
You wire Slider with its min and max, run it, drag: and the thumb snaps straight back to 1 every time. Not broken: Flutter is teaching you its single most important interaction law, and this lesson is built around learning it properly: right after the button family gets its ranks.
Theory
The button family, by rank
All buttons share the same heart (onPressed + child, null disables): what differs is visual rank:
- ElevatedButton: raised, filled: THE primary action (Book)
- OutlinedButton: bordered: middle-rank actions (Add to wishlist)
- TextButton: flat text: secondary, low-noise (Cancel, Skip)
- IconButton: icon-only, compact: toolbars (share, favourite)
- FloatingActionButton: the round floating one, living in Scaffold's own
floatingActionButton:slot: one per screen, its defining action
Rank = visual loudness = importance: one Elevated per region, quiet Texts beside it.
Practical
The quantity slider, done right
class _BookingState extends State<BookingScreen> {
double tickets = 1; // the STATE owns the value
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Tickets: ${tickets.toInt()}'),
Slider(
value: tickets, // slider DISPLAYS the state
min: 1,
max: 6,
divisions: 5, // discrete stops: 1,2,3,4,5,6
label: '${tickets.toInt()}', // bubble at the thumb
onChanged: (v) {
setState(() { tickets = v; }); // no setState = snap-back
},
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: () {}, child: const Text('Cancel')),
ElevatedButton(
onPressed: () { /* book tickets.toInt() seats */ },
child: const Text('Book'),
),
],
),
],
);
}
}
Theory
The controlled-widget law
Why did the naked slider snap back? Because a Flutter Slider does not own its position. Read the listing's loop:
value: tickets: the slider DISPLAYS whatever the state variable holds- dragging fires
onChanged(v)with the CANDIDATE value: continuously, throughout the drag - your handler setStates the variable : build reruns : the slider now displays the new value
Skip the setState and the variable never changes, so every rebuild repaints the thumb at the old value: snap-back. The widget is a view of state, never the keeper of it: and Checkbox, Radio and Switch, waiting in the next lessons, obey the identical law.
Quiz
A Slider has correct min, max and value, and onChanged: (v) { tickets = v; } WITHOUT setState. What does the user experience?
- The slider works: the variable is being updated after all
- The thumb snaps back on every drag: the variable changes but no rebuild repaints the slider with it
- A compile error: onChanged must call setState
- The slider moves but the Text label lags one drag behind
Show the answer
The thumb snaps back on every drag: the variable changes but no rebuild repaints the slider with it
The variable DOES change (option A's grain of truth): but the screen only repaints on rebuild, and only setState schedules one: so the slider keeps being rebuilt from... nothing, since no rebuild happens: it repaints at the old position on the next frame of the drag gesture, snapping back. This is the controlled-widget law in one bug: widgets display state; setState publishes changes. Option C: the compiler cannot know your intent: this is a logic bug, the worst kind. Option D describes a subtler staleness that other mistakes cause: this one is total.
Think first
Rank the screen's buttons
The booking screen wants: Book (the point of the screen), Cancel, a heart icon to wishlist, and a screen-wide New Booking action visible over the whole event list. Assign each its button variant with the ranking reason, then tap.
Show the answer
Book: ElevatedButton: the one loud primary per region. Cancel: TextButton: present but quiet, never competing with Book (the listing places them exactly so). Wishlist heart: IconButton: compact, recognisable, toolbar-grade. New Booking over the list: FloatingActionButton in the Scaffold slot: THE screen action by Material convention (think WhatsApp's compose button). The principle: visual loudness should match importance: 2 Elevated buttons side by side is a shouting match, and exams reward saying so.
Watch out
Slider and button slips
Slider value is a double: displaying it raw shows 3.0 tickets: .toInt() for humans (the listing does, twice).
divisions off by one: stops = divisions + 1: range 1 to 6 needs divisions: 5: the fencepost again.
onChanged fires DURING the drag: continuously, not once at release: heavy work there stutters the gesture (the TextWatcher lesson's rule, third appearance).
Two FABs: the Scaffold slot takes one; a second floating action is a design smell, not a parameter.
Theory
One law, four widgets ahead
Frame the controlled-widget law once and keep it lit: value from state, change through onChanged + setState, widget never keeps its own score. The next lesson's Checkbox and Radio are this law verbatim (with Radio adding a clever twist on exclusivity that BCA404's GroupBox veterans will want to compare), and Unit 5's Switch is it again. Learn the loop here, collect the marks 3 more times.
Summary
Key takeaways
- Button ranks: ElevatedButton primary, OutlinedButton middle, TextButton quiet, IconButton compact, FAB in Scaffold's slot for THE screen action.
- All share onPressed (null = disabled) + child; loudness should match importance.
- Slider(value, min, max, divisions, label, onChanged): value is a double the STATE owns.
- Controlled-widget law: the widget displays state; onChanged + setState publish changes; without setState the thumb snaps back.
- divisions + 1 = stops; label bubbles at the thumb; onChanged fires continuously during drag.
- The same law governs Checkbox, Radio and Switch next.
- Memory hook: widgets show the score, setState writes it.