flutterproyekkasirriverpodMahir4 mnt baca

Proyek Mini: Aplikasi Kasir Sederhana

Gabungkan semua yang dipelajari: aplikasi kasir dengan daftar produk, keranjang, dan struk transaksi.

Ringkasan proyek

Kita bangun aplikasi kasir warung: daftar produk dari database lokal, tambah ke keranjang, ubah jumlah, hitung total, dan simpan transaksi. Proyek ini menggabungkan sqflite, Riverpod, navigasi, dan form.

Struktur file

lib/ main.dart models/produk.dart data/database_helper.dart providers/keranjang_provider.dart screens/daftar_produk_screen.dart screens/keranjang_screen.dart screens/tambah_produk_screen.dart

Pisahkan model, data, provider, dan screen sejak awal. Kebiasaan ini menyelamatkanmu saat project tumbuh.

Model dan provider keranjang

dart
// models/produk.dart
class Produk {
  final String id;
  final String nama;
  final int harga;

  const Produk({required this.id, required this.nama, required this.harga});

  factory Produk.fromJson(Map<String, dynamic> json) => Produk(
        id: json['id'] as String? ?? '',
        nama: json['nama'] as String? ?? '',
        harga: (json['harga'] as num?)?.toInt() ?? 0,
      );

  Map<String, dynamic> toJson() => {'id': id, 'nama': nama, 'harga': harga};
}

// providers/keranjang_provider.dart
class ItemKeranjang {
  final Produk produk;
  final int jumlah;

  const ItemKeranjang({required this.produk, required this.jumlah});

  ItemKeranjang copyWith({int? jumlah}) =>
      ItemKeranjang(produk: produk, jumlah: jumlah ?? this.jumlah);
}

class Keranjang extends Notifier<List<ItemKeranjang>> {
  @override
  List<ItemKeranjang> build() => [];

  void tambah(Produk produk) {
    final index = state.indexWhere((e) => e.produk.id == produk.id);
    if (index >= 0) {
      state = [
        for (var i = 0; i < state.length; i++)
          if (i == index)
            state[i].copyWith(jumlah: state[i].jumlah + 1)
          else
            state[i],
      ];
    } else {
      state = [...state, ItemKeranjang(produk: produk, jumlah: 1)];
    }
  }

  void kurangi(String id) {
    state = state
        .map((e) => e.produk.id == id
            ? e.copyWith(jumlah: e.jumlah - 1)
            : e)
        .where((e) => e.jumlah > 0)
        .toList();
  }

  int get total =>
      state.fold(0, (sum, e) => sum + e.produk.harga * e.jumlah);

  void kosongkan() => state = [];
}

final keranjangProvider =
    NotifierProvider<Keranjang, List<ItemKeranjang>>(Keranjang.new);

Layar keranjang dengan struk

dart
class KeranjangScreen extends ConsumerWidget {
  const KeranjangScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isi = ref.watch(keranjangProvider);
    final total = ref.read(keranjangProvider.notifier).total;

    return Scaffold(
      appBar: AppBar(title: const Text('Keranjang')),
      body: isi.isEmpty
          ? const Center(child: Text('Keranjang kosong'))
          : ListView.builder(
              itemCount: isi.length,
              itemBuilder: (context, i) {
                final item = isi[i];
                return ListTile(
                  title: Text(item.produk.nama),
                  subtitle: Text('Rp${item.produk.harga} x ${item.jumlah}'),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove),
                        onPressed: () => ref
                            .read(keranjangProvider.notifier)
                            .kurangi(item.produk.id),
                      ),
                      Text('${item.jumlah}'),
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () => ref
                            .read(keranjangProvider.notifier)
                            .tambah(item.produk),
                      ),
                    ],
                  ),
                );
              },
            ),
      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Text('Total', style: TextStyle(fontSize: 18)),
                  Text(
                    'Rp$total',
                    style: const TextStyle(
                        fontSize: 20, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
              const SizedBox(height: 12),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: isi.isEmpty
                      ? null
                      : () {
                          ref.read(keranjangProvider.notifier).kosongkan();
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(
                                content: Text('Transaksi tersimpan')),
                          );
                        },
                  child: const Text('Bayar'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Tombol Bayar nonaktif saat keranjang kosong (onPressed: null), total selalu sinkron karena dihitung dari state.

Tantangan lanjutan

Setelah versi dasar jalan, coba tambahkan: simpan transaksi ke tabel transaksi di sqflite, halaman riwayat transaksi, diskon per item, dan cetak struk teks. Setiap fitur adalah latihan mengulang pola yang sama: model, provider, UI.

Catatan teknis: Di proyek nyata, jangan mencampur logic database langsung di Notifier tanpa lapisan repository. Buat class ProdukRepository yang membungkus DatabaseHelper, lalu inject ke provider. Memang terlihat over-engineering untuk proyek mini, tapi pola ini membuat kode testable dan siap tumbuh. Mulai biasakan dari proyek kecil agar otomatis saat proyek besar.