1. Introduction
1.1 Latar Belakang
Input widget merupakan komponen antarmuka pada Flutter yang berfungsi menangkap data masukan dari pengguna, seperti teks, angka, maupun pilihan dari sejumlah opsi yang tersedia. Komponen tersebut umumnya disusun ke dalam sebuah form, yaitu wadah yang menyatukan seluruh input widget agar dapat divalidasi secara bersamaan melalui sebuah GlobalKey. Tiga pilar utama yang membentuk sebuah form pada Flutter meliputi Form beserta GlobalKey sebagai wadah utama untuk memvalidasi banyak input sekaligus, TextFormField sebagai kolom teks yang mendukung validasi terintegrasi, dan TextEditingController sebagai pengendali untuk membaca teks yang diketik pengguna. Implementasi form tersebut umumnya diletakkan pada sebuah StatefulWidget, karena nilai pada kolom masukan maupun pilihan dropdown dapat berubah sewaktu-waktu seiring interaksi pengguna, sehingga memerlukan method setState untuk memperbarui tampilan sesuai perubahan tersebut. Selain menangkap data, setiap kolom masukan juga memerlukan validasi berupa fungsi yang memeriksa isi kolom, mengembalikan nilai null apabila data valid, atau mengembalikan sebuah String berisi pesan kesalahan apabila data tidak valid, sehingga data yang tidak sesuai format dapat dicegah sebelum disimpan ke dalam sistem. Pemahaman terhadap input widget, form, keterkaitannya dengan StatefulWidget, serta mekanisme validasi tersebut menjadi penting agar aplikasi yang dibangun mampu menangani masukan pengguna secara aman dan sesuai format yang diharapkan.
Berdasarkan hal tersebut, dilakukan praktik implementasi formulir pencatatan transaksi yang mencakup penangkapan teks masukan melalui controller, penerapan widget dropdown statis untuk pemilihan kategori, serta penerapan validasi input guna mencegah data kosong atau format yang tidak sesuai. Kegiatan ini disusun sebagai pemenuhan tugas mata kuliah Aplikasi Mobile pada Departemen Informatika, Universitas Andalas, tahun ajaran 2026/2027.
1.2 Rumusan Masalah
Berdasarkan latar belakang, modul praktikum yang telah diberikan, maka rumusan masalah sebagai berikut:
- Bagaimana cara menangkap teks masukan dari pengguna melalui widget controller?
- Bagaimana penerapan widget dropdown statis pada formulir?
- Bagaimana penerapan validasi input untuk mencegah data kosong atau format yang tidak sesuai?
1.3 Tujuan
Berdasarkan rumusan masalah yang telah dijabarkan, maka tujuan pelaksanaan praktikum ini adalah sebagai berikut:
- Memahami dan menerapkan cara menangkap teks masukan dari pengguna melalui widget controller.
- Memahami dan menerapkan widget dropdown statis pada formulir.
- Memahami dan menerapkan validasi input untuk mencegah data kosong atau format yang tidak sesuai.
2. Methodology
2.1 Persiapan State & Controller
- Dibuat sebuah project flutter baru dengan nama "pertemuan3" dengan menjalani perintah "flutter create pertemuan3" pada cmd.
- Didefinisikan class _AddTransactionScreenState sebagai turunan dari State<AddTransactionScreen>, yaitu kelas state dari widget AddTransactionScreen.
- Dideklarasikan variabel _formKey bertipe GlobalKey<FormState> sebagai kunci utama untuk mengontrol widget Form yang akan dibangun pada method build.
- Dideklarasikan variabel _titleController dan _amountController, masing-masing bertipe TextEditingController, sebagai pengontrol pembaca ketikan pada kolom judul transaksi dan kolom nominal transaksi.
- Dideklarasikan variabel _selectedCategory bertipe String dengan nilai awal "Makanan", sebagai opsi kategori transaksi yang terpilih secara default pada dropdown.
- Pada method build, struktur antarmuka belum didefinisikan dan akan dibangun pada langkah selanjutnya.

