FLUTTER + MOCKAPI๐ฅ
Membuat Aplikasi Absensi dengan Flutter & MockAPI
Halo semuanya! ๐
Perkenalkan, saya Afrizal Herlambang, dari kelas XII RPL 2. Kali ini saya ingin berbagi pengalaman saya dalam membuat aplikasi absensi sederhana menggunakan Flutter dengan dukungan MockAPI sebagai backend-nya.
Kenapa Flutter & MockAPI?
Flutter menjadi pilihan saya karena:
✨ Bisa membuat aplikasi multiplatform (Android & iOS) dengan satu codebase.
✨ Tampilan UI mudah dikustomisasi dan terlihat modern.
✨ Banyak library yang mendukung pengembangan.
Sementara MockAPI sangat membantu dalam tahap awal pengembangan karena:
⚡ Mudah membuat REST API tanpa harus setup server.
⚡ Bisa membuat schema data sendiri sesuai kebutuhan.
⚡ Sangat cocok untuk testing aplikasi sebelum nanti dihubungkan ke database asli.
Schema Absensi
Di aplikasi absensi ini, saya membuat schema sederhana seperti berikut:
id → nomor unik absensi
name → nama siswa
date → tanggal absensi
timeIn → jam masuk
timeOut → jam pulang
status → status kehadiran (Hadir, Izin, Sakit, Alpha, Terlambat)
note → catatan tambahan
Dengan schema ini, saya bisa mencatat siapa yang hadir, siapa yang izin, atau siapa yang terlambat, lengkap dengan keterangan waktunya.
Code Lengkap
Link : https://z12w06f112x0.zapp.page/#/
Code:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:intl/intl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Slow down animations for demonstration (remove in production)
timeDilation = 2.0;
return MaterialApp(
title: 'Absensi Management',
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'Poppins',
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.white,
),
),
home: const HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
final String apiUrl = "https://68a51cab2a3deed2960c6dbb.mockapi.io/absen";
List items = [];
bool isLoading = true;
bool isError = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
final TextEditingController nameController = TextEditingController();
final TextEditingController dateController = TextEditingController();
final TextEditingController timeInController = TextEditingController();
final TextEditingController timeOutController = TextEditingController();
final TextEditingController noteController = TextEditingController();
String selectedStatus = "Hadir";
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward();
fetchData();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> fetchData() async {
setState(() {
isLoading = true;
isError = false;
});
try {
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
setState(() {
items = json.decode(response.body);
isLoading = false;
});
} else {
setState(() {
isLoading = false;
isError = true;
});
}
} catch (e) {
setState(() {
isLoading = false;
isError = true;
});
}
}
Future<void> createData(String name, String date, String timeIn,
String timeOut, String status, String note) async {
try {
await http.post(
Uri.parse(apiUrl),
headers: {"Content-Type": "application/json"},
body: json.encode({
"name": name,
"date": date,
"timeIn": timeIn,
"timeOut": timeOut,
"status": status,
"note": note
}),
);
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil ditambahkan!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menambahkan data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> updateData(String id, String name, String date, String timeIn,
String timeOut, String status, String note) async {
try {
await http.put(
Uri.parse("$apiUrl/$id"),
headers: {"Content-Type": "application/json"},
body: json.encode({
"name": name,
"date": date,
"timeIn": timeIn,
"timeOut": timeOut,
"status": status,
"note": note
}),
);
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil diperbarui!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal memperbarui data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> deleteData(String id) async {
try {
await http.delete(Uri.parse("$apiUrl/$id"));
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil dihapus!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menghapus data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
void showForm({String? id, Map<String, dynamic>? item}) {
if (item != null) {
nameController.text = item["name"] ?? "";
dateController.text = item["date"] ?? "";
timeInController.text = item["timeIn"] ?? "";
timeOutController.text = item["timeOut"] ?? "";
noteController.text = item["note"] ?? "";
selectedStatus = item["status"] ?? "Hadir";
} else {
nameController.clear();
dateController.clear();
timeInController.clear();
timeOutController.clear();
noteController.clear();
selectedStatus = "Hadir";
}
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 10,
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
id == null ? "Tambah Data Absensi" : "Edit Data Absensi",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 20),
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: "Nama",
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 16),
TextField(
controller: dateController,
decoration: const InputDecoration(
labelText: "Tanggal (YYYY-MM-DD)",
prefixIcon: Icon(Icons.calendar_today),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: timeInController,
decoration: const InputDecoration(
labelText: "Jam Masuk",
prefixIcon: Icon(Icons.access_time),
),
),
),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: timeOutController,
decoration: const InputDecoration(
labelText: "Jam Keluar",
prefixIcon: Icon(Icons.access_time),
),
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedStatus,
decoration: const InputDecoration(
labelText: "Status",
prefixIcon: Icon(Icons.circle),
),
items: ["Hadir", "Izin", "Sakit", "Alpha", "Cuti"]
.map((status) => DropdownMenuItem(
value: status,
child: Text(status),
))
.toList(),
onChanged: (value) {
setState(() {
selectedStatus = value!;
});
},
),
const SizedBox(height: 16),
TextField(
controller: noteController,
maxLines: 3,
decoration: const InputDecoration(
labelText: "Catatan",
prefixIcon: Icon(Icons.note),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (id == null) {
createData(
nameController.text,
dateController.text,
timeInController.text,
timeOutController.text,
selectedStatus,
noteController.text
);
} else {
updateData(
id,
nameController.text,
dateController.text,
timeInController.text,
timeOutController.text,
selectedStatus,
noteController.text
);
}
Navigator.pop(context);
},
child: const Text("Simpan"),
),
],
),
],
),
),
),
);
}
void _confirmDelete(String id, String name) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Konfirmasi Hapus"),
content: Text("Apakah Anda yakin ingin menghapus data absensi $name?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () {
deleteData(id);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text("Hapus"),
),
],
),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case "hadir":
return Colors.green;
case "izin":
return Colors.orange;
case "sakit":
return Colors.yellow;
case "cuti":
return Colors.blue;
case "alpha":
return Colors.red;
default:
return Colors.grey;
}
}
IconData _getStatusIcon(String status) {
switch (status.toLowerCase()) {
case "hadir":
return Icons.check_circle;
case "izin":
return Icons.info;
case "sakit":
return Icons.medical_services;
case "cuti":
return Icons.beach_access;
case "alpha":
return Icons.cancel;
default:
return Icons.help;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Manajemen Absensi"),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 5,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: fetchData,
),
],
),
body: isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
SizedBox(height: 16),
Text('Memuat data absensi...'),
],
),
)
: isError
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Gagal memuat data absensi',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: fetchData,
child: const Text('Coba Lagi'),
),
],
),
)
: items.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
const Text(
'Tidak ada data absensi',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
const Text('Klik tombol + untuk menambahkan data absensi'),
],
),
)
: FadeTransition(
opacity: _fadeAnimation,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key(item["id"].toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Konfirmasi"),
content: Text("Hapus data absensi ${item["name"]}?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Batal"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text("Hapus"),
),
],
),
);
},
onDismissed: (direction) {
deleteData(item["id"].toString());
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item["name"] ?? "Tidak Ada Nama",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor(item["status"] ?? "Hadir"),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getStatusIcon(item["status"] ?? "Hadir"),
color: Colors.white,
size: 16,
),
const SizedBox(width: 4),
Text(
item["status"] ?? "Hadir",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
item["date"] ?? "Tidak Ada Tanggal",
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 12),
Row(
children: [
_buildTimeCard("MASUK", item["timeIn"] ?? "--:--", Icons.login),
const SizedBox(width: 12),
_buildTimeCard("KELUAR", item["timeOut"] ?? "--:--", Icons.logout),
],
),
if (item["note"] != null && item["note"].toString().isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Text(
"Catatan:",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
item["note"] ?? "",
style: TextStyle(
color: Colors.grey[700],
fontStyle: FontStyle.italic,
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => showForm(
id: item["id"].toString(),
item: item,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _confirmDelete(
item["id"].toString(), item["name"]),
),
],
),
],
),
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => showForm(),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
);
}
Widget _buildTimeCard(String label, String time, IconData icon) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey),
),
child: Column(
children: [
Icon(icon, size: 20, color: Colors.blue),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
time,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}
Fitur Aplikasi
Aplikasi absensi ini memiliki beberapa fitur utama:
✅ Input data absensi melalui form sederhana.
✅ Pilihan status menggunakan dropdown menu agar lebih terstruktur.
✅ Catatan opsional untuk keterangan khusus (misalnya sakit, izin, dll).
✅ Data langsung tersimpan ke MockAPI dan bisa dilihat kembali.
Pengalaman & Pelajaran
Dari pembuatan aplikasi ini, saya belajar banyak hal:
๐ Cara menghubungkan Flutter dengan REST API.
๐ Pentingnya membuat schema data yang rapi agar pengelolaan absensi mudah.
๐ Bagaimana membangun aplikasi yang nantinya bisa digunakan untuk kebutuhan nyata.
Penutup
Meskipun aplikasi ini masih sederhana, tapi bagi saya ini adalah langkah awal yang penting dalam memahami pengembangan aplikasi mobile dengan backend.
Ke depan, aplikasi ini bisa dikembangkan lebih jauh, misalnya dengan menambahkan login user, export laporan, atau bahkan terhubung dengan database sekolah secara langsung.
Terima kasih sudah membaca! ๐
Semoga pengalaman saya ini bisa menginspirasi teman-teman lain yang ingin belajar membuat aplikasi dengan Flutter dan MockAPI.
Komentar
Posting Komentar