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
// 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
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
ProdukRepositoryyang 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.