Daftar Kontak menggunakan Teknis ( CRUD )

Bikin Aplikasi Daftar Kontak Kekinian dengan Flutter: Belajar CRUD Gampang dan Seru!

Yo, gengs!
Lo pasti familiar banget sama aplikasi daftar kontak di HP kan? Nah, di blog kali ini gue mau ngenalin aplikasi daftar kontak yang gue buat pake Flutter, lengkap banget sama fitur CRUD: Create, Read, Update, dan Delete. Intinya, lo bisa nambah, liat, edit, dan hapus kontak dengan cara yang super gampang dan user friendly.

Kenapa Harus Ngerti CRUD?
CRUD itu singkatan dari empat operasi dasar buat ngatur data di aplikasi:

Create: Nambah data baru (misal: kontak baru)
Read: Nampilin data yang udah ada (liat daftar kontak)
Update: Merubah data yang ada (edit kontak)
Delete: Ngapus data yang udah gak dipake (hapus kontak)

Kalau lo ngerti CRUD, lo bisa bikin hampir semua aplikasi yang butuh database dan manajemen data. Nah, aplikasi daftar kontak ini bakal jadi latihan nyata buat lo paham gimana teknis CRUD diaplikasiin dalam dunia nyata — khususnya di aplikasi yang kita pake sehari-hari di HP.

Apa Aja Fitur Keren di Aplikasi Ini?

Tambah Kontak (Create)
Gak perlu ribet, tinggal pencet tombol “Tambah Kontak” yang ada di pojok bawah. Muncul form buat isi nama dan nomor telepon. Ada validasi biar gak bisa isi sembarangan, jadi data yang masuk bersih dan rapi.

Lihat Daftar Kontak (Read)
Lo bisa pilih dua mode tampil:

Nama Saja: Buat yang suka simple, cukup liat nama aja, tanpa nomor telepon.

Lengkap: Nama dan nomor telepon lengkap dengan tombol edit dan hapus, buat kontrol penuh atas kontak.

Edit Kontak (Update)
Ketemu typo di nama atau nomor? Tinggal klik ikon edit, form muncul dengan data lama, tinggal update, dan simpan. Gak perlu hapus dan tambah ulang, simpel kan?

Hapus Kontak (Delete)
Kalau ada kontak yang udah gak kepake, tinggal pencet ikon hapus. Tapi aplikasi ini gak asal hapus, ada konfirmasi biar lo gak salah klik. Setelah yakin, kontak langsung hilang dan aplikasi kasih notifikasi biar lo tahu.

UI Kekinian dan Interaktif
Warna pastel yang eye-friendly, avatar warna-warni yang bikin aplikasi hidup, plus snackbar notifikasi yang muncul setiap kali lo berhasil tambah, edit, atau hapus kontak.

Fitur Panggilan (Coming Soon!)
Ada fitur buat telepon langsung dari aplikasi, tapi masih tahap pengembangan. Jadi sabar dulu ya, bro!

Kenapa Lo Harus Coba?

Selain belajar dasar-dasar CRUD yang wajib dikuasai developer mana pun, aplikasi ini juga ngajarin lo gimana cara bikin user interface yang enak dipandang dan gampang digunakan. Pokoknya, ini latihan coding yang recommended banget buat pemula dan juga yang pengen upgrade skill Flutter-nya.

