CRUD & UI Keren: Tutorial Aplikasi Peminjaman Mobil Untuk Pemula💻

  

🚗✨ Tutorial Santuy: Bikin Aplikasi Peminjaman Mobil

Bikin Aplikasi Peminjaman Mobil Gampang Banget (Flutter) , Tutorial Santuy  🚀


Intro

Halo gengs!
Mau bikin app simpel buat ngatur pinjam-meminjam mobil? Nih aku share langkah & kode lengkap pake Flutter (single file main.dart) dan menggunakan konsep CRUD. Desainnya modern, pake Material 3, bisa:

  • Tambah / edit data mobil

  • Pilih tanggal & waktu pinjam

  • Upload gambar via URL (pakai Image.network)

  • Ceklis kalo udah selesai → pindah ke Riwayat
    Cocok buat proyek mini atau portofolio, gampang banget. Let’s goooo 🧑‍💻🎉


Fitur yang ada (singkat & padet)

  • 📋 Daftar Peminjaman (list)

  • ➕ Tambah peminjaman lengkap (nama mobil, tipe, tahun, gambar URL, peminjam, tanggal & jam)

  • ✏️ Edit item (data bisa diubah kapan aja)

  • ✅ Tombol selesai → pindah ke Riwayat

  • 🗑️ Hapus item

  • 🎨 UI modern (Card, rounded, Material3)

  • 🚫 Fallback gambar kalau URL error (biar gak jebret)



Struktur inti kode (penjelasan singkat)

  • class Car → model data (name/type/year/imageUrl/borrower/date/time).

  • List<Car> ongoingRentals → mobil yang masih dipinjam.

  • List<Car> historyRentals → riwayat mobil yg udah selesai.

  • _showAddRentalDialog() → dialog tambah peminjaman (pakai StatefulBuilder supaya date/time update realtime).

  • _showEditRentalDialog(Car car) → dialog edit, isi diisi otomatis dari car.

  • _finishRental(Car car) → remove dari ongoing → add ke history.

  • _buildRentalList(...) → UI list pakai Card + ListTile + Image.network (dengan errorBuilder fallback).


Code fullnya dibawah sini:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Peminjaman Mobil',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.deepPurple,
      ),
      home: const CarRentalApp(),
    );
  }
}

class Car {
  String name;
  String type;
  String year;
  String imageUrl;
  String borrower;
  String date; // YYYY-MM-DD
  String time; // HH:MM

  Car({
    required this.name,
    required this.type,
    required this.year,
    required this.imageUrl,
    required this.borrower,
    required this.date,
    required this.time,
  });
}

class CarRentalApp extends StatefulWidget {
  const CarRentalApp({super.key});

  @override
  State<CarRentalApp> createState() => _CarRentalAppState();
}

class _CarRentalAppState extends State<CarRentalApp> {
  final List<Car> ongoingRentals = [
    Car(
      name: "Avanza",
      type: "MPV",
      year: "2022",
      imageUrl:
          "https://img.hargamobil.com/2022/09/14/H41I0qrF/eksterior-toyota-avanza-2022-berubah-total-dari-se-de64.jpg",
      borrower: "Budi",
      date: "2025-08-14",
      time: "09:00",
    ),
    Car(
      name: "Fortuner",
      type: "SUV",
      year: "2023",
      imageUrl:
          "https://assets.pikiran-rakyat.com/crop/2x544:718x1048/720x0/webp/photo/2023/06/04/394821350.jpg",
      borrower: "Siti",
      date: "2025-08-14",
      time: "13:00",
    ),
  ];

  final List<Car> historyRentals = [];
  int _selectedIndex = 0;

  // ---------- Helpers ----------
  String _pad2(int v) => v.toString().padLeft(2, '0');

  void _addRental(Car car) {
    setState(() => ongoingRentals.add(car));
  }

  void _finishRental(Car car) {
    setState(() {
      ongoingRentals.remove(car);
      historyRentals.add(car);
    });
  }

  void _deleteRental(Car car, bool fromOngoing) {
    setState(() {
      if (fromOngoing) {
        ongoingRentals.remove(car);
      } else {
        historyRentals.remove(car);
      }
    });
  }

