Membuat Aplikasi jadwal pelajaran sederhana dengan flutter
📝 Aplikasi Jadwal Pelajaran Sederhana dengan Flutter
Pada praktikum ini dibuat aplikasi Jadwal Pelajaran Sederhana menggunakan Flutter dengan fitur:
Tambah data
Edit data
Hapus data
Validasi input (huruf & angka sesuai kolom)
Tampilan modern tema kuning-hitam
Aplikasi menggunakan Stateful Widget untuk mengelola data dan ListView untuk menampilkan daftar jadwal.
⚙️ Konsep Utama
Beberapa konsep yang digunakan:
State Management (
setState) → untuk update tampilan saat data berubahForm & Validasi → memastikan input sesuai format (huruf / angka)
ListView → menampilkan data jadwal
Dialog → untuk tambah, edit, dan konfirmasi hapus
Custom UI → menggunakan tema gelap + aksen kuning
💡 Validasi Input
Setiap field memiliki aturan:
Mata pelajaran & hari → hanya huruf
Jam → hanya angka
Ruang → huruf + angka
Hal ini dilakukan menggunakan TextInputFormatter dan validator.
🧩 Kode Lengkap
Berikut adalah implementasi lengkap dari aplikasi:
import 'package:flutter/material.dart';
import 'package:flutter/services.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(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFFFC107),
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF0E0E0E),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF111111),
foregroundColor: Colors.amber,
centerTitle: true,
elevation: 0,
),
),
home: const JadwalPage(),
);
}
}
class JadwalItem {
final String mapel;
final String hari;
final String jam;
final String ruang;
JadwalItem({
required this.mapel,
required this.hari,
required this.jam,
required this.ruang,
});
}
class JadwalPage extends StatefulWidget {
const JadwalPage({super.key});
@override
State<JadwalPage> createState() => _JadwalPageState();
}
class _JadwalPageState extends State<JadwalPage> {
final List<JadwalItem> _data = [
JadwalItem(
mapel: 'Matematika',
hari: 'Senin',
jam: '1',
ruang: 'X RPL 1',
),
JadwalItem(
mapel: 'Pemrograman',
hari: 'Selasa',
jam: '2',
ruang: 'Lab Komputer',
),
JadwalItem(
mapel: 'Bahasa Indonesia',
hari: 'Rabu',
jam: '3',
ruang: 'XI RPL 1',
),
];
int get _totalMapelUnik => _data.map((e) => e.mapel.toLowerCase()).toSet().length;
void _tampilForm({JadwalItem? item, int? index}) {
final formKey = GlobalKey<FormState>();
final mapelController = TextEditingController(text: item?.mapel ?? '');
final hariController = TextEditingController(text: item?.hari ?? '');
final jamController = TextEditingController(text: item?.jam ?? '');
final ruangController = TextEditingController(text: item?.ruang ?? '');
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: const Color(0xFF171717),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
titlePadding: const EdgeInsets.fromLTRB(20, 18, 20, 10),
contentPadding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
title: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.edit_note, color: Colors.black),
),
const SizedBox(width: 12),
Expanded(
child: Text(
item == null ? 'Tambah Jadwal' : 'Edit Jadwal',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
content: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildInput(
controller: mapelController,
label: 'Mata Pelajaran',
icon: Icons.menu_book,
hint: 'Huruf saja, contoh: Matematika',
keyboardType: TextInputType.name,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
],
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Mata pelajaran wajib diisi';
if (!RegExp(r'^[a-zA-Z\s]+$').hasMatch(text)) {
return 'Gunakan huruf saja';
}
return null;
},
),
const SizedBox(height: 12),
_buildInput(
controller: hariController,
label: 'Hari',
icon: Icons.calendar_month,
hint: 'Huruf saja, contoh: Senin',
keyboardType: TextInputType.name,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
],
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Hari wajib diisi';
if (!RegExp(r'^[a-zA-Z\s]+$').hasMatch(text)) {
return 'Gunakan huruf saja';
}
return null;
},
),
const SizedBox(height: 12),
_buildInput(
controller: jamController,
label: 'Jam Pelajaran',
icon: Icons.looks_one,
hint: 'Angka saja, contoh: 1',
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Jam pelajaran wajib diisi';
if (!RegExp(r'^[0-9]+$').hasMatch(text)) {
return 'Gunakan angka saja';
}
return null;
},
),
const SizedBox(height: 12),
_buildInput(
controller: ruangController,
label: 'Kelas / Ruang',
icon: Icons.location_on,
hint: 'Contoh: X RPL 1',
keyboardType: TextInputType.text,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
],
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return 'Ruang wajib diisi';
if (!RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(text)) {
return 'Gunakan huruf atau angka';
}
return null;
},
),
const SizedBox(height: 4),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
onPressed: () {
if (!(formKey.currentState?.validate() ?? false)) return;
setState(() {
final dataBaru = JadwalItem(
mapel: mapelController.text.trim(),
hari: hariController.text.trim(),
jam: jamController.text.trim(),
ruang: ruangController.text.trim(),
);
if (item == null) {
_data.add(dataBaru);
} else {
_data[index!] = dataBaru;
}
});
Navigator.pop(context);
},
icon: const Icon(Icons.save),
label: const Text('Simpan'),
),
],
);
},
);
}
void _hapusData(int index) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: const Color(0xFF171717),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: const [
Icon(Icons.warning_amber_rounded, color: Colors.amber),
SizedBox(width: 10),
Text('Hapus Jadwal'),
],
),
content: const Text('Yakin ingin menghapus data ini?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
),
onPressed: () {
setState(() {
_data.removeAt(index);
});
Navigator.pop(context);
},
icon: const Icon(Icons.delete),
label: const Text('Hapus'),
),
],
);
},
);
}
Widget _buildInput({
required TextEditingController controller,
required String label,
required IconData icon,
required String hint,
required TextInputType keyboardType,
required List<TextInputFormatter> inputFormatters,
required String? Function(String?) validator,
}) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
style: const TextStyle(color: Colors.white),
validator: validator,
decoration: InputDecoration(
labelText: label,
hintText: hint,
hintStyle: TextStyle(color: Colors.grey.shade600),
labelStyle: const TextStyle(color: Colors.amber),
prefixIcon: Icon(icon, color: Colors.amber),
filled: true,
fillColor: const Color(0xFF111111),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.amber.withOpacity(0.55)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.amber, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.redAccent),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.redAccent, width: 1.5),
),
),
);
}
Color _warnaCard(int index) {
final warna = [
const Color(0xFF181818),
const Color(0xFF1D1D1D),
const Color(0xFF222222),
];
return warna[index % warna.length];
}
Widget _buildStat(String label, String value, IconData icon) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.amber.withOpacity(0.35)),
),
child: Column(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.14),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.amber),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 12,
),
),
],
),
),
);
}
Widget _buildHeader() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: const LinearGradient(
colors: [Color(0xFFFFC107), Color(0xFFFFE082)],
),
boxShadow: [
BoxShadow(
color: Colors.amber.withOpacity(0.25),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: const Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: Colors.black,
child: Icon(Icons.school, color: Colors.amber, size: 30),
),
SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Pelajaran Sederhana',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
SizedBox(height: 4),
Text(
'Aplikasi praktikum Flutter kelas sekolah',
style: TextStyle(
fontSize: 13,
color: Colors.black87,
),
),
],
),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.amber.withOpacity(0.25)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.event_busy,
size: 72,
color: Colors.amber.withOpacity(0.9),
),
const SizedBox(height: 12),
const Text(
'Belum ada jadwal',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 6),
Text(
'Tekan tombol tambah untuk membuat jadwal baru',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade400),
),
],
),
),
);
}
Widget _buildListItem(JadwalItem item, int index) {
return Container(
decoration: BoxDecoration(
color: _warnaCard(index),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.amber.withOpacity(0.24)),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 54,
height: 54,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.menu_book,
color: Colors.black,
size: 30,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.mapel,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.calendar_month, size: 16, color: Colors.amber),
const SizedBox(width: 6),
Text(
item.hari,
style: TextStyle(color: Colors.grey.shade300),
),
const SizedBox(width: 12),
const Icon(Icons.looks_one, size: 16, color: Colors.amber),
const SizedBox(width: 6),
Text(
'Jam ${item.jam}',
style: TextStyle(color: Colors.grey.shade300),
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.location_on, size: 16, color: Colors.amber),
const SizedBox(width: 6),
Text(
item.ruang,
style: TextStyle(color: Colors.grey.shade300),
),
],
),
],
),
),
Column(
children: [
IconButton(
tooltip: 'Edit',
icon: const Icon(Icons.edit, color: Colors.amber),
onPressed: () => _tampilForm(item: item, index: index),
),
IconButton(
tooltip: 'Hapus',
icon: const Icon(Icons.delete, color: Colors.redAccent),
onPressed: () => _hapusData(index),
),
],
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.school, color: Colors.amber),
SizedBox(width: 8),
Text('Jadwal Pelajaran'),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _tampilForm(),
backgroundColor: Colors.amber,
foregroundColor: Colors.black,
icon: const Icon(Icons.add),
label: const Text('Tambah'),
),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF141414), Color(0xFF0B0B0B)],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildHeader(),
const SizedBox(height: 14),
Row(
children: [
_buildStat('Total Jadwal', '${_data.length}', Icons.list_alt),
const SizedBox(width: 10),
_buildStat('Mapel Unik', '$_totalMapelUnik', Icons.book_outlined),
],
),
const SizedBox(height: 16),
Expanded(
child: _data.isEmpty
? _buildEmptyState()
: ListView.separated(
itemCount: _data.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _buildListItem(_data[index], index);
},
),
),
],
),
),
),
),
);
}
}
Hasilnya:
🎯 Kesimpulan
Aplikasi ini sudah mencakup dasar penting dalam Flutter:
CRUD (Create, Read, Update, Delete)
Validasi form
UI modern
Penggunaan widget dasar Flutter
Cocok digunakan sebagai tugas praktikum sekolah karena:
tidak terlalu kompleks
mudah dipahami
bisa dikembangkan lebih lanjut
Comments
Post a Comment