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

Postingan populer dari blog ini

๐Ÿฒ Aplikasi Jastip Baso Soteng dengan Flutter

๐Ÿš€ Project Flutter Web: Lokasi Mie Gacoan dengan Google Maps & WhatsApp ๐Ÿ“๐Ÿ“ฑ

kode program Flutter untuk aplikasi To-Do List dengan fitur CRUD menggunakan database SQLite