  // ---------- Edit Dialog ----------
  void _showEditRentalDialog(Car car) {
    final nameController = TextEditingController(text: car.name);
    final typeController = TextEditingController(text: car.type);
    final yearController = TextEditingController(text: car.year);
    final imageUrlController = TextEditingController(text: car.imageUrl);
    final borrowerController = TextEditingController(text: car.borrower);

    DateTime initialDate;
    try {
      initialDate = DateTime.parse(car.date);
    } catch (_) {
      initialDate = DateTime.now();
    }

    TimeOfDay initialTime;
    try {
      final parts = car.time.split(':');
      initialTime =
          TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
    } catch (_) {
      initialTime = TimeOfDay.now();
    }

    DateTime? selectedDate = initialDate;
    TimeOfDay? selectedTime = initialTime;

    showDialog(
      context: context,
      builder: (context) {
        return StatefulBuilder(builder: (context, setStateDialog) {
          return AlertDialog(
            title: const Text("Edit Peminjaman"),
            content: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextField(
                    controller: borrowerController,
                    decoration:
                        const InputDecoration(labelText: "Nama Peminjam"),
                  ),
                  TextField(
                    controller: nameController,
                    decoration:
                        const InputDecoration(labelText: "Nama Mobil"),
                  ),
                  TextField(
                    controller: typeController,
                    decoration: const InputDecoration(labelText: "Tipe"),
                  ),
                  TextField(
                    controller: yearController,
                    decoration: const InputDecoration(labelText: "Tahun"),
                    keyboardType: TextInputType.number,
                  ),
                  TextField(
                    controller: imageUrlController,
                    decoration: const InputDecoration(
                      labelText: "URL Gambar Mobil",
                      hintText: "https://....",
                    ),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          icon: const Icon(Icons.calendar_today),
                          label: Text(selectedDate == null
                              ? "Pilih Tanggal"
                              : "${selectedDate!.year}-${_pad2(selectedDate!.month)}-${_pad2(selectedDate!.day)}"),
                          onPressed: () async {
                            final picked = await showDatePicker(
                              context: context,
                              initialDate: selectedDate ?? DateTime.now(),
                              firstDate: DateTime(2020),
                              lastDate: DateTime(2035),
                            );
                            if (picked != null) {
                              setStateDialog(() => selectedDate = picked);
                            }
                          },
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          icon: const Icon(Icons.access_time),
                          label: Text(selectedTime == null
                              ? "Pilih Waktu"
                              : "${_pad2(selectedTime!.hour)}:${_pad2(selectedTime!.minute)}"),
                          onPressed: () async {
                            final picked = await showTimePicker(
                              context: context,
                              initialTime: selectedTime ?? TimeOfDay.now(),
                            );
                            if (picked != null) {
                              setStateDialog(() => selectedTime = picked);
                            }
                          },
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            actions: [
              TextButton(
                child: const Text("Batal"),
                onPressed: () => Navigator.pop(context),
              ),
              ElevatedButton(
                child: const Text("Simpan"),
                onPressed: () {
                  if (nameController.text.isEmpty ||
                      typeController.text.isEmpty ||
                      yearController.text.isEmpty ||
                      imageUrlController.text.isEmpty ||
                      borrowerController.text.isEmpty ||
                      selectedDate == null ||
                      selectedTime == null) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                          content:
                              Text("Lengkapi semua field terlebih dahulu")),
                    );
                    return;
                  }

                  setState(() {
                    car.name = nameController.text;
                    car.type = typeController.text;
                    car.year = yearController.text;
                    car.imageUrl = imageUrlController.text;
                    car.borrower = borrowerController.text;
                    car.date =
                        "${selectedDate!.year}-${_pad2(selectedDate!.month)}-${_pad2(selectedDate!.day)}";
                    car.time =
                        "${_pad2(selectedTime!.hour)}:${_pad2(selectedTime!.minute)}";
                  });

                  Navigator.pop(context);
                },
              ),
            ],
          );
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final tabs = [
      _buildRentalList(ongoingRentals, true),
      _buildRentalList(historyRentals, false),
    ];

    return Scaffold(
      appBar: AppBar(
        leading: const Padding(
          padding: EdgeInsets.only(left: 12.0),
          child: Icon(Icons.directions_car_filled, size: 28),
        ),
        title: Text(
          _selectedIndex == 0
              ? "Daftar Peminjaman Mobil"
              : "Riwayat Peminjaman",
        ),
        centerTitle: false,
        elevation: 3,
      ),
      body: tabs[_selectedIndex],
      floatingActionButton: _selectedIndex == 0
          ? FloatingActionButton(
              onPressed: () => _showAddRentalDialog(context),
              child: const Icon(Icons.add),
            )
          : null,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() => _selectedIndex = index);
        },
        destinations: const [
          NavigationDestination(
              icon: Icon(Icons.directions_car), label: "Peminjaman"),
          NavigationDestination(icon: Icon(Icons.history), label: "Riwayat"),
        ],
      ),
    );
  }

  Widget _buildRentalList(List<Car> list, bool isOngoing) {
    if (list.isEmpty) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(28.0),
          child: Text(
            "Tidak ada data",
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ),
      );
    }
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: list.length,
      itemBuilder: (context, index) {
        final car = list[index];
        return Card(
          elevation: 3,
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
          margin: const EdgeInsets.symmetric(vertical: 8),
          child: ListTile(
            contentPadding:
                const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            leading: ClipRRect(
              borderRadius: BorderRadius.circular(10),
              child: Image.network(
                car.imageUrl,
                width: 72,
                height: 72,
                fit: BoxFit.cover,
                errorBuilder: (context, error, stackTrace) {
                  return Container(
                    width: 72,
                    height: 72,
                    color: Theme.of(context).colorScheme.surfaceVariant,
                    child: const Icon(Icons.directions_car, size: 40),
                  );
                },
              ),
            ),
            title: Text("${car.name} (${car.year})",
                style: const TextStyle(fontWeight: FontWeight.w600)),
            subtitle: Text(
                "${car.type} • Peminjam: ${car.borrower}\n${car.date} • ${car.time}"),
            isThreeLine: true,
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (isOngoing)
                  IconButton(
                    tooltip: 'Edit',
                    icon: const Icon(Icons.edit, color: Colors.blue),
                    onPressed: () => _showEditRentalDialog(car),
                  ),
                if (isOngoing)
                  IconButton(
                    tooltip: 'Selesai (pindah ke riwayat)',
                    icon: const Icon(Icons.check_circle, color: Colors.green),
                    onPressed: () => _finishRental(car),
                  ),
                IconButton(
                  tooltip: 'Hapus',
                  icon: const Icon(Icons.delete, color: Colors.red),
                  onPressed: () => _deleteRental(car, isOngoing),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  // ---------- Add Dialog ----------
  void _showAddRentalDialog(BuildContext context) {
    final nameController = TextEditingController();
    final typeController = TextEditingController();
    final yearController = TextEditingController();
    final imageUrlController = TextEditingController();
    final borrowerController = TextEditingController();
    DateTime? selectedDate;
    TimeOfDay? selectedTime;

    showDialog(
      context: context,
      builder: (context) {
        return StatefulBuilder(builder: (context, setStateDialog) {
          return AlertDialog(
            title: const Text("Tambah Peminjaman"),
            content: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  TextField(
                    controller: borrowerController,
                    decoration:
                        const InputDecoration(labelText: "Nama Peminjam"),
                  ),
                  TextField(
                    controller: nameController,
                    decoration:
                        const InputDecoration(labelText: "Nama Mobil"),
                  ),
                  TextField(
                    controller: typeController,
                    decoration: const InputDecoration(labelText: "Tipe"),
                  ),
                  TextField(
                    controller: yearController,
                    decoration: const InputDecoration(labelText: "Tahun"),
                    keyboardType: TextInputType.number,
                  ),
                  TextField(
                    controller: imageUrlController,
                    decoration: const InputDecoration(
                      labelText: "URL Gambar Mobil",
                      hintText: "https://....",
                    ),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: {
                      Expanded(
                        child: ElevatedButton.icon(
                          icon: const Icon(Icons.calendar_today),
                          label: Text(selectedDate == null
                              ? "Pilih Tanggal"
                              : "${selectedDate!.year}-${_pad2(selectedDate!.month)}-${_pad2(selectedDate!.day)}"),
                          onPressed: () async {
                            final picked = await showDatePicker(
                              context: context,
                              initialDate: selectedDate ?? DateTime.now(),
                              firstDate: DateTime(2020),
                              lastDate: DateTime(2035),
                            );
                            if (picked != null) {
                              setStateDialog(() => selectedDate = picked);
                            }
                          },
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: ElevatedButton.icon(
                          icon: const Icon(Icons.access_time),
                          label: Text(selectedTime == null
                              ? "Pilih Waktu"
                              : "${_pad2(selectedTime!.hour)}:${_pad2(selectedTime!.minute)}"),
                          onPressed: () async {
                            final picked = await showTimePicker(
                              context: context,
                              initialTime: selectedTime ?? TimeOfDay.now(),
                            );
                            if (picked != null) {
                              setStateDialog(() => selectedTime = picked);
                            }
                          },
                        ),
                      ),
                    }.toList(),
                  ),
                ],
              ),
            ),
            actions: [
              TextButton(
                child: const Text("Batal"),
                onPressed: () => Navigator.pop(context),
              ),
              ElevatedButton(
                child: const Text("Simpan"),
                onPressed: () {
                  if (nameController.text.isNotEmpty &&
                      typeController.text.isNotEmpty &&
                      yearController.text.isNotEmpty &&
                      imageUrlController.text.isNotEmpty &&
                      borrowerController.text.isNotEmpty &&
                      selectedDate != null &&
                      selectedTime != null) {
                    _addRental(Car(
                      name: nameController.text,
                      type: typeController.text,
                      year: yearController.text,
                      imageUrl: imageUrlController.text,
                      borrower: borrowerController.text,
                      date:
                          "${selectedDate!.year}-${_pad2(selectedDate!.month)}-${_pad2(selectedDate!.day)}",
                      time:
                          "${_pad2(selectedTime!.hour)}:${_pad2(selectedTime!.minute)}",
                    ));
                    Navigator.pop(context);
                  } else {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                          content:
                              Text("Lengkapi semua field terlebih dahulu")),
                    );
                  }
                },
              ),
            ],
          );
        });
      },
    );
  }
}

