
Menangkap Data, Dropdown, dan Validasi
ITujuan Praktikum
Setelah menyelesaikan praktikum ini, mahasiswa diharapkan mampu:
- ✓Menangkap teks dari pengguna melalui TextEditingController.
- ✓Menerapkan widget dropdown statis (DropdownButtonFormField) untuk pemilihan kategori.
- ✓Menerapkan validasi input untuk mencegah data kosong atau format yang salah.
- ✓Menambahkan input tanggal transaksi menggunakan showDatePicker beserta validasinya (bagian tugas).
IIDasar Teori
Form & GlobalKey
Widget Form berfungsi sebagai wadah utama yang mengelompokkan beberapa input agar dapat divalidasi secara bersamaan. GlobalKey<FormState> digunakan sebagai kunci untuk mengakses dan memicu proses validasi seluruh field yang berada di dalam Form melalui pemanggilan _formKey.currentState!.validate().
TextFormField & TextEditingController
TextFormField merupakan kolom input teks yang sudah terintegrasi dengan mekanisme validasi bawaan Form. TextEditingController digunakan sebagai pengendali (controller) untuk membaca dan mengelola teks yang diketikkan pengguna pada masing-masing field, seperti judul transaksi dan nominal.
Validasi Input
Validasi digunakan untuk mencegah data yang tidak sesuai format masuk ke dalam sistem sebelum disimpan. Mekanismenya menggunakan properti validator pada TextFormField, berupa fungsi yang memeriksa isi kolom:
Jika data valid, validator mengembalikan null (tidak ada error).
Jika data tidak valid, validator mengembalikan sebuah String berisi pesan error yang akan ditampilkan berwarna merah di bawah field.
DropdownButtonFormField
Widget ini digunakan untuk menyediakan pilihan kategori yang bersifat statis kepada pengguna, sehingga input kategori tidak berupa teks bebas melainkan dipilih dari daftar yang telah ditentukan (Makanan, Transportasi, Hiburan, Lainnya).
showDatePicker
showDatePicker adalah fungsi bawaan Flutter yang menampilkan dialog kalender interaktif kepada pengguna untuk memilih tanggal. Fungsi ini bersifat asynchronous dan mengembalikan objek DateTime yang dipilih pengguna, atau null jika dialog dibatalkan.
IIILangkah Kerja Praktikum
Persiapan State & Controller
Halaman dibuat sebagai StatefulWidget karena isi formulir (teks dan dropdown) akan terus berubah saat pengguna berinteraksi. Disiapkan sebuah GlobalKey untuk memicu validasi, serta TextEditingController untuk judul dan nominal.
class _AddTransactionScreenState extends State<AddTransactionScreen> {
// Kunci utama untuk mengontrol Form
final _formKey = GlobalKey<FormState>();
// Controller pembaca ketikan
final _titleController = TextEditingController();
final _amountController = TextEditingController();
// Default opsi dropdown
String _selectedCategory = 'Makanan';
final List<String> _categories = [
'Makanan',
'Transportasi',
'Hiburan',
'Lainnya',
];
@override
void dispose() {
// Bersihkan controller saat halaman ditutup agar tidak memory leak
_titleController.dispose();
_amountController.dispose();
super.dispose();
}
}Membuat Kolom Input (TextFormField)
Seluruh kolom dibungkus dengan widget Form dan dihubungkan dengan _formKey. Setiap TextFormField dihubungkan dengan controller masing-masing, dan khusus kolom nominal digunakan keyboardType: TextInputType.number.
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Input 1: Judul Transaksi
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: "Judul Transaksi",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "Judul transaksi tidak boleh kosong";
}
return null;
},
),
const SizedBox(height: 16),
// Input 2: Nominal Saldo (Keyboard Angka)
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "Nominal (Rp)",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "Nominal wajib diisi";
}
if (int.tryParse(value) == null) {
return "Harus berupa angka bulat yang valid";
}
return null;
},
),
],
),
)Dropdown Kategori & Tombol Simpan
DropdownButtonFormField digunakan untuk pilihan kategori transaksi, dan ElevatedButton digunakan untuk memicu validasi seluruh form melalui _formKey.currentState!.validate().
// Input 3: Kategori Dropdown
DropdownButtonFormField<String>(
initialValue: _selectedCategory,
decoration: const InputDecoration(
labelText: "Kategori",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: _categories.map((String category) {
return DropdownMenuItem<String>(
value: category,
child: Text(category),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCategory = newValue!;
});
},
),
const SizedBox(height: 32),
// Tombol Simpan
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Tersimpan: ${_titleController.text} '
'(Rp ${_amountController.text})',
),
backgroundColor: Colors.green,
),
);
}
},
child: const Text(
"Simpan Transaksi",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
)Konfigurasi main.dart
File main.dart menjadi entry point aplikasi dan langsung mengarahkan halaman utama ke AddTransactionScreen.
import 'package:flutter/material.dart';
import 'add_transaction_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Expense Tracker - Modul 3',
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: const AddTransactionScreen(),
);
}
}Menjalankan Aplikasi
Aplikasi dijalankan pada emulator/device untuk menguji tampilan formulir dan mekanisme validasi.
IVLatihan dan Tugas
Menambahkan Controller Tanggal
Sebuah TextEditingController baru (_dateController) ditambahkan pada state untuk menampung dan menampilkan tanggal yang dipilih pengguna, serta di-dispose bersama controller lainnya.
final _dateController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
_amountController.dispose();
_dateController.dispose(); // dispose controller tanggal
super.dispose();
}Widget Input Tanggal & Date Picker
Field tanggal dibuat readOnly agar pengguna tidak mengetik manual, melainkan wajib memilih tanggal melalui dialog kalender. Saat field ditekan (onTap), fungsi showDatePicker dipanggil secara asynchronous; hasil tanggal yang dipilih kemudian diformat menjadi DD/MM/YYYY dan ditampilkan pada _dateController.
// Input 4: Tanggal Transaksi
TextFormField(
controller: _dateController,
readOnly: true,
decoration: const InputDecoration(
labelText: "Tanggal Transaksi",
hintText: "DD/MM/YYYY",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
_dateController.text =
"${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
});
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return "Tanggal transaksi wajib diisi";
}
return null;
},
),Integrasi Tanggal ke Pesan SnackBar
Setelah validasi seluruh form berhasil (termasuk tanggal), pesan konfirmasi pada SnackBar diperbarui agar turut menampilkan tanggal transaksi yang dipilih.
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Tersimpan: ${_titleController.text} - '
'${_dateController.text} (Rp ${_amountController.text})',
),
backgroundColor: Colors.green,
),
);
}
},Hasil Pengujian Tugas
VKesimpulan
Berdasarkan praktikum yang telah dilakukan, dapat disimpulkan beberapa hal berikut:
Widget Form beserta GlobalKey<FormState> memudahkan pengelolaan dan validasi banyak input sekaligus hanya dengan satu kali pemanggilan validate().
TextEditingController berperan penting dalam membaca nilai input pengguna secara real-time, baik untuk kolom teks biasa maupun kolom yang bersifat readOnly seperti tanggal.
Properti validator pada TextFormField efektif mencegah data kosong maupun data dengan format yang tidak sesuai (misalnya nominal yang bukan angka) agar tidak tersimpan ke sistem.
DropdownButtonFormField cocok digunakan untuk input dengan pilihan yang bersifat tetap/statis seperti kategori transaksi.
Fungsi showDatePicker mempermudah pengguna memilih tanggal secara akurat melalui antarmuka kalender, dibandingkan mengetik tanggal secara manual, sekaligus mengurangi risiko kesalahan format input.
Kombinasi StatefulWidget, controller, dan validator menghasilkan formulir input transaksi yang lengkap, interaktif, dan tervalidasi dengan baik.