Membuat aplikasi jadwal pelajaran dengan flutter(CRUD + setstate) π₯️
Membuat Aplikasi Jadwal Pelajaran Sederhana dengan Flutter (CRUD + setState)
✍️ Pendahuluan
Flutter merupakan framework dari Google yang digunakan untuk membuat aplikasi mobile secara cross-platform. Pada artikel ini, kita akan membahas bagaimana membuat aplikasi Jadwal Pelajaran Sederhana dengan fitur CRUD (Create, Read, Update, Delete) menggunakan setState().
Aplikasi ini juga dilengkapi dengan desain tema merah hitam (dark mode) agar terlihat modern dan menarik.
π― Tujuan
Dengan membuat aplikasi ini, kita dapat:
- Memahami konsep CRUD
-
Menggunakan
setState()untuk update UI - Mengelola data sederhana dalam bentuk list
- Membuat tampilan UI yang menarik
π§© Struktur Aplikasi
Aplikasi terdiri dari beberapa bagian utama:
-
main()→ titik awal aplikasi -
JadwalApp→ konfigurasi aplikasi -
HomePage→ halaman utama -
Fungsi CRUD:
-
tambahData() -
editData() -
hapusData()
-
π» Source Code Lengkap
import 'package:flutter/material.dart';
void main() {
runApp(const JadwalApp());
}
class JadwalApp extends StatelessWidget {
const JadwalApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Jadwal Pelajaran',
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0C0C0C),
primarySwatch: Colors.red,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class Schedule {
final String id;
final String subject;
final String teacher;
final String room;
final String time;
final int dayIndex;
Schedule({
required this.id,
required this.subject,
required this.teacher,
required this.room,
required this.time,
required this.dayIndex,
});
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Schedule> _allSchedules = [];
List<Schedule> _filteredSchedules = [];
int? _selectedDayFilter;
String _searchQuery = '';
final Map<int, String> _dayNames = {
1: 'Senin',
2: 'Selasa',
3: 'Rabu',
4: 'Kamis',
5: 'Jumat',
6: 'Sabtu',
7: 'Minggu',
};
final List<Color> _subjectColors = [
Colors.redAccent,
Colors.pinkAccent,
Colors.deepOrangeAccent,
Colors.orangeAccent,
Colors.amberAccent,
Colors.limeAccent,
Colors.lightGreenAccent,
Colors.tealAccent,
];
@override
void initState() {
super.initState();
_loadSampleData();
}
void _loadSampleData() {
_allSchedules = [
Schedule(
id: '1',
subject: 'Matematika',
teacher: 'Bapak Ahmad',
room: 'Ruang 101',
time: '08:00',
dayIndex: 1,
),
Schedule(
id: '2',
subject: 'Bahasa Indonesia',
teacher: 'Ibu Siti',
room: 'Ruang 202',
time: '09:00',
dayIndex: 2,
),
Schedule(
id: '3',
subject: 'Fisika',
teacher: 'Bapak Budi',
room: 'Lab Fisika',
time: '10:30',
dayIndex: 1,
),
];
_sortSchedules();
_applyFilterAndSearch();
}
void _sortSchedules() {
_allSchedules.sort((a, b) {
if (a.dayIndex != b.dayIndex) return a.dayIndex.compareTo(b.dayIndex);
final aMinutes = _timeToMinutes(a.time);
final bMinutes = _timeToMinutes(b.time);
return aMinutes.compareTo(bMinutes);
});
}
int _timeToMinutes(String time) {
// Robust parsing for HH:mm format
final parts = time.split(':');
if (parts.length == 2) {
try {
return int.parse(parts[0]) * 60 + int.parse(parts[1]);
} catch (e) {
// Fallback for potentially malformed time strings (e.g., from old saves)
print('Warning: Failed to parse time "$time" to minutes. Error: $e');
return 0; // Default to 00:00 if parsing fails
}
}
return 0; // Default to 00:00 if format is incorrect
}
void _applyFilterAndSearch() {
setState(() {
_filteredSchedules = _allSchedules.where((s) {
final matchDay = _selectedDayFilter == null || s.dayIndex == _selectedDayFilter;
final matchSearch = _searchQuery.isEmpty ||
s.subject.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.room.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.teacher.toLowerCase().contains(_searchQuery.toLowerCase());
return matchDay && matchSearch;
}).toList();
});
}
void _addOrEditSchedule({Schedule? existing}) {
final isEditing = existing != null;
final formKey = GlobalKey<FormState>();
String subject = existing?.subject ?? '';
String teacher = existing?.teacher ?? '';
String room = existing?.room ?? '';
String time = existing?.time ?? '08:00'; // Initial time string
int dayIndex = existing?.dayIndex ?? 1;
final subjectCtrl = TextEditingController(text: subject);
final teacherCtrl = TextEditingController(text: teacher);
final roomCtrl = TextEditingController(text: room);
TimeOfDay selectedTime = _parseTimeOfDay(time); // Initial TimeOfDay object
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: const BorderSide(color: Colors.redAccent, width: 1.5),
),
title: Row(
children: [
Icon(isEditing ? Icons.edit : Icons.add, color: Colors.redAccent),
const SizedBox(width: 8),
Text(
isEditing ? 'Edit Jadwal' : 'Tambah Jadwal',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
content: Form(
key: formKey,
// FIX 2: Wrap Column with SingleChildScrollView to prevent overflow
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: subjectCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Mata Pelajaran', Icons.book),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => subject = val,
),
const SizedBox(height: 12),
TextFormField(
controller: teacherCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Nama Guru', Icons.person),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => teacher = val,
),
const SizedBox(height: 12),
TextFormField(
controller: roomCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Ruang / Lokasi', Icons.location_on),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => room = val,
),
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: dayIndex,
dropdownColor: const Color(0xFF2C2C2C),
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Hari', Icons.calendar_today),
items: _dayNames.entries.map((e) {
return DropdownMenuItem<int>(
value: e.key,
child: Text(e.value),
);
}).toList(),
onChanged: (val) {
if (val != null) {
setDialogState(() {
dayIndex = val;
});
}
},
),
const SizedBox(height: 12),
InkWell(
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: selectedTime,
builder: (context, child) {
return Theme(
data: ThemeData.dark().copyWith(
colorScheme: const ColorScheme.dark(primary: Colors.redAccent),
),
child: child!,
);
},
);
if (picked != null) {
setDialogState(() {
selectedTime = picked;
// FIX 1: Format time explicitly to HH:mm string to prevent FormatException
time = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
});
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFF252525),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.redAccent),
),
child: Row(
children: [
const Icon(Icons.access_time, color: Colors.redAccent),
const SizedBox(width: 12),
Text(time, style: const TextStyle(color: Colors.white, fontSize: 16)),
const Spacer(),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal', style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
setState(() {
if (isEditing) {
final index = _allSchedules.indexWhere((s) => s.id == existing.id);
if (index != -1) {
_allSchedules[index] = Schedule(
id: existing.id,
subject: subject,
teacher: teacher,
room: room,
time: time,
dayIndex: dayIndex,
);
}
} else {
_allSchedules.add(Schedule(
id: DateTime.now().millisecondsSinceEpoch.toString(),
subject: subject,
teacher: teacher,
room: room,
time: time,
dayIndex: dayIndex,
));
}
_sortSchedules();
_applyFilterAndSearch();
});
Navigator.pop(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Simpan'),
),
],
);
},
);
},
);
}
InputDecoration _inputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.grey),
prefixIcon: Icon(icon, color: Colors.redAccent, size: 20),
filled: true,
fillColor: const Color(0xFF252525),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.redAccent),
),
);
}
TimeOfDay _parseTimeOfDay(String time) {
// Robust parsing for HH:mm format
final parts = time.split(':');
if (parts.length == 2) {
try {
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
} catch (e) {
// Fallback for potentially malformed time strings (e.g., from old saves)
print('Warning: Failed to parse TimeOfDay "$time". Error: $e');
return TimeOfDay.now(); // Default to current time if parsing fails
}
}
return TimeOfDay.now(); // Default to current time if format is incorrect
}
void _deleteSchedule(Schedule schedule) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('Hapus Jadwal', style: TextStyle(color: Colors.redAccent)),
content: Text('Yakin ingin menghapus "${schedule.subject}"?', style: const TextStyle(color: Colors.white)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal', style: TextStyle(color: Colors.grey))),
ElevatedButton(
onPressed: () {
setState(() {
_allSchedules.removeWhere((s) => s.id == schedule.id);
_sortSchedules();
_applyFilterAndSearch();
});
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
child: const Text('Hapus'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Jadwal Pelajaran', style: TextStyle(fontWeight: FontWeight.bold)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A0A0A), Color(0xFF0C0C0C)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
actions: [
IconButton(
onPressed: () => _addOrEditSchedule(),
icon: const Icon(Icons.add_circle_outline, color: Colors.redAccent),
tooltip: 'Tambah',
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _addOrEditSchedule(),
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
icon: const Icon(Icons.add),
label: const Text('Tambah'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: TextField(
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Cari mata pelajaran, guru, atau ruang...',
hintStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.search, color: Colors.redAccent),
filled: true,
fillColor: const Color(0xFF1A1A1A),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
),
onChanged: (value) {
_searchQuery = value;
_applyFilterAndSearch();
},
),
),
SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
FilterChip(
label: const Text('Semua'),
selected: _selectedDayFilter == null,
onSelected: (_) {
setState(() => _selectedDayFilter = null);
_applyFilterAndSearch();
},
backgroundColor: const Color(0xFF252525),
selectedColor: Colors.redAccent,
labelStyle: const TextStyle(color: Colors.white),
),
const SizedBox(width: 8),
..._dayNames.entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(entry.value),
selected: _selectedDayFilter == entry.key,
onSelected: (_) {
setState(() => _selectedDayFilter = entry.key);
_applyFilterAndSearch();
},
backgroundColor: const Color(0xFF252525),
selectedColor: Colors.redAccent,
labelStyle: const TextStyle(color: Colors.white),
),
);
}),
],
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.schedule, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text(
'${_filteredSchedules.length} jadwal',
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: _filteredSchedules.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.calendar_today, size: 64, color: Colors.grey.shade700),
const SizedBox(height: 16),
Text(
_searchQuery.isNotEmpty ? 'Tidak ditemukan' : 'Belum ada jadwal',
style: const TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 8),
if (_searchQuery.isEmpty)
ElevatedButton.icon(
onPressed: () => _addOrEditSchedule(),
icon: const Icon(Icons.add),
label: const Text('Tambah Jadwal Pertama'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _filteredSchedules.length,
itemBuilder: (context, index) {
final s = _filteredSchedules[index];
final colorIndex = s.dayIndex % _subjectColors.length;
return Dismissible(
key: Key(s.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Colors.red.shade900,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
title: const Text('Hapus', style: TextStyle(color: Colors.redAccent)),
content: Text('Hapus "${s.subject}"?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Batal')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Hapus',
style: TextStyle(color: Colors.red))),
],
),
);
},
onDismissed: (_) => _deleteSchedule(s),
child: Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
color: const Color(0xFF1C1C1C),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_subjectColors[colorIndex], Colors.red.shade900],
),
shape: BoxShape.circle,
),
child: const Icon(Icons.book, color: Colors.white),
),
title: Text(
s.subject,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.calendar_today, size: 14, color: Colors.redAccent),
const SizedBox(width: 4),
Text(_dayNames[s.dayIndex]!, style: const TextStyle(fontSize: 12)),
const SizedBox(width: 12),
Icon(Icons.access_time, size: 14, color: Colors.redAccent),
const SizedBox(width: 4),
Text(s.time, style: const TextStyle(fontSize: 12)),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.person, size: 14, color: Colors.redAccent),
const SizedBox(width: 4),
Text(s.teacher, style: const TextStyle(fontSize: 12)),
const SizedBox(width: 12),
Icon(Icons.location_on, size: 14, color: Colors.redAccent),
const SizedBox(width: 4),
Text(s.room, style: const TextStyle(fontSize: 12)),
],
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => _addOrEditSchedule(existing: s),
icon: const Icon(Icons.edit, color: Colors.amber),
tooltip: 'Edit',
),
IconButton(
onPressed: () => _deleteSchedule(s),
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
tooltip: 'Hapus',
),
],
),
),
),
);
},
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
void main() {
runApp(const JadwalApp());
}
class JadwalApp extends StatelessWidget {
const JadwalApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Jadwal Pelajaran',
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0C0C0C),
primarySwatch: Colors.red,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class Schedule {
final String id;
final String subject;
final String teacher;
final String room;
final String time;
final int dayIndex;
Schedule({
required this.id,
required this.subject,
required this.teacher,
required this.room,
required this.time,
required this.dayIndex,
});
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Schedule> _allSchedules = [];
List<Schedule> _filteredSchedules = [];
int? _selectedDayFilter;
String _searchQuery = '';
final Map<int, String> _dayNames = {
1: 'Senin',
2: 'Selasa',
3: 'Rabu',
4: 'Kamis',
5: 'Jumat',
6: 'Sabtu',
7: 'Minggu',
};
final List<Color> _subjectColors = [
Colors.redAccent,
Colors.pinkAccent,
Colors.deepOrangeAccent,
Colors.orangeAccent,
Colors.amberAccent,
Colors.limeAccent,
Colors.lightGreenAccent,
Colors.tealAccent,
];
@override
void initState() {
super.initState();
_loadSampleData();
}
void _loadSampleData() {
_allSchedules = [
Schedule(
id: '1',
subject: 'Matematika',
teacher: 'Bapak Ahmad',
room: 'Ruang 101',
time: '08:00',
dayIndex: 1,
),
Schedule(
id: '2',
subject: 'Bahasa Indonesia',
teacher: 'Ibu Siti',
room: 'Ruang 202',
time: '09:00',
dayIndex: 2,
),
Schedule(
id: '3',
subject: 'Fisika',
teacher: 'Bapak Budi',
room: 'Lab Fisika',
time: '10:30',
dayIndex: 1,
),
];
_sortSchedules();
_applyFilterAndSearch();
}
void _sortSchedules() {
_allSchedules.sort((a, b) {
if (a.dayIndex != b.dayIndex) return a.dayIndex.compareTo(b.dayIndex);
final aMinutes = _timeToMinutes(a.time);
final bMinutes = _timeToMinutes(b.time);
return aMinutes.compareTo(bMinutes);
});
}
int _timeToMinutes(String time) {
// Robust parsing for HH:mm format
final parts = time.split(':');
if (parts.length == 2) {
try {
return int.parse(parts[0]) * 60 + int.parse(parts[1]);
} catch (e) {
// Fallback for potentially malformed time strings (e.g., from old saves)
print('Warning: Failed to parse time "$time" to minutes. Error: $e');
return 0; // Default to 00:00 if parsing fails
}
}
return 0; // Default to 00:00 if format is incorrect
}
void _applyFilterAndSearch() {
setState(() {
_filteredSchedules = _allSchedules.where((s) {
final matchDay = _selectedDayFilter == null || s.dayIndex == _selectedDayFilter;
final matchSearch = _searchQuery.isEmpty ||
s.subject.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.room.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.teacher.toLowerCase().contains(_searchQuery.toLowerCase());
return matchDay && matchSearch;
}).toList();
});
}
void _addOrEditSchedule({Schedule? existing}) {
final isEditing = existing != null;
final formKey = GlobalKey<FormState>();
String subject = existing?.subject ?? '';
String teacher = existing?.teacher ?? '';
String room = existing?.room ?? '';
String time = existing?.time ?? '08:00'; // Initial time string
int dayIndex = existing?.dayIndex ?? 1;
final subjectCtrl = TextEditingController(text: subject);
final teacherCtrl = TextEditingController(text: teacher);
final roomCtrl = TextEditingController(text: room);
TimeOfDay selectedTime = _parseTimeOfDay(time); // Initial TimeOfDay object
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: const BorderSide(color: Colors.redAccent, width: 1.5),
),
title: Row(
children: [
Icon(isEditing ? Icons.edit : Icons.add, color: Colors.redAccent),
const SizedBox(width: 8),
Text(
isEditing ? 'Edit Jadwal' : 'Tambah Jadwal',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
content: Form(
key: formKey,
// FIX 2: Wrap Column with SingleChildScrollView to prevent overflow
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: subjectCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Mata Pelajaran', Icons.book),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => subject = val,
),
const SizedBox(height: 12),
TextFormField(
controller: teacherCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Nama Guru', Icons.person),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => teacher = val,
),
const SizedBox(height: 12),
TextFormField(
controller: roomCtrl,
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Ruang / Lokasi', Icons.location_on),
validator: (v) => v == null || v.isEmpty ? 'Wajib diisi' : null,
onChanged: (val) => room = val,
),
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: dayIndex,
dropdownColor: const Color(0xFF2C2C2C),
style: const TextStyle(color: Colors.white),
decoration: _inputDecoration('Hari', Icons.calendar_today),
items: _dayNames.entries.map((e) {
return DropdownMenuItem<int>(
value: e.key,
child: Text(e.value),
);
}).toList(),
onChanged: (val) {
if (val != null) {
setDialogState(() {
dayIndex = val;
});
}
},
),
const SizedBox(height: 12),
InkWell(
onTap: () async {
final picked = await showTimePicker(
context: context,
initialTime: selectedTime,
builder: (context, child) {
return Theme(
data: ThemeData.dark().copyWith(
colorScheme: const ColorScheme.dark(primary: Colors.redAccent),
),
child: child!,
);
},
);
if (picked != null) {
setDialogState(() {
selectedTime = picked;
// FIX 1: Format time explicitly to HH:mm string to prevent FormatException
time = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
});
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFF252525),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.redAccent),
),
child: Row(
children: [
const Icon(Icons.access_time, color: Colors.redAccent),
const SizedBox(width: 12),
Text(time, style: const TextStyle(color: Colors.white, fontSize: 16)),
const Spacer(),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal', style: TextStyle(color: Colors.grey)),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
setState(() {
if (isEditing) {
final index = _allSchedules.indexWhere((s) => s.id == existing.id);
if (index != -1) {
_allSchedules[index] = Schedule(
id: existing.id,
subject: subject,
teacher: teacher,
room: room,
time: time,
dayIndex: dayIndex,
);
}
} else {
_allSchedules.add(Schedule(
id: DateTime.now().millisecondsSinceEpoch.toString(),
subject: subject,
teacher: teacher,
room: room,
time: time,
dayIndex: dayIndex,
));
}
_sortSchedules();
_applyFilterAndSearch();
});
Navigator.pop(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Simpan'),
),
],
);
},
);
},
);
}
InputDecoration _inputDecoration(String label, IconData icon) {
return InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.grey),
prefixIcon: Icon(icon, color: Colors.redAccent, size: 20),
filled: true,
fillColor: const Color(0xFF252525),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(14), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.redAccent),
),
);
}
TimeOfDay _parseTimeOfDay(String time) {
// Robust parsing for HH:mm format
final parts = time.split(':');
if (parts.length == 2) {
try {
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
} catch (e) {
// Fallback for potentially malformed time strings (e.g., from old saves)
print('Warning: Failed to parse TimeOfDay "$time". Error: $e');
return TimeOfDay.now(); // Default to current time if parsing fails
}
}
return TimeOfDay.now(); // Default to current time if format is incorrect
}
void _deleteSchedule(Schedule schedule) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('Hapus Jadwal', style: TextStyle(color: Colors.redAccent)),
content: Text('Yakin ingin menghapus "${schedule.subject}"?', style: const TextStyle(color: Colors.white)),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Batal', style: TextStyle(color: Colors.grey))),
ElevatedButton(
onPressed: () {
setState(() {
_allSchedules.removeWhere((s) => s.id == schedule.id);
_sortSchedules();
_applyFilterAndSearch();
});
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
child: const Text('Hapus'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Jadwal Pelajaran', style: TextStyle(fontWeight: FontWeight.bold)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1A0A0A), Color(0xFF0C0C0C)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
actions: [
IconButton(
onPressed: () => _addOrEditSchedule(),
icon: const Icon(Icons.add_circle_outline, color: Colors.redAccent),
tooltip: 'Tambah',
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _addOrEditSchedule(),
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
icon: const Icon(Icons.add),
label: const Text('Tambah'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: TextField(
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Cari mata pelajaran, guru, atau ruang...',
hintStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.search, color: Colors.redAccent),
filled: true,
fillColor: const Color(0xFF1A1A1A),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
),
onChanged: (value) {
_searchQuery = value;
_applyFilterAndSearch();
},
),
),
SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
FilterChip(
label: const Text('Semua'),
selected: _selectedDayFilter == null,
onSelected: (_) {
setState(() => _selectedDayFilter = null);
_applyFilterAndSearch();
},
backgroundColor: const Color(0xFF252525),
selectedColor: Colors.redAccent,
labelStyle: const TextStyle(color: Colors.white),
),
const SizedBox(width: 8),
..._dayNames.entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(entry.value),
selected: _selectedDayFilter == entry.key,
onSelected: (_) {
setState(() => _selectedDayFilter = entry.key);
_applyFilterAndSearch();
},
backgroundColor: const Color(0xFF252525),
selectedColor: Colors.redAccent,
labelStyle: const TextStyle(color: Colors.white),
),
);
}),
],
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.schedule, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text(
'${_filteredSchedules.length} jadwal',
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: _filteredSchedules.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.calendar_today, size: 64, color: Colors.grey.shade700),
const SizedBox(height: 16),
Text(
_searchQuery.isNotEmpty ? 'Tidak ditemukan' : 'Belum ada jadwal',
style: const TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 8),
if (_searchQuery.isEmpty)
ElevatedButton.icon(
onPressed: () => _addOrEditSchedule(),
icon: const Icon(Icons.add),
label: const Text('Tambah Jadwal Pertama'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _filteredSchedules.length,
itemBuilder: (context, index) {
final s = _filteredSchedules[index];
final colorIndex = s.dayIndex % _subjectColors.length;
return Dismissible(
key: Key(s.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Colors.red.shade900,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF1E1E1E),
title: const Text('Hapus', style: TextStyle(color: Colors.redAccent)),
content: Text('Hapus "${s.subject}"?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Batal')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Hapus',
style: TextStyle(color: Colors.red))), ], ), ); }, onDismissed: (_) => _deleteSchedule(s), child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), color: const Color(0xFF1C1C1C), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 48, height: 48, decoration: BoxDecoration( gradient: LinearGradient( colors: [_subjectColors[colorIndex], Colors.red.shade900], ), shape: BoxShape.circle, ), child: const Icon(Icons.book, color: Colors.white), ), title: Text( s.subject, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Row( children: [ Icon(Icons.calendar_today, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(_dayNames[s.dayIndex]!, style: const TextStyle(fontSize: 12)), const SizedBox(width: 12), Icon(Icons.access_time, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.time, style: const TextStyle(fontSize: 12)), ], ), const SizedBox(height: 4), Row( children: [ Icon(Icons.person, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.teacher, style: const TextStyle(fontSize: 12)), const SizedBox(width: 12), Icon(Icons.location_on, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.room, style: const TextStyle(fontSize: 12)), ], ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () => _addOrEditSchedule(existing: s), icon: const Icon(Icons.edit, color: Colors.amber), tooltip: 'Edit', ), IconButton( onPressed: () => _deleteSchedule(s), icon: const Icon(Icons.delete_outline, color: Colors.redAccent), tooltip: 'Hapus', ), ], ), ), ), ); }, ), ), ], ), ); } }
style: TextStyle(color: Colors.red))), ], ), ); }, onDismissed: (_) => _deleteSchedule(s), child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), color: const Color(0xFF1C1C1C), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 48, height: 48, decoration: BoxDecoration( gradient: LinearGradient( colors: [_subjectColors[colorIndex], Colors.red.shade900], ), shape: BoxShape.circle, ), child: const Icon(Icons.book, color: Colors.white), ), title: Text( s.subject, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Row( children: [ Icon(Icons.calendar_today, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(_dayNames[s.dayIndex]!, style: const TextStyle(fontSize: 12)), const SizedBox(width: 12), Icon(Icons.access_time, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.time, style: const TextStyle(fontSize: 12)), ], ), const SizedBox(height: 4), Row( children: [ Icon(Icons.person, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.teacher, style: const TextStyle(fontSize: 12)), const SizedBox(width: 12), Icon(Icons.location_on, size: 14, color: Colors.redAccent), const SizedBox(width: 4), Text(s.room, style: const TextStyle(fontSize: 12)), ], ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () => _addOrEditSchedule(existing: s), icon: const Icon(Icons.edit, color: Colors.amber), tooltip: 'Edit', ), IconButton( onPressed: () => _deleteSchedule(s), icon: const Icon(Icons.delete_outline, color: Colors.redAccent), tooltip: 'Hapus', ), ], ), ), ), ); }, ), ), ], ), ); } }
π‘ Kelebihan Aplikasi:
- Sederhana dan mudah dipahami
- Sudah menerapkan CRUD lengkap
- UI menarik (dark + red theme)
- Bisa dikembangkan lebih lanjut
π Pengembangan Selanjutnya
Aplikasi ini bisa ditingkatkan dengan:
- Menyimpan data ke database (SQLite / Hive)
- Menambahkan login
- Menggunakan state management seperti Provider
- Menambahkan fitur notifikasi jadwal
π Kesimpulan
Melalui aplikasi ini, kita belajar bahwa:- CRUD adalah konsep dasar penting dalam aplikasi
setState()sangat berperan dalam update UI
- Flutter memudahkan pembuatan aplikasi dengan cepat dan fleksibel
Name: RIZQI RESDHIANA
Class: XI RPL II
Comments
Post a Comment