venue_seat_picker
Interactive venue seat maps for Flutter: a zoomable viewer, optimistic reservation picker and grid editor behind one consistent model seam.
Built for and used in production by Festapp, including the live application at live.festapp.net.

Watch the higher-quality mobile WebM demo
| Customer picker | Venue editor |
|---|---|
![]() |
![]() |
The picker and editor operate on the same controller and venue document, so an application can offer customer selection and staff editing without maintaining two layout implementations. The media above uses an anonymized venue plan adapted from Festapp's production deployment at vstupenky.online.
Features
- Zoom, pan and automatic fit-to-viewport.
- Optimistic async selection with rollback and duplicate-tap protection.
- Selection limits, pending indicators and custom eligibility rules.
- Grid editor with immutable create/update delegates.
- Custom seat builders, tooltips, colors and SVG/image backdrops.
- Versioned JSON through
VenueSeatandVenueSeatDocument. - Read-only
SeatAdapter; the package never mutates your domain objects. - Android, iOS, Linux, macOS, web and Windows support.
Quick start
dependencies:
venue_seat_picker: ^0.1.0
final seats = [
const VenueSeat(id: 'A1', position: SeatPosition(0, 0), label: 'A1'),
const VenueSeat(id: 'A2', position: SeatPosition(0, 1), label: 'A2'),
];
final controller = VenueSeatController<VenueSeat, Object>(
adapter: venueSeatAdapter,
)..loadPlan(rows: 4, columns: 8, seats: seats);
VenueSeatPicker<VenueSeat, Object>(
controller: controller,
maxSelectedSeats: 4,
onSelectionRequested: (request) async {
return request.selected
? reservations.hold(request.seatId)
: reservations.release(request.seatId);
},
onSelectionChanged: (selectedIds) {
debugPrint('$selectedIds');
},
)
Create the controller in State.initState and dispose it with its owning
widget. The runnable example application demonstrates selection,
pending state, zoom, a detailed SVG floor plan, status rendering and live
layout editing.
Use an existing domain model
Map it through a read-only adapter. Pricing, authorization, persistence and reservation locking remain in your application.
final adapter = SeatAdapter<MySeat, int>(
idOf: (seat) => seat.id,
positionOf: (seat) => SeatPosition(seat.row, seat.column),
statusOf: (seat) => switch (seat.status) {
MyStatus.free => SeatStatus.available,
MyStatus.held => SeatStatus.held,
MyStatus.sold => SeatStatus.booked,
},
labelOf: (seat) => seat.label,
groupOf: (seat) => seat.sectionId,
);
final controller = VenueSeatController<MySeat, int>(adapter: adapter)
..loadPlan(rows: venue.rows, columns: venue.columns, seats: venue.seats);
Current-user selection is an optimistic UI overlay, not a SeatStatus. This
keeps the package from writing transient state into your authoritative model.
Editor
VenueSeatEditor<VenueSeat, Object>(
controller: controller,
editing: SeatEditingDelegate(
create: (position, status) => VenueSeat(
id: '${position.row}:${position.column}',
position: position,
status: status,
),
withStatus: (seat, status) => seat.copyWith(status: status),
),
onChanged: saveDraft,
)
Guides
Contract
- Rows, columns and seat size must be positive.
- Seat IDs and positions must be unique and inside the venue grid.
- Missing positions are empty slots, not a seat status.
- Shrinking refuses to clip occupied slots.
- Only
availableseats can be selected; selected seats can be released. - A rejected or failed request restores the previous optimistic selection.
Flutter 3.32+ and Dart 3.8+ are supported. The package intentionally has no backend or state-management dependency.
See CONTRIBUTING.md, SECURITY.md, and the issue tracker.
Licensed under the MIT License.