===== Ini code nya =====
import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; void main() => runApp(ContactApp()); class Contact { String name; String phone; Contact({required this.name, required this.phone}); } class ContactApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Kontak App', theme: ThemeData( primaryColor: Color(0xFFFCD8CD), scaffoldBackgroundColor: Color(0xFFFEEBF6), colorScheme: ColorScheme.light( primary: Color(0xFFFCD8CD), secondary: Color(0xFFFEEBF6), onPrimary: Colors.black87, ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: Color(0xFFFCD8CD), foregroundColor: Colors.black87, ), appBarTheme: AppBarTheme( backgroundColor: Color(0xFFFCD8CD), foregroundColor: Colors.black87, elevation: 2, titleTextStyle: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87, ), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: Color(0xFFF9D9D1), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Color(0xFFFCD8CD), width: 2), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade400, width: 1), ), labelStyle: TextStyle(color: Colors.black87), ), ), home: ContactListScreen(), ); } } class ContactListScreen extends StatefulWidget { @override _ContactListScreenState createState() => _ContactListScreenState(); } class _ContactListScreenState extends State<ContactListScreen> with SingleTickerProviderStateMixin { List<Contact> _contacts = []; late TabController _tabController; final List<Color> avatarColors = [ Color(0xFFE57373), Color(0xFFF06292), Color(0xFFBA68C8), Color(0xFF9575CD), Color(0xFF4DB6AC), Color(0xFF4FC3F7), Color(0xFFFFB74D), Color(0xFFFF8A65), Color(0xFFA1887F), Color(0xFF90A4AE), ]; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } Color _getAvatarColor(int index) { return avatarColors[index % avatarColors.length]; } Future<void> _makePhoneCall(String phoneNumber) async { final Uri telUri = Uri(scheme: 'tel', path: phoneNumber); if (await canLaunchUrl(telUri)) { await launchUrl(telUri); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Tidak dapat melakukan panggilan.'), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating, ), ); } } void _showFeatureNotAvailableDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Fitur Belum Tersedia'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Fitur panggilan belum tersedia.'), SizedBox(height: 16), Image.asset( 'lib/horror.jpg', height: 150, fit: BoxFit.cover, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Tutup'), ) ], ), ); } void _addOrEditContact({Contact? contact, int? index}) async { final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => ContactFormScreen(contact: contact), ), ); if (result != null && result is Contact) { setState(() { if (index == null) { _contacts.add(result); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Kontak berhasil ditambahkan!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400], )); } else { _contacts[index] = result; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Kontak berhasil diperbarui!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.orange[400], )); } }); } } void _deleteContact(int index) { showDialog( context: context, builder: (_) { return AlertDialog( title: Text('Hapus Kontak'), content: Text('Yakin ingin menghapus kontak ini?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Batal'), style: ButtonStyle( overlayColor: MaterialStateProperty.resolveWith<Color?>( (states) { if (states.contains(MaterialState.hovered) || states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) { return Colors.grey.withOpacity(0.2); } return null; }), ), ), TextButton( onPressed: () { setState(() { _contacts.removeAt(index); }); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Kontak dihapus."), behavior: SnackBarBehavior.floating, backgroundColor: Colors.red[400], )); }, child: Text('Hapus', style: TextStyle(color: Colors.red.shade800)), style: ButtonStyle( overlayColor: MaterialStateProperty.resolveWith<Color?>( (states) { if (states.contains(MaterialState.hovered) || states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) { return Colors.red.shade200.withOpacity(0.5); } return null; }), ), ), ], ); }, ); } Widget _buildNameOnlyList() { if (_contacts.isEmpty) { return Center( child: Text('Belum ada kontak.', style: TextStyle(fontSize: 18, color: Colors.black54))); } return ListView.builder( padding: EdgeInsets.symmetric(vertical: 8), itemCount: _contacts.length, itemBuilder: (_, index) { final contact = _contacts[index]; return Card( elevation: 3, margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), // diperlebar leading: CircleAvatar( backgroundColor: _getAvatarColor(index), child: Text( contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), title: Text( contact.name, style: TextStyle(fontWeight: FontWeight.w600), ), trailing: IconButton( icon: Icon(Icons.phone, color: Color(0xFFFCD8CD)), onPressed: _showFeatureNotAvailableDialog, tooltip: 'Panggil', ), onTap: () {}, // disable panggilan on tap ), ); }, ); } Widget _buildFullContactList() { if (_contacts.isEmpty) { return Center( child: Text('Belum ada kontak.', style: TextStyle(fontSize: 18, color: Colors.black54))); } return ListView.builder( padding: EdgeInsets.symmetric(vertical: 8), itemCount: _contacts.length, itemBuilder: (_, index) { final contact = _contacts[index]; return Card( elevation: 3, margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( leading: CircleAvatar( backgroundColor: _getAvatarColor(index), child: Text( contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), title: Text( contact.name, style: TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text(contact.phone, style: TextStyle(color: Colors.black54)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon(Icons.edit, color: Colors.orange[700]), onPressed: () => _addOrEditContact(contact: contact, index: index), tooltip: 'Edit kontak', ), IconButton( icon: Icon(Icons.delete, color: Colors.red[700]), onPressed: () => _deleteContact(index), tooltip: 'Hapus kontak', ), ], ), onTap: () => _makePhoneCall(contact.phone), // di sini telepon aktif ), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Daftar Kontak'), bottom: TabBar( controller: _tabController, tabs: [ Tab(text: 'Nama Saja'), Tab(text: 'Lengkap'), ], ), ), body: TabBarView( controller: _tabController, children: [ _buildNameOnlyList(), _buildFullContactList(), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () => _addOrEditContact(), icon: Icon(Icons.add), label: Text("Tambah Kontak"), tooltip: "Tambah kontak baru", ), ); } } class ContactFormScreen extends StatefulWidget { final Contact? contact; ContactFormScreen({this.contact}); @override _ContactFormScreenState createState() => _ContactFormScreenState(); } class _ContactFormScreenState extends State<ContactFormScreen> { final _formKey = GlobalKey<FormState>(); late String _name; late String _phone; @override void initState() { super.initState(); _name = widget.contact?.name ?? ''; _phone = widget.contact?.phone ?? ''; } void _saveForm() { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); final newContact = Contact(name: _name, phone: _phone); Navigator.pop(context, newContact); } } @override Widget build(BuildContext context) { final isEditing = widget.contact != null; return Scaffold( appBar: AppBar( title: Text(isEditing ? 'Edit Kontak' : 'Tambah Kontak'), ), body: Padding( padding: EdgeInsets.all(16), child: Form( key: _formKey, child: Column( children: [ TextFormField( initialValue: _name, decoration: InputDecoration( labelText: 'Nama', ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Nama tidak boleh kosong'; } return null; }, onSaved: (value) => _name = value!.trim(), ), SizedBox(height: 16), TextFormField( initialValue: _phone, decoration: InputDecoration( labelText: 'Nomor Telepon', ), keyboardType: TextInputType.phone, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Nomor telepon tidak boleh kosong'; } if (!RegExp(r'^[0-9]+$').hasMatch(value.trim())) { return 'Hanya angka yang diperbolehkan'; } return null; }, onSaved: (value) => _phone = value!.trim(), ), SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( primary: Color(0xFFFCD8CD), onPrimary: Colors.black87, minimumSize: Size(double.infinity, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), textStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onPressed: _saveForm, child: Text('Simpan'), ) ], ), ), ), ); } }
===== === ==== === =====

Langsung Coba Aplikasinya di Sini, Bro!

Jangan cuma baca teori, langsung praktik! Cek aplikasi daftar kontak gue yang sudah live di:
👉 https://ze1a068xe1b0.zapp.page/#/

Lo bisa langsung tambahin kontak, edit, liat daftar, sampai hapus kontak. Rasain gimana CRUD bekerja nyata dalam aplikasi yang sehari-hari kita pakai.

Kesimpulan

Belajar CRUD itu wajib buat lo yang pengen serius di dunia programming. Dengan aplikasi daftar kontak ini, lo gak cuma belajar teori tapi juga praktek langsung gimana data dibuat, dibaca, diupdate, dan dihapus dengan cara yang user friendly dan modern.

Kalau lo pengen gue bikin tutorial yang lebih mendalam atau versi yang lebih santuy, tinggal bilang aja. Gue siap bantu biar coding lo makin mantul!

Stay chill, keep coding, and let’s make cool apps together! ✌️🔥

Komentar

Postingan populer dari blog ini

Mobile Development dengan React Native 👨🏻‍💻.

KantinKu ~ By M Raffa Izzel H