Sitemap

Restaurant Booking Process

9 min readDec 2, 2025

--

Restaurant Booking Process from Apps Development with Flutter Path MySkill.id

Yuk subscribe untuk mendapatkan email notifikasi setiap ada artikel terbaru oleh MySkill

Press enter or click to view image in full size
Sumber: My Skill

Di era digital, kemudahan memesan meja restoran secara langsung dari smartphone menjadi kebutuhan primer bagi pengguna. Fitur ini tidak hanya meningkatkan pengalaman pelanggan tetapi juga membantu restoran mengelola kapasitas dan mengurangi no-show. Flutter menawarkan solusi ideal untuk membangun sistem pemesanan yang intuitif dengan performa tinggi. Kita akan membuat alur pemesanan restoran yang lengkap, mulai dari pemilihan tanggal hingga konfirmasi booking.

1. Persiapan Proyek dan Struktur Data

Sebelum memulai, kita perlu menyiapkan struktur proyek yang terorganisir:

a. Struktur Folder

lib/
├── models/
│ ├── restaurant_model.dart
│ └── booking_model.dart
├── screens/
│ ├── home_screen.dart
│ ├── restaurant_detail_screen.dart
│ └── booking_screen.dart
├── widgets/
│ ├── booking_form.dart
│ └── success_dialog.dart
└── main.dart

b. Model Data Booking

Buat file models/booking_model.dart:

class Booking {
final String id; // ID unik untuk setiap pemesanan
final String restaurantId; // ID restoran yang dipesan
final DateTime bookingDate; // Tanggal pemesanan
final String bookingTime; // Waktu pemesanan (contoh: "19:00")
int guestCount; // Jumlah tamu
String? specialNotes; // Catatan khusus (opsional)
DateTime createdAt; // Waktu pemesanan dibuat
String status; // Status: "pending", "confirmed", "cancelled"

Booking({
required this.id,
required this.restaurantId,
required this.bookingDate,
required this.bookingTime,
required this.guestCount,
this.specialNotes,
required this.createdAt,
required this.status,
});

// Konversi ke JSON untuk penyimpanan
Map<String, dynamic> toJson() {
return {
'id': id,
'restaurantId': restaurantId,
'bookingDate': bookingDate.toIso8601String(),
'bookingTime': bookingTime,
'guestCount': guestCount,
'specialNotes': specialNotes,
'createdAt': createdAt.toIso8601String(),
'status': status,
};
}

// Buat objek dari JSON
factory Booking.fromJson(Map<String, dynamic> json) {
return Booking(
id: json['id'],
restaurantId: json['restaurantId'],
bookingDate: DateTime.parse(json['bookingDate']),
bookingTime: json['bookingTime'],
guestCount: json['guestCount'],
specialNotes: json['specialNotes'],
createdAt: DateTime.parse(json['createdAt']),
status: json['status'],
);
}
}

Penjelasan Kode untuk Pemula:

  • class Booking: Seperti cetakan untuk membuat data pemesanan
  • final String id: Variabel yang tidak bisa diubah, berfungsi sebagai nomor identitas unik
  • DateTime bookingDate: Tipe data khusus untuk tanggal dan waktu
  • Map<String, dynamic> toJson(): Fungsi untuk mengubah data booking menjadi format JSON agar mudah disimpan
  • factory Booking.fromJson(): Fungsi khusus untuk membuat objek Booking dari data JSON

2. Desain UI Pemesanan Restoran

Kita akan membuat UI yang memandu pengguna melalui proses pemesanan dengan langkah-langkah jelas.

a. Halaman Utama Booking (screens/booking_screen.dart)

import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // Untuk format tanggal
import '../models/restaurant_model.dart';
import '../models/booking_model.dart';
import '../widgets/booking_form.dart';

class BookingScreen extends StatefulWidget {
final Restaurant restaurant;

const BookingScreen({Key? key, required this.restaurant}) : super(key: key);

@override
_BookingScreenState createState() => _BookingScreenState();
}

class _BookingScreenState extends State<BookingScreen> {
// Controller untuk mengontrol input teks
late TextEditingController _dateController;
late TextEditingController _notesController;

// Variabel state untuk menyimpan pilihan pengguna
DateTime? _selectedDate;
String? _selectedTime;
int _guestCount = 2;
bool _isLoading = false;

@override
void initState() {
super.initState();
// Inisialisasi controller saat halaman dibuat
_dateController = TextEditingController();
_notesController = TextEditingController();
}

@override
void dispose() {
// Membersihkan controller saat halaman ditutup
_dateController.dispose();
_notesController.dispose();
super.dispose();
}

// Fungsi untuk memilih tanggal dari date picker
Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(), // Tanggal awal yang ditampilkan
firstDate: DateTime.now(), // Tanggal terawal yang bisa dipilih
lastDate: DateTime.now().add(const Duration(days: 30)), // Tanggal terakhir
);