2.2 Membuat Kolom Input
- Dikembalikan widget Form dengan properti key diisi _formKey untuk menghubungkan Form dengan kunci yang telah didefinisikan sebelumnya, serta properti child diisi widget Column.
- Widget Column menyusun elemen-elemen pada properti children secara vertikal dari atas ke bawah, berisi dua widget TextFormField.
- TextFormField pertama diisi properti controller dengan _titleController untuk membaca ketikan judul transaksi, properti decoration berupa InputDecoration dengan labelText bertuliskan "Judul Transaksi", serta properti validator yang mengembalikan pesan "Tidak boleh kosong" apabila nilai kosong, atau null apabila nilai terisi.
- TextFormField kedua diisi properti controller dengan _amountController, properti keyboardType bernilai TextInputType.number agar papan ketik yang tampil berupa papan ketik angka, serta properti decoration berupa InputDecoration dengan labelText bertuliskan "Nominal (Rp)".
- Properti validator pada TextFormField kedua memeriksa nilai masukan melalui dua pernyataan if. Apabila nilai kosong, dikembalikan pesan "Wajib diisi". Apabila nilai tidak dapat diubah menjadi bilangan bulat melalui int.tryParse, dikembalikan pesan "Harus angka". Apabila kedua kondisi tidak terpenuhi, dikembalikan null.

2.3 Dropdown & Tombol Simpan
- Widget Column pada Form ditambahkan dua elemen berikutnya, yaitu widget DropdownButtonFormField<String> dan widget ElevatedButton, disisipi widget SizedBox berukuran height 20 sebagai jarak pemisah keduanya.
- DropdownButtonFormField<String> diisi properti value dengan _selectedCategory sebagai kategori terpilih, serta properti items berupa daftar DropdownMenuItem yang dibentuk melalui method map terhadap daftar "Makanan", "Transportasi", dan "Hiburan", dengan masing-masing item bernilai category dan child berupa widget Text yang menampilkan category tersebut.
- Properti onChanged dijalankan setiap kali pilihan dropdown berubah, memanggil setState untuk memperbarui nilai _selectedCategory sesuai pilihan yang dipilih.
- Ditambahkan widget ElevatedButton dengan properti child berupa widget Text bertuliskan "Simpan Transaksi".
- Properti onPressed pada ElevatedButton dijalankan ketika tombol ditekan, memeriksa hasil validasi seluruh TextFormField melalui pemanggilan _formKey.currentState!.validate() pada sebuah pernyataan if. Apabila validasi bernilai benar, ditampilkan SnackBar melalui ScaffoldMessenger.of(context).showSnackBar berisi widget Text bertuliskan "Menyimpan Transaksi...".

