Date and time slot picker
Horizontal day strip with a grid of time slot chips that disables unavailable slots and reports the chosen DateTime.
FlutterFormsflutterbookingcalendarslots
Code
import 'package:flutter/material.dart';
typedef SlotLoader = Future<Set<TimeOfDay>> Function(DateTime day); // free slots
class SlotPicker extends StatefulWidget {
const SlotPicker({super.key, required this.load, required this.onChanged, this.days = 14});
final SlotLoader load;
final ValueChanged<DateTime?> onChanged;
final int days;
@override
State<SlotPicker> createState() => _SlotPickerState();
}
class _SlotPickerState extends State<SlotPicker> {
static final _all = [for (var h = 9; h < 18; h++) TimeOfDay(hour: h, minute: 0)];
late final DateTime _today = DateUtils.dateOnly(DateTime.now());
late DateTime _day = _today;
TimeOfDay? _slot;
Future<Set<TimeOfDay>>? _free;
@override
void initState() {
super.initState();
_free = widget.load(_day);
}
void _pickDay(DateTime d) {
setState(() {
_day = d;
_slot = null;
_free = widget.load(d);
});
widget.onChanged(null);
}
@override
Widget build(BuildContext context) {
final loc = MaterialLocalizations.of(context);
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 72,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: widget.days,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (_, i) {
final d = _today.add(Duration(days: i));
return ChoiceChip(
selected: DateUtils.isSameDay(d, _day),
onSelected: (_) => _pickDay(d),
label: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(loc.narrowWeekdays[d.weekday % 7]),
Text('${d.day}', style: const TextStyle(fontWeight: FontWeight.bold)),
]),
);
},
),
),
const SizedBox(height: 12),
FutureBuilder<Set<TimeOfDay>>(
future: _free,
builder: (context, snap) {
if (!snap.hasData) return const LinearProgressIndicator();
return Wrap(spacing: 8, runSpacing: 8, children: [
for (final t in _all)
ChoiceChip(
label: Text(loc.formatTimeOfDay(t)),
selected: _slot == t,
// Past times today are also unavailable.
onSelected: snap.data!.contains(t) && !_isPast(t)
? (_) {
setState(() => _slot = t);
widget.onChanged(DateTime(_day.year, _day.month, _day.day, t.hour, t.minute));
}
: null,
),
]);
},
),
]);
}
bool _isPast(TimeOfDay t) =>
DateUtils.isSameDay(_day, _today) &&
DateTime(_day.year, _day.month, _day.day, t.hour, t.minute).isBefore(DateTime.now());
}
How it works
What it solves
Booking forms need a day first and then only the times that are still free. The picker must reset the time when the day changes and never allow slots in the past.
How it works
A horizontally scrolling list of ChoiceChips shows the next days. Picking a day clears the selected time, starts a new load(day) future and tells the parent that nothing is selected yet. A FutureBuilder renders the time grid: each slot is a chip that becomes unselectable (null onSelected) when the server did not return it as free or when it is earlier than now on today's date.
Labels come from MaterialLocalizations, so weekday letters and time formats follow the device locale and 12 or 24 hour setting.
Adapting it
Change the _all list or generate it from opening hours and a service duration. Send the returned DateTime to your booking endpoint, converted to UTC. For multi-staff shops, add the staff id as a parameter of the loader.
Pitfalls
DateUtils.dateOnlystrips the time so day comparisons are reliable; avoid comparing rawDateTimes.- The loader result is cached per chosen day only through the stored future; re-selecting reloads.
- Time zones: slots are in the viewer's zone, so convert if the venue lives elsewhere.
How to use it
Copy the file to lib/booking/. No packages.
SlotPicker(
load: (day) => api.freeSlots(day),
onChanged: (when) => setState(() => selected = when),
)
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.