if (picked != null) {
setState(() {
_selectedDate = picked;
// Format tanggal menjadi DD/MM/YYYY
_dateController.text = DateFormat('dd/MM/yyyy').format(picked);
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pesan ${widget.restaurant.name}'),
backgroundColor: Colors.deepOrange,
),
body: _isLoading
? const Center(child: CircularProgressIndicator()) // Loading indicator
: SingleChildScrollView(
child: Column(
children: [
// Header restoran
_buildRestaurantHeader(),
// Form pemesanan
BookingForm(
dateController: _dateController,
notesController: _notesController,
selectedDate: _selectedDate,
selectedTime: _selectedTime,
guestCount: _guestCount,
onDateTap: _selectDate,
onTimeSelected: (time) {
setState(() => _selectedTime = time);
},
onGuestChanged: (count) {
setState(() => _guestCount = count);
},
onNotesChanged: (notes) {
// Tidak perlu setState karena otomatis update
},
),
// Tombol konfirmasi
_buildConfirmButton(),
],
),
),
);
}

// Widget untuk header restoran
Widget _buildRestaurantHeader() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.deepOrange,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
),
child: Column(
children: [
// Gambar restoran
CircleAvatar(
radius: 50,
backgroundImage: AssetImage(widget.restaurant.imageUrl),
),
const SizedBox(height: 16),
// Nama restoran
Text(
widget.restaurant.name,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
// Lokasi restoran
Text(
widget.restaurant.city,
style: const TextStyle(color: Colors.white70),
),
],
),
);
}

// Widget untuk tombol konfirmasi
Widget _buildConfirmButton() {
return Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isFormValid() ? _confirmBooking : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Konfirmasi Pemesanan',
style: TextStyle(fontSize: 16),
),
),
),
);
}

// Validasi form sebelum submit
bool _isFormValid() {
return _selectedDate != null &&
_selectedTime != null &&
_guestCount > 0;
}

// Proses konfirmasi pemesanan
void _confirmBooking() async {
if (!_isFormValid()) return;

setState(() => _isLoading = true);

// Simulasi proses booking ke server
await Future.delayed(const Duration(seconds: 2));

// Buat objek booking baru
final newBooking = Booking(
id: DateTime.now().millisecondsSinceEpoch.toString(), // ID unik
restaurantId: widget.restaurant.id,
bookingDate: _selectedDate!,
bookingTime: _selectedTime!,
guestCount: _guestCount,
specialNotes: _notesController.text.isNotEmpty
? _notesController.text
: null,
createdAt: DateTime.now(),
status: 'confirmed',
);

setState(() => _isLoading = false);

// Tampilkan dialog sukses
_showSuccessDialog(newBooking);
}

// Dialog konfirmasi sukses
void _showSuccessDialog(Booking booking) {
showDialog(
context: context,
barrierDismissible: false, // Tidak bisa ditutup dengan klik di luar
builder: (context) => AlertDialog(
title: const Text('Pemesanan Berhasil!'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Restoran: ${widget.restaurant.name}'),
Text('Tanggal: ${DateFormat('dd/MM/yyyy').format(booking.bookingDate)}'),
Text('Waktu: ${booking.bookingTime}'),
Text('Jumlah Tamu: ${booking.guestCount} orang'),
if (booking.specialNotes != null)
Text('Catatan: ${booking.specialNotes}'),
const SizedBox(height: 16),
const Text(
'Kode Booking: ${booking.id}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // Tutup dialog
Navigator.pop(context); // Kembali ke halaman restoran
},
child: const Text('Tutup'),
),
],
),
);
}
}

Penjelasan Kode yang Sulit:

  1. State Management dengan StatefulWidget:
  • StatefulWidget digunakan karena ada data yang berubah (tanggal, waktu, jumlah tamu)
  • setState() memberi tahu Flutter untuk memperbarui UI saat data berubah
  • initState() dijalankan sekali saat widget dibuat, cocok untuk inisialisasi
  • dispose() membersihkan resource saat widget dihapus, mencegah memory leak

2. Date Picker:

  • showDatePicker() menampilkan kalender sistem
  • firstDate dan lastDate membatasi rentang tanggal yang bisa dipilih
  • DateFormat('dd/MM/yyyy') mengubah format tanggal menjadi lebih mudah dibaca

3. Validasi Form:

  • _isFormValid() memeriksa semua field wajib sudah diisi
  • Tombol konfirmasi hanya aktif jika form valid (onPressed: _isFormValid() ? _confirmBooking : null)

4. Simulasi Proses Booking:

  • Future.delayed() mensimulasikan proses async ke server
  • DateTime.now().millisecondsSinceEpoch.toString() membuat ID unik dari timestamp