2.4 Pengerjaan Full Code
- Diimpor pustaka 'package:flutter/material.dart' yang menyediakan komponen antarmuka bawaan Flutter.
- Didefinisikan class AddTransactionScreen sebagai turunan dari StatefulWidget, dengan constructor const AddTransactionScreen({super.key}), serta method createState yang mengembalikan instance _AddTransactionScreenState.
- Didefinisikan class _AddTransactionScreenState sebagai turunan dari State<AddTransactionScreen>, berisi variabel _formKey bertipe GlobalKey<FormState> untuk mengontrol form dan memicu validasi, variabel _titleController dan _amountController bertipe TextEditingController untuk mengambil teks dari kolom masukan, variabel _selectedCategory bertipe String dengan nilai awal "Makanan", serta variabel _categories bertipe List<String> berisi "Makanan", "Transportasi", "Hiburan", dan "Lainnya" sebagai daftar pilihan kategori.
- Pada method dispose, dipanggil _titleController.dispose() dan _amountController.dispose() untuk membersihkan controller saat halaman ditutup, kemudian dipanggil super.dispose() untuk meneruskan proses pembersihan ke kelas induk.
- Pada method build, dikembalikan widget Scaffold dengan properti appBar diisi widget AppBar bertuliskan "Catat Transaksi Baru", warna latar backgroundColor Colors.deepPurple, dan warna foregroundColor Colors.white.
- Properti body pada Scaffold diisi widget SingleChildScrollView dengan padding sebesar 16 agar tampilan tidak mengalami galat tata letak saat papan ketik muncul, dengan properti child diisi widget Form.
- Widget Form diisi properti key dengan _formKey, serta properti child diisi widget Column dengan properti crossAxisAlignment bernilai CrossAxisAlignment.stretch agar elemen di dalamnya melebar penuh secara horizontal.
- Widget Column menyusun lima elemen pada properti children, yaitu tiga widget masukan (TextFormField judul, TextFormField nominal, dan DropdownButtonFormField kategori), satu widget ElevatedButton, serta beberapa widget SizedBox sebagai jarak pemisah antarelemen.
- TextFormField pertama diisi properti controller dengan _titleController, properti decoration berupa InputDecoration dengan labelText bertuliskan "Judul Transaksi", border berupa OutlineInputBorder, dan prefixIcon berupa Icon Icons.title, serta properti validator yang mengembalikan pesan "Judul transaksi tidak boleh kosong" apabila nilai kosong, atau null apabila nilai terisi.
- TextFormField kedua diisi properti controller dengan _amountController, properti keyboardType bernilai TextInputType.number, properti decoration berupa InputDecoration dengan labelText bertuliskan "Nominal (Rp)", border berupa OutlineInputBorder, dan prefixIcon berupa Icon Icons.attach_money. Properti validator memeriksa nilai masukan melalui dua pernyataan if, mengembalikan pesan "Nominal wajib diisi" apabila nilai kosong, mengembalikan pesan "Harus berupa angka bulat yang valid" apabila nilai tidak dapat diubah menjadi bilangan bulat melalui int.tryParse, atau mengembalikan null apabila kedua kondisi tidak terpenuhi.
- DropdownButtonFormField<String> diisi properti value dengan _selectedCategory, properti decoration berupa InputDecoration dengan labelText bertuliskan "Kategori", border berupa OutlineInputBorder, dan prefixIcon berupa Icon Icons.category, serta properti items berupa daftar DropdownMenuItem<String> yang dibentuk melalui method map terhadap _categories, dengan masing-masing item bernilai category dan child berupa widget Text yang menampilkan category tersebut.
- Properti onChanged pada DropdownButtonFormField dijalankan setiap kali pilihan berubah, memanggil setState untuk memperbarui nilai _selectedCategory sesuai pilihan yang dipilih.
- Ditambahkan widget ElevatedButton dengan properti style diatur melalui ElevatedButton.styleFrom, yaitu backgroundColor Colors.deepPurple, foregroundColor Colors.white, dan padding vertikal sebesar 16, serta properti child berupa widget Text bertuliskan "Simpan Transaksi" dengan style ukuran huruf 16 dan ketebalan FontWeight.bold.
- Properti onPressed pada ElevatedButton dijalankan ketika tombol ditekan, memeriksa hasil validasi seluruh TextFormField melalui pemanggilan _formKey.currentState!.validate() pada sebuah pernyataan if. Apabila validasi bernilai benar, ditampilkan SnackBar melalui ScaffoldMessenger.of(context).showSnackBar berisi widget Text yang menampilkan teks judul dan nominal transaksi yang telah diisi, dengan warna latar backgroundColor Colors.green.

2.5 Integrasi Form pada main.dart
- Pada file 'main.dart', Diimpor pustaka 'package:flutter/material.dart' dan berkas 'add_transaction_screen.dart' untuk memanggil halaman formulir yang telah dibuat sebelumnya.
- Didefinisikan fungsi main sebagai titik masuk aplikasi, memanggil runApp dengan argumen const MyApp() untuk menjalankan aplikasi.
- Didefinisikan class MyApp sebagai turunan dari StatelessWidget, dengan constructor const MyApp({super.key}).
- Pada method build, dikembalikan widget MaterialApp dengan properti debugShowCheckedModeBanner bernilai false untuk menyembunyikan label debug, properti title bernilai "Expense Tracker - Modul 3", properti theme berupa ThemeData dengan primarySwatch Colors.deepPurple sebagai warna tema aplikasi, serta properti home diisi widget AddTransactionScreen agar halaman tersebut langsung ditampilkan saat aplikasi dijalankan.