Tips penting biar gak ngedrop saat jalanin app

  1. URL gambar: gunain URL gambar langsung yang boleh di-hotlink. Kalau pake situs yang blok hotlink, gambar bakal error → pake fallback. Contoh aman: gambar dari Wikimedia/Unsplash.

  2. Null safety: form validasi wajib; dialog pake StatefulBuilder supaya picker update UI.

  3. Format tanggal/waktu: simpen sebagai string YYYY-MM-DD & HH:MM biar gampang tampil dan parse.

  4. Duplicate class: pastikan cuma 1x class Car, 1x CarRentalApp di file — kalo ada duplikat bakal error compiler.

  5. Run: copy-paste file main.dart ke zapp.run atau Flutter project, klik run. Gass!


Potongan kode penting (liat nih)

Dialog tambah pake date + time picker, pake StatefulBuilder biar tombol berubah pas pilih:

ElevatedButton.icon(
  icon: Icon(Icons.calendar_today),
  label: Text(selectedDate==null ? "Pilih Tanggal" : "2025-08-14"),
  onPressed: () async {
    final picked = await showDatePicker(...);
    if (picked != null) setStateDialog(()=> selectedDate = picked);
  },
),

Edit: isi form otomatis, bisa ubah tanggal & jam, tekan Simpan → data updated in-place.