3. Widget Form Pemesanan (widgets/booking_form.dart)

import 'package:flutter/material.dart';

class BookingForm extends StatelessWidget {
final TextEditingController dateController;
final TextEditingController notesController;
final DateTime? selectedDate;
final String? selectedTime;
final int guestCount;
final VoidCallback onDateTap;
final Function(String) onTimeSelected;
final Function(int) onGuestChanged;
final Function(String) onNotesChanged;

const BookingForm({
Key? key,
required this.dateController,
required this.notesController,
required this.selectedDate,
required this.selectedTime,
required this.guestCount,
required this.onDateTap,
required this.onTimeSelected,
required this.onGuestChanged,
required this.onNotesChanged,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Pemilihan Tanggal
_buildDatePicker(),
const SizedBox(height: 20),

// Pemilihan Waktu
_buildTimePicker(),
const SizedBox(height: 20),

// Jumlah Tamu
_buildGuestSelector(),
const SizedBox(height: 20),

// Catatan Khusus
_buildSpecialNotes(),
],
),
);
}

// Widget untuk pemilihan tanggal
Widget _buildDatePicker() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Tanggal',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
GestureDetector(
onTap: onDateTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.calendar_today, color: Colors.grey),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: dateController,
enabled: false, // Tidak bisa diketik manual
decoration: const InputDecoration(
hintText: 'Pilih tanggal',
border: InputBorder.none,
),
),
),
],
),
),
),
],
);
}

// Widget untuk pemilihan waktu
Widget _buildTimePicker() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pilih Waktu',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8, // Jarak antar chip
runSpacing: 8, // Jarak antar baris
children: [
'11:00', '13:00', '15:00', '17:00', '19:00', '21:00'
].map((time) {
final isSelected = selectedTime == time;
return ChoiceChip(
label: Text(time),
selected: isSelected,
onSelected: (selected) {
onTimeSelected(selected ? time : '');
},
backgroundColor: Colors.grey[200],
selectedColor: Colors.deepOrange[100],
labelStyle: TextStyle(
color: isSelected ? Colors.deepOrange : Colors.black,
),
);
}).toList(),
),
],
);
}

// Widget untuk pemilihan jumlah tamu
Widget _buildGuestSelector() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Jumlah Tamu',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
children: [
// Tombol kurang
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: guestCount > 1
? () => onGuestChanged(guestCount - 1)
: null, // Disabled jika sudah minimum
),
// Tampilkan jumlah tamu
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'$guestCount',
style: const TextStyle(fontSize: 18),
),
),
// Tombol tambah
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: guestCount < 10
? () => onGuestChanged(guestCount + 1)
: null, // Disabled jika sudah maksimum
),
const Spacer(), // Mendorong teks ke kanan
const Text('Orang'),
],
),
],
);
}

// Widget untuk catatan khusus
Widget _buildSpecialNotes() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Catatan Khusus (Opsional)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
TextField(
controller: notesController,
maxLines: 3,
onChanged: onNotesChanged,
decoration: InputDecoration(
hintText: 'Contoh: Dekat jendela, Kursi anak, Allergi makanan',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
);
}
}

Penjelasan Widget Penting:

  1. ChoiceChip untuk Waktu:
  • ChoiceChip seperti tombol pilihan yang bisa diklik
  • selected menentukan apakah chip sedang aktif
  • onSelected dipanggil saat chip diklik
  • Wrap widget otomatis memindahkan chip ke baris baru jika tidak muat

2. Jumlah Tamu dengan Batasan:

  • guestCount > 1 mencegah jumlah tamu kurang dari 1
  • guestCount < 10 membatasi maksimum 10 tamu
  • Spacer mengisi ruang kosong untuk mendorong teks ke kanan

3. TextField untuk Catatan:

  • maxLines: 3 membuat input bisa beberapa baris
  • onChanged dipanggil setiap kali teks berubah
  • InputDecoration menghias input dengan border dan hint

4. Integrasi dengan Halaman Detail Restoran

Update restaurant_detail_screen.dart untuk menambahkan tombol booking:

// Di bagian bawah build method
Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookingScreen(
restaurant: restaurant,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Pesan Meja Sekarang',
style: TextStyle(fontSize: 16),
),
),
),
),

5. Fitur Tambahan untuk Pengalaman Lengkap

a. Notifikasi Lokal

Tambahkan package flutter_local_notifications di pubspec.yaml:

dependencies:
flutter_local_notifications: ^14.1.1

Implementasi notifikasi:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

Future<void> initNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');

final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
);

await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
);
}

Future<void> showBookingNotification(String restaurantName) async {
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'booking_channel',
'Booking Notifications',
importance: Importance.max,
priority: Priority.high,
);

const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);