2.6 Pengerjaan Tugas
A. Penerapan TextFormField untuk Input Tanggal
- Dibuat file baru dengan nama 'tugas_pertemuan3.dart' pada folder lib di projek flutter.
- Ditambahkan widget TextFormField keempat sebelum tombol Simpan Transaksi, diisi properti controller dengan _dateController, properti keyboardType bernilai TextInputType.number, properti decoration berupa InputDecoration dengan labelText bertuliskan "Tanggal Transaksi", prefixIcon berupa Icon Icons.calendar_today, border berupa OutlineInputBorder, serta suffixIcon berupa IconButton dengan icon Icons.add yang memanggil method _pilihTanggal saat ditekan.
- Properti validator pada TextFormField tersebut memeriksa nilai masukan melalui dua pernyataan if, mengembalikan pesan "Tanggal wajib diisi" apabila nilai kosong, mengembalikan pesan "Format tanggal harus DD/MM/YYYY" apabila nilai tidak sesuai pola melalui RegExp, atau mengembalikan null apabila kedua kondisi tidak terpenuhi.

B. Implementasi Method untuk Pemilihan Tanggal
- Ditambahkan method asinkron _pilihTanggal yang memanggil fungsi showDatePicker dengan argumen context, initialDate bernilai _selectedDate atau DateTime.now() apabila belum terisi, firstDate DateTime(2000), dan lastDate DateTime(2100), kemudian hasilnya ditunggu (await) dan disimpan pada variabel pilih.
- Apabila pilih tidak bernilai null, nilai tersebut disimpan ke _selectedDate, tanggal dan bulan diformat menjadi dua digit melalui padLeft, lalu hasilnya ditetapkan sebagai teks pada _dateController dengan format DD/MM/YYYY.

C.Deklarasi Variabel Pendukung Pemilihan Tanggal
- Dideklarasikan variabel _selectedDate bertipe DateTime yang dapat bernilai null untuk menyimpan tanggal yang dipilih melalui method _pilihTanggal, serta variabel _dateController bertipe TextEditingController sebagai pengontrol pembaca teks pada kolom Tanggal Transaksi
- Ditambahkan pemanggilan _dateController.dispose() pada method dispose agar sumber daya (resource) yang dialokasikan oleh controller tersebut turut dibebaskan bersama controller lainnya saat halaman ditutup, guna mencegah terjadinya memory leak.
3. Results & Discussion
3.1 Tampilan Antarmuka Add_Transaction_Screen
Tampilan antarmuka AddTransactionScreen menampilkan sebuah AppBar berwarna ungu tua dengan judul "Catat Transaksi Baru" berwarna putih, diikuti bagian isi berupa formulir yang dapat digulir menggunakan widget SingleChildScrollView agar tidak mengalami galat tata letak saat papan ketik muncul. Formulir tersebut memuat tiga kolom masukan yang disusun secara vertikal, yaitu kolom "Judul Transaksi" untuk menuliskan nama transaksi, kolom "Nominal (Rp)" yang hanya menerima masukan berupa angka, serta kolom "Kategori" berupa dropdown dengan pilihan "Makanan", "Transportasi", "Hiburan", dan "Lainnya", diakhiri tombol "Simpan Transaksi" pada bagian bawah formulir.
Validasi pada formulir diterapkan secara berjenjang pada tiap kolom masukan sebelum data disimpan. Kolom "Judul Transaksi" divalidasi melalui pemeriksaan kekosongan nilai, menampilkan pesan "Judul transaksi tidak boleh kosong" apabila kolom tidak diisi. Kolom "Nominal (Rp)" divalidasi melalui dua pemeriksaan berurutan, yaitu pemeriksaan kekosongan nilai yang menampilkan pesan "Nominal wajib diisi", serta pemeriksaan format angka melalui int.tryParse yang menampilkan pesan "Harus berupa angka bulat yang valid" apabila nilai yang dimasukkan tidak dapat diubah menjadi bilangan bulat. Kolom "Kategori" tidak memerlukan validasi tambahan karena nilai dropdown selalu memiliki opsi terpilih secara default. Seluruh validator tersebut baru dijalankan secara serentak ketika tombol "Simpan Transaksi" ditekan, melalui pemanggilan _formKey.currentState!.validate(), yang mengembalikan nilai true hanya apabila seluruh kolom lolos pemeriksaan. Apabila salah satu kolom tidak valid, pesan kesalahan ditampilkan langsung di bawah kolom yang bersangkutan tanpa mengubah nilai yang telah diisi pada kolom lain, sedangkan notifikasi (SnackBar) berisi ringkasan transaksi hanya ditampilkan apabila seluruh data dinyatakan valid.