Troubleshooting singkat (kalo error)

  • ❌ No MaterialLocalizations → pastiin MaterialApp ada di root main() (kode yang aku berikan sudah bener).

  • ❌ Image network error / 404 → cek URL di browser dulu. Kalo 404 artinya salah link.

  • ❌ Duplicate identifier / nama state → cari class dengan nama sama di file, hapus duplikat.

  • ❌ Picker nggak update text → pake StatefulBuilder di dialog (sudah dipakai).


Cara kustom & next steps (kalau mau nambah)

  • Simpen data agar tetap ada setelah refresh: pakai shared_preferences (mobile) atau window.localStorage (web).

  • Tambahin notifikasi / reminder sebelum jadwal (pakai flutter_local_notifications).

  • Export riwayat ke CSV / Excel buat laporan.

  • Tambah filter & search (by name / borrower) buat UX makin cakep.

  • Tambah gambar upload via kamera / gallery (butuh permission & package).


Penutup✊

Yoi gengs, itu dia tutorial santuy buat bikin app pinjam mobil. Loe udah punya kode lengkap — tinggal copy-paste main.dart yang udah aku kirim, lalu tweak warna / gambar sesuai selera.
Kalau lo mau aku bikin versi:

  • nyimpen data di localStorage biar gak ilang setelah refresh, atau

  • nambah fitur notifikasi / export CSV,
    coba bilang/comment aja: “bikinin storage” atau “bikinin notifs”, gue langsung kasih. 😉

Jangan lupa: kalau kepake, share link blog/IG/Twitter lo. Tag aku biar aku bisa ngintip hasilnya. #MadeWithFlutter #GenZCode #Code#


Jika ingin lihat hasilnya ada di sini:https://z66e06nq66f0.zapp.page/#/

MADE BY: YOGI ARDIANSYAH AND RIZQI RESDHIANA XI RPL II

Comments

Popular posts from this blog

Latihan membuat tampilan flutter sederhana 🤖

Jenis-jenis sistem operasi os

Belajar Flutter