await flutterLocalNotificationsPlugin.show(
0,
'Pemesanan Berhasil!',
'Meja Anda di $restaurantName telah berhasil dipesan',
platformChannelSpecifics,
);
}

Get My Skill’s stories in your inbox

Join Medium for free to get updates from this writer.

Penjelasan Notifikasi:

  • initNotifications() mengatur notifikasi untuk Android
  • AndroidNotificationDetails mengatur perilaku notifikasi (suara, getaran, dll)
  • show() menampilkan notifikasi dengan judul dan isi pesan

b. Penyimpanan Booking Lokal

Gunakan shared_preferences untuk menyimpan riwayat booking:

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

Future<void> saveBooking(Booking booking) async {
final prefs = await SharedPreferences.getInstance();
final bookings = prefs.getStringList('bookings') ?? [];
bookings.add(jsonEncode(booking.toJson()));
await prefs.setStringList('bookings', bookings);
}

Future<List<Booking>> getBookings() async {
final prefs = await SharedPreferences.getInstance();
final bookingsJson = prefs.getStringList('bookings') ?? [];
return bookingsJson
.map((json) => Booking.fromJson(jsonDecode(json)))
.toList();
}

Penjelasan Penyimpanan:

  • SharedPreferences menyimpan data sederhana di perangkat
  • jsonEncode() mengubah objek Booking menjadi string JSON
  • jsonDecode() mengubah string JSON kembali menjadi objek

c. Animasi Transisi

Tambahkan animasi saat berpindah halaman:

Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => BookingScreen(
restaurant: restaurant,
),
),
);

6. Penanganan Error dan Validasi

a. Validasi Tanggal

Future<void> _selectDate() async {
// Daftar tanggal fully booked
final fullyBookedDates = [
DateTime.now().add(const Duration(days: 3)),
DateTime.now().add(const Duration(days: 7)),
];

final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 30)),
selectableDayPredicate: (day) {
// Cek apakah tanggal sudah fully booked
return !fullyBookedDates.any((date) =>
day.year == date.year &&
day.month == date.month &&
day.day == date.day);
},
);

if (picked != null) {
setState(() {
_selectedDate = picked;
_dateController.text = DateFormat('dd/MM/yyyy').format(picked);
});
} else {
// Tampilkan pesan jika tidak ada tanggal dipilih
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Silakan pilih tanggal')),
);
}
}

b. Validasi Jam Operasional

bool _isWithinOperatingHours() {
if (_selectedTime == null) return false;

final hour = int.parse(_selectedTime!.split(':')[0]);
// Jam operasional 10:00 - 22:00
return hour >= 10 && hour <= 22;
}

void _confirmBooking() {
if (!_isWithinOperatingHours()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Restoran tutup pada jam tersebut')),
);
return;
}

// Lanjutkan proses booking...
}

c. Pesan Error yang Ramah

void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Oops!'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Mengerti'),
),
],
),
);
}

7. Pengujian Fitur

a. Test Case Dasar

  1. Pemilihan Tanggal:
  • Klik field tanggal → Date picker muncul
  • Pilih tanggal → Field terisi dengan format DD/MM/YYYY
  • Pilih tanggal fully booked → Tidak bisa dipilih

2. Pemilihan Waktu:

  • Klik chip waktu → Chip berubah warna
  • Hanya satu chip yang bisa aktif
  • Klik chip aktif → Chip menjadi non-aktif

3. Jumlah Tamu:

  • Tekan tombol (-) → Jumlah berkurang
  • Tekan tombol (+) → Jumlah bertambah
  • Jumlah tidak bisa kurang dari 1 atau lebih dari 10

b. Test Case Edge Cases

  1. Submit Form Kosong:
  • Tekan tombol konfirmasi → Tombol tidak aktif
  • Tidak terjadi apa-apa

2. Koneksi Internet:

  • Matikan internet → Proses booking gagal
  • Tampilkan pesan error

3. Rotasi Layar:

  • Putar layar saat mengisi form → Data tidak hilang

Kesimpulan

Kita telah berhasil membuat sistem pemesanan restoran lengkap dengan Flutter yang mencakup:

  1. UI yang Intuitif:
  • Form pemesanan dengan langkah-langkah jelas
  • Komponen interaktif yang mudah digunakan
  • Desain responsif untuk berbagai ukuran layar

2. Logika yang Solid:

  • Validasi input real-time
  • Penanganan error yang ramah
  • Simulasi proses booking yang realistis

3. Fitur Tambahan:

  • Notifikasi lokal untuk konfirmasi
  • Penyimpanan riwayat booking
  • Animasi transisi yang halus

4. Best Practices:

  • Kode terstruktur dengan pemisahan yang jelas
  • Penggunaan StatefulWidget untuk state management
  • Penanganan edge cases dengan baik

Learn More Via: https://myskill.id/course/state-and-props-management

--

--