3.2 Hasil Pengerjaan Tugas
Hasil pengerjaan tugas menambahkan satu kolom masukan baru pada formulir, yaitu kolom "Tanggal Transaksi" yang diletakkan sebelum tombol "Simpan Transaksi". Kolom tersebut dilengkapi ikon kalender pada bagian awal serta tombol tambah pada bagian sisi form yang berfungsi memunculkan date picker bawaan Flutter melalui showDatePicker. Tanggal yang dipilih pengguna melalui date picker tersebut secara otomatis diformat menjadi pola DD/MM/YYYY dan ditampilkan pada kolom.
Validasi pada kolom "Tanggal Transaksi" diterapkan melalui dua pemeriksaan berurutan. Pemeriksaan pertama memeriksa kekosongan nilai, menampilkan pesan "Tanggal wajib diisi" apabila kolom tidak diisi baik melalui date picker maupun pengetikan manual. Pemeriksaan kedua memeriksa kesesuaian format teks terhadap pola dua digit tanggal, dua digit bulan, dan empat digit tahun yang dipisahkan tanda garis miring melalui RegExp, menampilkan pesan "Format tanggal harus DD/MM/YYYY" apabila teks yang dimasukkan tidak sesuai pola tersebut. Validasi ini turut berlaku terhadap pengisian secara manual pada kolom, karena kolom tetap dapat menerima ketikan langsung selain melalui date picker. Sama seperti kolom lainnya, validator pada kolom Tanggal Transaksi turut dijalankan bersamaan saat tombol "Simpan Transaksi" ditekan melalui _formKey.currentState!.validate(), sehingga transaksi hanya tersimpan apabila seluruh kolom, termasuk tanggal transaksi, telah terisi dengan format yang benar.




4. Conclusion
Berdasarkan praktik yang telah dilakukan, dapat disimpulkan bahwa TextEditingController berperan sebagai penghubung antara antarmuka dan data yang diketik pengguna, sehingga nilai pada setiap kolom masukan dapat dibaca dan diproses lebih lanjut. Widgetdropdown berperan menyediakan pilihan kategori yang tetap dan konsisten, sedangkan validator pada setiap kolom berperan mencegah data kosong maupun data dengan format yang tidak sesuai agar tidak tersimpan ke dalam sistem. Seluruh komponen tersebut ditempatkan pada sebuah StatefulWidget agar perubahan nilai kolom maupun pilihan dropdown dapat diperbarui secara langsung pada tampilan melalui setState. Pada pemenuhan tugas, ditambahkan pula kolom tanggal transaksi yang memanfaatkan date picker bawaan Flutter beserta validasi format tanggal, sehingga formulir yang dibangun mampu menangkap data transaksi secara lebih lengkap dan akurat.