LAPORAN PRAKTIKUM 5 DAN PROYEK APLIKASI MOBILE

Oct 26, 2026 •Daffa Aira Adrin

1. Introduction

1.1 Latar Belakang

Aplikasi yang memiliki banyak halaman membutuhkan arsitektur navigasi yang terpusat agar perpindahan antarhalaman tetap rapi dan mudah dikelola. Pada Flutter, mekanisme named routes memungkinkan setiap halaman didaftarkan dengan nama berupa string pada properti routes milik MaterialApp, sedangkan properti initialRoute menentukan halaman pertama yang dimuat, sehingga kode terhindar dari spaghetti code ketika jumlah layar bertambah. Selain itu, BottomNavigationBar menyediakan menu navigasi di bagian bawah layar yang mengganti tampilan utama (body) tanpa berpindah Scaffold, dengan properti currentIndex sebagai penanda tab yang aktif dan items sebagai daftar ikon serta label tab. Di sisi lain, aplikasi yang hanya menggunakan data statis akan kehilangan data yang dimasukkan pengguna setiap kali aplikasi ditutup, sehingga dibutuhkan penyimpanan lokal (local storage) seperti SQLite. Pada Flutter, SQLite dapat digunakan melalui paket sqflite untuk menyimpan data pengguna dan transaksi secara permanen pada perangkat. Penguasaan navigasi terpusat dan penyimpanan lokal tersebut penting karena keduanya menjadi fondasi aplikasi mobile yang memiliki banyak halaman, proses autentikasi pengguna, dan data yang bertahan lama.

Berdasarkan hal tersebut, dilakukan praktik penerapan arsitektur navigasi pada aplikasi Expense Tracker, meliputi pendaftaran named routes melalui initialRoute dan routes, pembuatan penampung tab MainScreen dengan BottomNavigationBar, serta pembaruan tombol tambah transaksi menggunakan Navigator.pushNamed. Praktik dilanjutkan dengan tugas praktikum berupa halaman riwayat transaksi, halaman profil, dan halaman login yang mengirimkan email ke halaman utama melalui arguments. Selanjutnya, hasil Praktikum 1 sampai Praktikum 5 digabungkan menjadi satu proyek akhir yang dilengkapi penyimpanan SQLite menggunakan sqflite, fitur register dan login, serta berkas APK. Kegiatan ini disusun sebagai pemenuhan tugas mata kuliah Aplikasi Mobile pada Departemen Informatika, Universitas Andalas, tahun ajaran 2026/2027.

 

1.2 RUMUSAN MASALAH

  1. Bagaimana penerapan named routes melalui initialRoute, routes, dan Navigator.pushNamed dalam mengatur perpindahan antarhalaman aplikasi?
  2. Bagaimana penerapan BottomNavigationBar pada StatefulWidget MainScreen dalam mengintegrasikan halaman Beranda, Riwayat, dan Profil dalam satu wadah?
  3. Bagaimana pembuatan halaman riwayat transaksi, halaman profil, dan halaman login yang mengirimkan email ke halaman utama melalui arguments?
  4. Bagaimana penerapan penyimpanan lokal SQLite menggunakan sqflite pada fitur register, login, dan pencatatan transaksi agar data tidak hilang ketika aplikasi ditutup, hingga aplikasi dihasilkan dalam bentuk berkas APK?

1.3 TUJUAN

  1. Memahami penerapan named routes dalam mengatur perpindahan antarhalaman aplikasi.
  2. Memahami penerapan BottomNavigationBar dalam mengintegrasikan beberapa halaman dalam satu wadah.
  3. Memahami pembuatan halaman riwayat transaksi, profil, dan login beserta pengiriman data antarhalaman melalui arguments.
  4. Memahami penerapan penyimpanan lokal SQLite pada fitur register, login, dan pencatatan transaksi hingga pembuatan berkas APK.

2. Methodology

2.1 Praktikum 5

A. Mendaftarkan Named Routes

  1. Pada fungsi main, dipanggil fungsi runApp dengan argumen berupa instance kelas MyApp, sehingga MyApp dijadikan root widget pada widget tree aplikasi.
  2. Kelas MyApp dideklarasikan sebagai StatelessWidget karena tidak memiliki state yang berubah. Pada metode build, dikembalikan widget MaterialApp sebagai penyedia struktur dasar aplikasi bergaya Material Design.
  3. Properti debugShowCheckedModeBanner pada MaterialApp diisi false agar banner DEBUG disembunyikan, properti title diisi "Expense Tracker Final", dan properti theme diisi ThemeData dengan primarySwatch bernilai Colors.amber.
  4. Properti home tidak digunakan pada MaterialApp. Sebagai gantinya, properti initialRoute diisi "/", sehingga route "/" dimuat pertama kali saat aplikasi dijalankan.
  5. Properti routes diisi Map yang memetakan nama route ke fungsi builder berparameter context, yaitu route "/" ke LoginScreen, route "/home" ke MainScreen, dan route "/add" ke AddTransactionScreen yang berasal dari Modul IV.
  6. Pada modul, route "/" diarahkan ke MainScreen. Karena tugas praktikum mensyaratkan halaman login sebagai halaman pertama, route "/" diarahkan ke LoginScreen dan MainScreen dipindahkan ke route "/home".
Kode Penerapan Named Routes

 

  1. Output Aplikasi:
Tampilan Halaman Login

B. Membuat Penampung Tab (MainScreen) dan Merangkai BottomNavigationBar

  1. Kelas MainScreen dideklarasikan sebagai StatefulWidget, dan objek state _MainScreenState dibentuk melalui metode createState, karena indeks tab yang aktif harus disimpan sebagai state yang dapat berubah.
  2. Pada kelas _MainScreenState, dideklarasikan variabel _selectedIndex bertipe int dengan nilai awal 0, sehingga tab berindeks 0, yaitu Beranda, ditampilkan secara bawaan.
  3. Fungsi _onItemTapped dideklarasikan dengan parameter index bertipe int. Di dalamnya, dipanggil setState untuk mengisi _selectedIndex dengan nilai index, sehingga metode build dijalankan ulang (rebuild) dan isi body diperbarui.
  4. Pada metode build, nilai arguments dari route dibaca melalui ModalRoute.of(context)!.settings.arguments, lalu dikonversi menjadi String melalui operator as dan disimpan pada variabel email.
  5. Variabel pages bertipe List<Widget> dideklarasikan dengan isi DashboardScreen dan ProfileScreen yang diberi parameter email melalui constructor, serta HistoryScreen. Variabel ini dideklarasikan di dalam metode build karena nilainya bergantung pada email yang dibaca dari context.
  6. Pada metode build, dikembalikan widget Scaffold dengan bagian body diisi pages[_selectedIndex], sehingga widget halaman yang ditampilkan ditukar sesuai indeks tab yang aktif.
  7. Bagian bottomNavigationBar diisi widget BottomNavigationBar. Properti currentIndex diisi _selectedIndex, callback onTap diisi fungsi _onItemTapped yang menerima indeks item yang ditekan, dan selectedItemColor diisi Colors.amber[800] sebagai warna item yang aktif.
  8. Properti items diisi tiga BottomNavigationBarItem, masing-masing dengan properti icon berupa widget Icon dan properti label, yaitu Icons.home berlabel "Beranda", Icons.history berlabel "Riwayat", dan Icons.person berlabel "Profil".
Kode BottomNavigationBar
  1. Output program:
Tampilan BottomNavigationBar

C. Pembaharuan Dashboard Screen

  1. Kelas DashboardScreen dideklarasikan sebagai StatelessWidget dengan field email bertipe String yang bersifat final dan diisi melalui constructor dengan parameter required this.email.
  2. Pada metode build, dikembalikan Scaffold dengan bagian appBar diisi AppBar. Properti title pada AppBar diisi Text "Dashboard Keuangan", backgroundColor diisi Colors.red, dan foregroundColor diisi Colors.white.
  3. Bagian floatingActionButton diisi FloatingActionButton dengan backgroundColor Colors.red, foregroundColor Colors.white, dan child berupa Icon Icons.add.
  4. Pada callback onPressed milik FloatingActionButton, dipanggil Navigator.pushNamed(context, "/add"), sehingga route "/add" ditambahkan ke navigation stack berdasarkan nama yang didaftarkan pada main.dart. Pemanggilan ini menggantikan Navigator.push dengan MaterialPageRoute dari Modul IV.
  5. Bagian body diisi SingleChildScrollView dengan padding EdgeInsets.all(16.0), yang membungkus Column dengan crossAxisAlignment bernilai CrossAxisAlignment.start.
  6. Elemen pertama pada children milik Column berupa Text "Halo, $email" yang menyisipkan nilai email melalui string interpolation, dengan TextStyle berukuran fontSize 20 dan fontWeight FontWeight.bold.
  7. Elemen berikutnya disusun secara berurutan, yaitu SizedBox setinggi 20 piksel, hasil fungsi _buildBalanceCard, SizedBox setinggi 20 piksel, Text "Transaksi Akhir" berukuran 18 dan bercetak tebal, SizedBox setinggi 12 piksel, serta hasil fungsi _buildTransactionList yang diberi argumen context.
  8. Pada fungsi _buildBalanceCard, dikembalikan Card dengan elevation 4, color Colors.redAccent, dan shape berupa RoundedRectangleBorder dengan borderRadius BorderRadius.circular(16).
  9. Child dari Card tersebut diisi Padding EdgeInsets.all(20.0) yang membungkus Column rata kiri, berisi Text "Saldo utama" berukuran 16 berwarna Colors.white70, SizedBox setinggi 8 piksel, dan Text "Rp 2.000.000" berukuran 28, bercetak tebal, dan berwarna Colors.white.
  10. Fungsi _buildTransactionList dideklarasikan dengan parameter context bertipe BuildContext. Di dalamnya, dikembalikan Card dengan elevation 2 yang berisi Column dengan dua ListTile yang dipisahkan oleh Divider setinggi 1 piksel.
  11. Pada ListTile pertama, bagian leading diisi CircleAvatar berwarna Colors.orange dengan Icon Icons.fastfood berwarna putih, title diisi "Makan Siang", subtitle diisi "Hari ini", dan trailing diisi "- Rp 25.000" berwarna merah dan bercetak tebal.
  12. Pada ListTile kedua, bagian leading diisi CircleAvatar berwarna Colors.green dengan Icon Icons.attach_money, title diisi "Gaji Bulanan", subtitle diisi "1 September 2026", dan trailing diisi "+ Rp 2.500.000" berwarna hijau dan bercetak tebal.
  13. Pada callback onTap di setiap ListTile, dipanggil Navigator.push dengan MaterialPageRoute yang builder-nya mengembalikan TransactionDetailScreen. Data title, amount, category, dan date dikirim melalui constructor, yaitu "Makan siang", "- Rp 25.000", "Makanan", dan "13 September 2026" untuk transaksi pertama, serta "Gaji Bulanan", "+ Rp 2.500.000", "Pemasukan", dan "1 September 2026" untuk transaksi kedua.
Kode Program Dashboard Screen Updated
Kode Program Dashboard Screen Updated
Kode Program Dashboard
  1. Output Aplikasi:
Output Update
Output

 

2.2 Tugas Praktikum

A. Halaman Riwayat Transaksi

  1. Kelas HistoryScreen dideklarasikan sebagai StatelessWidget. Pada metode build, dikembalikan Scaffold dengan AppBar yang title-nya diisi "Riwayat Transaksi", backgroundColor diisi Colors.red, dan foregroundColor diisi Colors.white.
  2. Bagian body diisi SingleChildScrollView dengan padding EdgeInsets.all(16.0), dan child-nya diisi RecentTransactionWidget, yaitu widget daftar transaksi dari Praktikum 2 yang digunakan kembali.
  3. Kelas RecentTransactionWidget dideklarasikan sebagai StatelessWidget. Pada metode build, dikembalikan Column rata kiri melalui CrossAxisAlignment.start dengan children berupa Text "Transaksi Terakhir" berukuran 18 dan bercetak tebal, SizedBox setinggi 12 piksel, serta Card.
  4. Card tersebut diberi elevation 2 dan shape RoundedRectangleBorder dengan BorderRadius.circular(12), lalu child-nya diisi Column yang memuat lima ListTile.
  5. Setiap ListTile diberi leading berupa CircleAvatar dengan Icon berwarna putih, title berupa nama transaksi, subtitle berupa tanggal, dan trailing berupa nominal yang bercetak tebal, berwarna merah untuk pengeluaran dan hijau untuk pemasukan.
  6. Isi kelima ListTile tersebut adalah "Makan Siang" dengan Icons.fastfood berlatar Colors.redAccent, tanggal "13 Sep 2026", dan nominal "- Rp 25.000", serta "Gaji Bulanan" dengan Icons.attach_money berlatar Colors.green, tanggal "01 Sep 2026", dan nominal "+Rp 2.500.000".
  7. Tiga ListTile berikutnya berlatar Colors.blueAccent dengan tanggal "10 Sep 2026", yaitu "Naik Transpadang" dengan Icons.directions_bus dan nominal "- Rp 3.500", "Google One" dengan Icons.cloud dan nominal "- Rp 85.000", serta "Anthropic" dengan Icons.phone_android dan nominal "- Rp 387.000".
  8. Divider setinggi 1 piksel diletakkan setelah ListTile pertama dan kedua sebagai pemisah antaritem.
a
b
c
  1. Output Kode Program:
d

B. Halaman Profile

  1. Kelas ProfileScreen dideklarasikan sebagai StatelessWidget dengan field email bertipe String yang bersifat final dan diisi melalui constructor.
  2. Pada metode build, dideklarasikan variabel nama bertipe String yang diisi hasil email.split('@')[0], yaitu elemen pertama dari List hasil pemecahan email berdasarkan karakter @.
  3. Dikembalikan Scaffold dengan AppBar yang title-nya diisi "Profil", backgroundColor diisi Colors.red, dan foregroundColor diisi Colors.white. Bagian body diisi Padding EdgeInsets.all(24.0) yang membungkus Column rata tengah melalui CrossAxisAlignment.center.
  4. Elemen pertama Column berupa CircleAvatar dengan radius 50 dan backgroundColor Colors.redAccent, serta child berupa Icon Icons.person berukuran 60 berwarna putih.
  5. Setelah SizedBox setinggi 16 piksel, ditampilkan Text berisi nama berukuran 24 dan bercetak tebal. Kemudian, setelah SizedBox setinggi 8 piksel, ditampilkan Text berisi email berukuran 16 berwarna Colors.grey, lalu diberi SizedBox setinggi 32 piksel.
  6. Elemen terakhir berupa SizedBox dengan width double.infinity yang membungkus ElevatedButton. Properti style diisi ElevatedButton.styleFrom dengan backgroundColor Colors.red, foregroundColor Colors.white, dan padding EdgeInsets.symmetric(vertical: 16), sedangkan child diisi Text "Logout" berukuran 16.
  7. Pada callback onPressed, dipanggil Navigator.pushReplacementNamed(context, '/'), sehingga route MainScreen diganti dengan route "/" (LoginScreen) pada navigation stack dan halaman utama tidak dapat dibuka kembali melalui tombol back.
a
  1. Output kode program:
s

C. Halaman Login

  1. Kelas LoginScreen dideklarasikan sebagai StatefulWidget, dan objek state _LoginScreenState dibentuk melalui metode createState.
  2. Pada kelas _LoginScreenState, dideklarasikan _formKey bertipe GlobalKey<FormState> untuk mengakses state Form dan memicu validasi, serta _emailController dan _passwordController bertipe TextEditingController untuk mengambil teks dari TextFormField.
  3. Pada metode dispose, dipanggil _emailController.dispose() dan _passwordController.dispose() sebelum super.dispose(), sehingga kedua controller dilepaskan dari memori saat state dihapus dari widget tree.
  4. Pada fungsi _submitForm, dipanggil _formKey.currentState!.validate() untuk menjalankan seluruh validator. Bila seluruh validator mengembalikan null, dipanggil Navigator.pushReplacementNamed menuju route "/home" dengan arguments berisi _emailController.text, sehingga route login diganti dengan halaman utama.
  5. Pada metode build, dikembalikan Scaffold dengan AppBar yang title-nya diisi "Login", backgroundColor diisi Colors.red, dan foregroundColor diisi Colors.white.
  6. Bagian body diisi SingleChildScrollView dengan padding EdgeInsets.all(16.0) yang membungkus Form. Properti key pada Form diisi _formKey, dan child-nya diisi Column yang children-nya direntangkan selebar layar melalui CrossAxisAlignment.stretch.
  7. Elemen awal Column berupa Icon Icons.account_circle berukuran 100 berwarna Colors.redAccent yang diapit dua SizedBox setinggi 24 piksel.
  8. TextFormField pertama dihubungkan ke _emailController melalui properti controller, dengan keyboardType TextInputType.emailAddress. Properti decoration diisi InputDecoration dengan labelText "Email", border OutlineInputBorder, dan prefixIcon Icons.email.
  9. Pada validator TextFormField pertama, dikembalikan "Email wajib diisi" bila value bernilai null atau kosong (isEmpty), "Email tidak valid" bila value tidak mengandung karakter @ (contains), dan null bila masukan sah.
  10. Setelah SizedBox setinggi 16 piksel, TextFormField kedua dihubungkan ke _passwordController dengan obscureText bernilai true agar karakter password disamarkan. InputDecoration-nya diisi labelText "Password", OutlineInputBorder, dan prefixIcon Icons.lock, sedangkan validator-nya mengembalikan "Password wajib diisi" bila kosong.
  11. Setelah SizedBox setinggi 32 piksel, ditambahkan ElevatedButton dengan style ElevatedButton.styleFrom berwarna latar Colors.red, foregroundColor Colors.white, dan padding vertikal 16. Callback onPressed diisi _submitForm, sedangkan child diisi Text "Login" berukuran 16.
  12. Agar email ditampilkan di halaman utama, route "/" pada main.dart diarahkan ke LoginScreen, arguments dibaca oleh MainScreen melalui ModalRoute, dan email ditampilkan oleh DashboardScreen melalui Text "Halo, $email", sebagaimana dijelaskan pada bagian Praktikum 5.
a
v
  1. Output Kode Program:
n
n
v

2.3 Pengerjaan Proyek

A. Persiapan Proyek dan Paket SQLite

  1. Proyek Flutter baru dibuat melalui perintah flutter create tugas_akhir, sehingga struktur proyek beserta folder lib, berkas pubspec.yaml, dan folder platform seperti android dan windows dibentuk secara otomatis.
  2. Paket sqflite dan path ditambahkan ke bagian dependencies pada pubspec.yaml melalui perintah flutter pub add sqflite path. Paket sqflite digunakan sebagai plugin SQLite, sedangkan paket path digunakan untuk fungsi join dalam menyusun alamat berkas basis data.
  3. Paket sqflite_common_ffi ditambahkan melalui perintah flutter pub add sqflite_common_ffi, sehingga sqflite dapat dijalankan pada platform Windows melalui FFI (Foreign Function Interface) untuk keperluan uji coba di laptop.
  4. Berkas Dart hasil Praktikum 1 sampai Praktikum 5 disalin ke folder lib, lalu disesuaikan dengan basis data sebagaimana dijelaskan pada bagian berikutnya. Folder test bawaan dihapus karena berisi pengujian aplikasi counter yang tidak sesuai dengan proyek.
  5. Atribut android:label pada berkas android/app/src/main/AndroidManifest.xml diubah menjadi "Expense Tracker", sehingga nama aplikasi pada launcher Android ditampilkan sesuai.
v

B. Pemodelan Data

  1. Kelas UserModel dideklarasikan dengan field id bertipe int? (nullable) karena nilainya diisi otomatis oleh SQLite saat data disimpan, serta field name, email, dan password bertipe String. Seluruh field bersifat final.
  2. Constructor UserModel dideklarasikan dengan named parameter, yaitu this.id yang bersifat opsional serta name, email, dan password yang diberi kata kunci required.
  3. Pada metode toMap, dikembalikan Map<String, Object?> dengan key yang disamakan dengan nama kolom tabel pengguna, yaitu "id", "nama", "email", dan "kataSandi", sehingga objek dapat disimpan langsung melalui db.insert.
  4. Kelas TransactionModel dideklarasikan dengan field final id bertipe int?, userId bertipe int, title bertipe String, amount bertipe int agar nominal dapat dijumlahkan, serta category dan date bertipe String. Constructor-nya menggunakan named parameter, dengan id bersifat opsional dan field lain diberi required.
  5. Getter isIncome bertipe bool dideklarasikan dengan sintaks arrow (=>) dan mengembalikan true bila category bernilai "Pemasukan", sehingga transaksi tersebut dihitung sebagai penambah saldo, sedangkan kategori lain dihitung sebagai pengurang saldo.
  6. Pada metode toMap milik TransactionModel, field dipetakan ke kolom tabel transaksi, yaitu id ke "id", userId ke "idPengguna", title ke "judul", amount ke "nominal", category ke "kategori", dan date ke "tanggal".
v
v

C. Pembuatan Basis Data SQLite

  1. Pada berkas database.dart, dideklarasikan getter tingkat atas database bertipe Future<Database> yang bersifat async. Di dalamnya, dikembalikan hasil openDatabase dengan alamat berkas berupa hasil join antara await getDatabasesPath() dan nama berkas "expense_tracker.db".
  2. Fungsi getDatabasesPath digunakan untuk memperoleh direktori basis data bawaan setiap platform, sehingga berkas expense_tracker.db disimpan pada lokasi yang sesuai di Android maupun Windows.
  3. Properti onCreate pada openDatabase diisi callback async berparameter db dan version yang hanya dijalankan saat berkas basis data belum ada. Di dalamnya, dijalankan dua perintah CREATE TABLE melalui await db.execute.
  4. Perintah pertama membentuk tabel pengguna dengan kolom id INTEGER PRIMARY KEY, nama TEXT, email TEXT UNIQUE agar satu email hanya dimiliki satu akun, dan kataSandi TEXT. Perintah kedua membentuk tabel transaksi dengan kolom id INTEGER PRIMARY KEY, idPengguna INTEGER, judul TEXT, nominal INTEGER, kategori TEXT, dan tanggal TEXT.
  5. Properti version diisi 1 sebagai nomor versi skema basis data. Nilai ini menjadi acuan bila struktur tabel diubah pada versi aplikasi berikutnya.
  6. Fungsi insertUser bertipe Future<void> dideklarasikan async dengan parameter user bertipe UserModel. Referensi basis data diambil melalui final db = await database, lalu data disimpan ke tabel pengguna melalui await db.insert('pengguna', user.toMap()).
  7. Fungsi getUser bertipe Future<UserModel?> dideklarasikan dengan parameter email. Di dalamnya, dipanggil await db.query pada tabel pengguna dengan where "email = ?" dan whereArgs berisi email, sehingga placeholder ? diisi sebagai data, bukan bagian perintah SQL, dan query terlindungi dari SQL injection.
  8. Hasil query disimpan pada variabel userMaps bertipe List<Map<String, Object?>>, lalu dikonversi menjadi List<UserModel> bernama users melalui collection for dengan map pattern yang mengambil kolom id, nama, email, dan kataSandi beserta pengecekan tipe melalui as int dan as String.
  9. Pada akhir fungsi getUser, dikembalikan null bila users.isEmpty bernilai true, atau users.first bila data ditemukan, menggunakan ternary operator. Elemen pertama diambil karena kolom email bersifat UNIQUE, sehingga hasil query paling banyak berisi satu baris.
  10. Fungsi insertTransaction bertipe Future<void> dideklarasikan dengan parameter transaction bertipe TransactionModel, lalu data disimpan ke tabel transaksi melalui await db.insert('transaksi', transaction.toMap()).
  11. Fungsi transactions bertipe Future<List<TransactionModel>> dideklarasikan dengan parameter userId. Di dalamnya, dipanggil await db.query pada tabel transaksi dengan where "idPengguna = ?", whereArgs berisi userId, dan orderBy "id DESC", sehingga hanya transaksi milik pengguna tersebut yang diambil dengan urutan terbaru di atas.
  12. Hasil query pada variabel transactionMaps dikonversi melalui collection for dengan map pattern yang mengambil kolom id, idPengguna, judul, nominal, kategori, dan tanggal, lalu setiap baris dibentuk menjadi objek TransactionModel dan List tersebut dikembalikan.
v
a

D. Konfigurasi Aplikasi dan Route

  1. Pada fungsi main, diperiksa kondisi Platform.isWindows dari pustaka dart:io. Bila aplikasi dijalankan pada Windows, dipanggil sqfliteFfiInit() dan variabel databaseFactory diisi databaseFactoryFfi, sehingga sqflite dijalankan melalui FFI.
  2. Setelah inisialisasi tersebut, dipanggil runApp dengan argumen instance kelas MyApp. Pada platform Android, blok if dilewati dan sqflite dijalankan secara bawaan.
  3. Kelas MyApp dideklarasikan sebagai StatelessWidget. Pada metode build, dikembalikan MaterialApp dengan debugShowCheckedModeBanner false, title "Expense Tracker", theme berupa ThemeData dengan primarySwatch Colors.amber, dan initialRoute "/".
  4. Properti routes diisi pemetaan route "/" ke LoginScreen, "/register" ke RegisterScreen, "/home" ke MainScreen, dan "/add" ke AddTransactionScreen. Route "/register" ditambahkan sebagai route baru untuk halaman register.
v
  1. Output kode program:
v

E. Halaman Register

  1. Kelas RegisterScreen dideklarasikan sebagai StatefulWidget, dan objek state _RegisterScreenState dibentuk melalui metode createState.
  2. Pada kelas _RegisterScreenState, dideklarasikan _formKey bertipe GlobalKey<FormState> serta empat TextEditingController, yaitu _nameController, _emailController, _passwordController, dan _confirmPasswordController.
  3. Pada metode dispose, keempat controller tersebut dilepaskan melalui pemanggilan dispose() masing-masing sebelum super.dispose() dipanggil.
  4. Fungsi _submitForm bertipe Future<void> dideklarasikan async. Bila _formKey.currentState!.validate() bernilai true, dipanggil await getUser(_emailController.text) dan hasilnya disimpan pada variabel user bertipe UserModel?, lalu diperiksa if (!mounted) return agar context tidak digunakan setelah state dihapus dari widget tree.
  5. Bila user tidak bernilai null, berarti email sudah terdaftar, sehingga dipanggil ScaffoldMessenger.of(context).showSnackBar dengan SnackBar berisi Text "Email sudah terdaftar" dan backgroundColor Colors.red, lalu fungsi dihentikan melalui return.
  6. Bila email belum terdaftar, dipanggil await insertUser dengan objek UserModel yang name, email, dan password-nya diisi dari teks ketiga controller, sedangkan id tidak diisi karena dibuat otomatis oleh SQLite.
  7. Setelah data tersimpan, mounted diperiksa kembali, lalu ditampilkan SnackBar "Register berhasil, silakan login" dengan backgroundColor Colors.green, dan RegisterScreen dikeluarkan dari navigation stack melalui Navigator.pop(context), sehingga LoginScreen ditampilkan kembali.
  8. Pada metode build, dikembalikan Scaffold dengan AppBar yang title-nya diisi "Register", backgroundColor diisi Colors.amber, dan foregroundColor diisi Colors.black. Bagian body diisi SingleChildScrollView dengan padding 16 piksel yang membungkus Form ber-key _formKey dan Column dengan CrossAxisAlignment.stretch.
  9. TextFormField pertama dihubungkan ke _nameController dengan InputDecoration berisi labelText "Nama", border OutlineInputBorder, dan prefixIcon Icons.person, sedangkan validator-nya mengembalikan "Nama wajib diisi" bila value bernilai null atau kosong.
  10. TextFormField kedua dihubungkan ke _emailController dengan keyboardType TextInputType.emailAddress, labelText "Email", dan prefixIcon Icons.email. Validator-nya mengembalikan "Email wajib diisi" bila kosong dan "Email tidak valid" bila tidak mengandung karakter @.
  11. TextFormField ketiga dihubungkan ke _passwordController dengan obscureText true, labelText "Password", dan prefixIcon Icons.lock, sedangkan validator-nya mengembalikan "Password wajib diisi" bila kosong.
  12. TextFormField keempat dihubungkan ke _confirmPasswordController dengan obscureText true, labelText "Konfirmasi Password", dan prefixIcon Icons.lock. Validator-nya membandingkan value dengan _passwordController.text dan mengembalikan "Password tidak sama" bila keduanya berbeda.
  13. Keempat TextFormField dipisahkan oleh SizedBox setinggi 16 piksel. Setelah SizedBox setinggi 32 piksel, ditambahkan ElevatedButton dengan ElevatedButton.styleFrom berwarna latar Colors.amber[400], foregroundColor Colors.black, dan padding vertikal 16, dengan onPressed diisi _submitForm dan child diisi Text "Register" berukuran 16.
v
v
v
v
  1. Output kode program:
v
v
v

F. Halaman Login dengan Basis Data

  1. Struktur LoginScreen dari tugas praktikum digunakan kembali, meliputi StatefulWidget dengan createState, _formKey, _emailController, _passwordController, metode dispose, Form, serta dua TextFormField email dan password beserta validator-nya.
  2. Warna disesuaikan dengan tema aplikasi, yaitu backgroundColor AppBar diisi Colors.amber dengan foregroundColor Colors.black, Icon Icons.account_circle diisi Colors.amber, dan tombol "Login" diberi latar Colors.amber[400] dengan teks hitam.
  3. Fungsi _submitForm diubah menjadi Future<void> yang bersifat async. Setelah validasi berhasil, dipanggil await getUser(_emailController.text) untuk mengambil data pengguna dari tabel pengguna, lalu mounted diperiksa.
  4. Bila user bernilai null atau user.password tidak sama dengan _passwordController.text, ditampilkan SnackBar "Email atau password salah" dengan backgroundColor Colors.red, lalu fungsi dihentikan melalui return.
  5. Bila data cocok, dipanggil Navigator.pushReplacementNamed(context, '/home', arguments: user), sehingga objek UserModel dikirim ke MainScreen melalui arguments dan route login diganti dengan halaman utama.
  6. Di bawah tombol "Login", ditambahkan SizedBox setinggi 16 piksel dan ElevatedButton berwarna latar Colors.amber dengan foregroundColor Colors.black dan child Text "Belum punya akun? Register". Pada onPressed-nya, dipanggil Navigator.pushNamed(context, '/register') untuk menambahkan RegisterScreen ke navigation stack.
v
v
v
  1. Output kode program:
v

G. Halaman Utama dan Dashboard

  1. Pada MainScreen, _selectedIndex, _onItemTapped, Scaffold, dan BottomNavigationBar beserta tiga BottomNavigationBarItem dari Praktikum 5 digunakan kembali, dengan selectedItemColor diubah menjadi Colors.amber.
  2. Pada metode build milik _MainScreenState, arguments dikonversi menjadi UserModel melalui as UserModel dan disimpan pada variabel user. List pages diisi DashboardScreen, HistoryScreen, dan ProfileScreen yang masing-masing diberi parameter user melalui constructor.
v
  1. Pada bagian atas berkas dashboard_screen.dart, dideklarasikan fungsi tingkat atas formatRupiah dengan parameter value bertipe int. Angka dikonversi menjadi String melalui toString, lalu titik disisipkan setiap tiga digit dari kanan melalui replaceAllMapped dengan pola RegExp, misalnya 5000000 menjadi 5.000.000.
  2. Kelas DashboardScreen diubah menjadi StatefulWidget dengan field user bertipe UserModel yang diisi melalui constructor, dan objek state _DashboardScreenState dibentuk melalui metode createState.
  3. Pada kelas _DashboardScreenState, dideklarasikan variabel _transactions bertipe List<TransactionModel> dengan nilai awal List kosong. Pada metode initState, dipanggil super.initState() lalu _loadTransactions(), sehingga data dimuat satu kali saat state pertama kali dimasukkan ke widget tree.
  4. Fungsi _loadTransactions bertipe Future<void> dideklarasikan async. Di dalamnya, dipanggil await transactions(widget.user.id!), dengan widget digunakan untuk mengakses field user milik DashboardScreen dan operator null assertion (!) digunakan untuk mengubah id bertipe int? menjadi int.
  5. Hasil query disimpan pada variabel data, lalu diperiksa if (!mounted) return. Bila state masih terpasang, dipanggil setState untuk mengisi _transactions dengan data, sehingga widget di-rebuild dengan data dari basis data.
  6. Fungsi _hitungSaldo bertipe int dideklarasikan dengan variabel lokal saldo bernilai awal 0. Melalui perulangan for pada _transactions, amount ditambahkan ke saldo bila isIncome bernilai true dan dikurangkan bila false, lalu saldo dikembalikan.
  7. Pada metode build, dikembalikan Scaffold dengan AppBar yang title-nya diisi "Dashboard Keuangan", backgroundColor diisi Colors.amber, dan foregroundColor diisi Colors.black. Bagian floatingActionButton diisi FloatingActionButton berwarna Colors.amber dengan foregroundColor Colors.black dan Icon Icons.add.
  8. Callback onPressed pada FloatingActionButton dideklarasikan async. Di dalamnya, dipanggil await Navigator.pushNamed(context, "/add", arguments: widget.user.id). Future tersebut selesai ketika AddTransactionScreen dikeluarkan dari navigation stack, lalu _loadTransactions() dipanggil kembali untuk memuat ulang data.
  9. Bagian body diisi SingleChildScrollView dengan padding 16 piksel yang membungkus Column rata kiri. Children-nya berupa GreetingWidget dengan parameter name berisi widget.user.name, BalancedCardWidget dengan parameter balance berisi hasil _hitungSaldo(), dan ActionButtonsWidget, masing-masing dipisahkan oleh SizedBox setinggi 20 piksel.
  10. Elemen berikutnya berupa Text "Transaksi Akhir" berukuran 18 dan bercetak tebal, SizedBox setinggi 12 piksel, serta TransactionListWidget dengan parameter transactions berisi _transactions.take(3).toList(), sehingga hanya tiga transaksi terbaru yang ditampilkan.
  11. Kelas GreetingWidget dari Praktikum 1 dideklarasikan sebagai StatelessWidget dengan field name. Pada metode build, dikembalikan Row berisi CircleAvatar dengan radius 24, backgroundColor Colors.amber, dan Icon Icons.person berukuran 30 berwarna putih, diikuti SizedBox selebar 12 piksel.
  12. Elemen terakhir Row milik GreetingWidget berupa Column rata kiri berisi Text "Halo $name" berukuran 20 dan bercetak tebal, serta Text "Selamat Datang Kembali!" berukuran 14 berwarna Colors.grey.
  13. Kelas BalancedCardWidget dari Praktikum 1 dideklarasikan sebagai StatefulWidget dengan field balance bertipe int, dan objek state _BalancedCardWidgetState dibentuk melalui createState. Pada kelas state-nya, dideklarasikan variabel _isBalanceVisible bertipe bool bernilai true dan fungsi _toggleVisibility yang membalik nilainya melalui setState.
  14. Pada metode build milik _BalancedCardWidgetState, dikembalikan Card dengan elevation 4, RoundedRectangleBorder BorderRadius.circular(16), dan color Colors.amber[400], berisi Padding 20 piksel yang membungkus Column rata kiri.
  15. Elemen pertama Column tersebut berupa Row dengan MainAxisAlignment.spaceBetween yang berisi Text "Saldo Utama" berwarna Colors.black54 dan IconButton. Ikon pada IconButton ditentukan melalui ternary operator, yaitu Icons.visibility bila _isBalanceVisible bernilai true dan Icons.visibility_off bila false, dengan onPressed diisi _toggleVisibility.
  16. Setelah SizedBox setinggi 8 piksel, ditampilkan Text berukuran 28, bercetak tebal, dan berwarna hitam. Isinya berupa "Rp " diikuti hasil formatRupiah(widget.balance) bila _isBalanceVisible bernilai true, atau deretan tanda bintang bila saldo disembunyikan.
  17. Kelas ActionButtonsWidget dari Praktikum 2 dideklarasikan sebagai StatelessWidget. Pada metode build, dikembalikan Row dengan MainAxisAlignment.spaceEvenly berisi tiga pemanggilan _buildActionButton, yaitu Icons.arrow_downward berlabel "Pemasukan" berwarna Colors.green, Icons.arrow_upward berlabel "Pengeluaran" berwarna Colors.red, dan Icons.swap_horiz berlabel "Transfer" berwarna Colors.amber.
  18. Fungsi _buildActionButton dideklarasikan dengan parameter icon bertipe IconData, label bertipe String, dan color bertipe Color. Di dalamnya, dikembalikan Column berisi Container dengan padding 12 piksel dan BoxDecoration berwarna color.withOpacity(0.2) serta BorderRadius.circular(12).
  19. Child dari Container diisi Icon sesuai parameter icon dan color dengan ukuran 28, lalu diikuti SizedBox setinggi 8 piksel dan Text berisi label dengan FontWeight.w500. Ketiga tombol tidak diberi callback, sehingga hanya berfungsi sebagai elemen tampilan.
  20. Kelas TransactionListWidget diturunkan dari RecentTransactionWidget Praktikum 2 dan dideklarasikan sebagai StatelessWidget dengan field transactions bertipe List<TransactionModel>. Pada metode build, dikembalikan Text "Belum ada transaksi" berwarna Colors.grey bila transactions.isEmpty bernilai true.
  21. Bila List tidak kosong, dikembalikan Card dengan elevation 2 dan RoundedRectangleBorder BorderRadius.circular(12), yang child-nya berupa Column dengan children hasil transactions.map(...).toList().
  22. Di dalam fungsi map, dideklarasikan variabel amount bertipe String berisi tanda "+" atau "-" sesuai isIncome, diikuti "Rp " dan hasil formatRupiah(transaction.amount), lalu dikembalikan sebuah ListTile untuk setiap objek transaction.
  23. Pada ListTile, bagian leading diisi CircleAvatar berwarna Colors.green atau Colors.redAccent dengan Icon Icons.arrow_downward atau Icons.arrow_upward sesuai isIncome. Bagian title diisi transaction.title, subtitle diisi transaction.date, dan trailing diisi amount bercetak tebal dengan warna hijau atau merah.
  24. Pada callback onTap setiap ListTile, dipanggil Navigator.push dengan MaterialPageRoute yang builder-nya mengembalikan TransactionDetailScreen dengan parameter title, amount, category, dan date dari objek transaction.
v
v
v
v
v
v
  1. Output Kode Program:
v
v
v

H. Form Penambahan Transaksi

  1. Kelas AddTransactionScreen dari tugas Praktikum 3 dideklarasikan sebagai StatefulWidget, dan objek state _AddTransactionScreenState dibentuk melalui metode createState.
  2. Pada kelas _AddTransactionScreenState, dideklarasikan _formKey bertipe GlobalKey<FormState>, serta _titleController, _amountController, dan _dateController bertipe TextEditingController.
  3. Untuk dropdown, dideklarasikan _selectedCategory bertipe String dengan nilai awal "Makanan" dan _categories bertipe List<String> berisi "Makanan", "Transportasi", "Hiburan", "Lainnya", serta "Pemasukan" yang ditambahkan untuk transaksi pemasukan. Selain itu, dideklarasikan _selectedDate bertipe DateTime?.
  4. Fungsi _pilihTanggal bertipe Future<void> dideklarasikan async. Di dalamnya, dipanggil await showDatePicker dengan initialDate berupa _selectedDate ?? DateTime.now(), firstDate DateTime(2000), dan lastDate DateTime(2100), lalu hasilnya disimpan pada variabel pilih bertipe DateTime?.
  5. Bila pilih tidak bernilai null, _selectedDate diisi pilih, tanggal dan bulan diubah menjadi dua digit melalui toString().padLeft(2, '0'), lalu _dateController.text diisi dengan format DD/MM/YYYY.
  6. Pada metode dispose, ketiga controller dilepaskan melalui dispose() sebelum super.dispose() dipanggil, untuk mencegah memory leak.
  7. Pada metode build, dikembalikan Scaffold dengan AppBar yang title-nya diisi "Catat Transaksi Baru", backgroundColor diisi Colors.amber, dan foregroundColor diisi Colors.black. Bagian body diisi SingleChildScrollView dengan padding 16 piksel yang membungkus Form ber-key _formKey dan Column dengan CrossAxisAlignment.stretch.
  8. TextFormField "Judul Transaksi" dihubungkan ke _titleController dengan OutlineInputBorder dan prefixIcon Icons.title, sedangkan validator-nya mengembalikan "Judul transaksi tidak boleh kosong" bila value bernilai null atau kosong.
  9. TextFormField "Nominal (Rp)" dihubungkan ke _amountController dengan keyboardType TextInputType.number dan prefixIcon Icons.attach_money. Validator-nya mengembalikan "Nominal wajib diisi" bila kosong dan "Harus berupa angka bulat yang valid" bila int.tryParse(value) bernilai null.
  10. DropdownButtonFormField<String> diberi value _selectedCategory dan InputDecoration berlabel "Kategori" dengan prefixIcon Icons.category. Properti items diisi hasil _categories.map(...).toList() yang membentuk DropdownMenuItem<String> dengan value dan child Text berisi nama kategori.
  11. Pada callback onChanged milik DropdownButtonFormField, diterima newValue bertipe String?, lalu dipanggil setState untuk mengisi _selectedCategory dengan newValue!.
  12. TextFormField "Tanggal Transaksi" dihubungkan ke _dateController dengan keyboardType TextInputType.number, prefixIcon Icons.calendar_today, dan suffixIcon berupa IconButton dengan Icon Icons.add yang onPressed-nya diisi _pilihTanggal.
  13. Validator pada TextFormField tanggal mengembalikan "Tanggal wajib diisi" bila kosong dan "Format tanggal harus DD/MM/YYYY" bila value tidak cocok dengan pola RegExp dua digit, dua digit, dan empat digit yang dipisahkan garis miring, yang diperiksa melalui hasMatch.
  14. Seluruh kolom isian dipisahkan oleh SizedBox setinggi 16 piksel, lalu setelah SizedBox setinggi 32 piksel, ditambahkan ElevatedButton dengan ElevatedButton.styleFrom berwarna latar Colors.amber[400], foregroundColor Colors.black, dan padding vertikal 16, serta child Text "Simpan Transaksi" berukuran 16 dan bercetak tebal.
  15. Callback onPressed pada ElevatedButton dideklarasikan async. Bila _formKey.currentState!.validate() bernilai true, userId dibaca dari ModalRoute.of(context)!.settings.arguments dan dikonversi melalui as int, yaitu id pengguna yang dikirim dari FloatingActionButton pada DashboardScreen.
  16. Selanjutnya, dipanggil await insertTransaction dengan objek TransactionModel yang userId-nya diisi userId, title diisi _titleController.text, amount diisi int.parse(_amountController.text), category diisi _selectedCategory, dan date diisi _dateController.text.
  17. Setelah data tersimpan, diperiksa if (!context.mounted) return, lalu ditampilkan SnackBar berwarna Colors.green berisi teks "Tersimpan: " yang diikuti judul dan nominal transaksi. Halaman ditutup melalui Navigator.pop(context), sehingga Future dari pushNamed pada DashboardScreen diselesaikan dan data dimuat ulang.
v
v
v
v
v
  1. Output kode program:
v
v

I. Halaman Riwayat, Profil, dan Detail Transaksi

  1. Kelas HistoryScreen diubah menjadi StatefulWidget dengan field user bertipe UserModel yang diisi melalui constructor, dan objek state _HistoryScreenState dibentuk melalui createState.
  2. Pada kelas _HistoryScreenState, dideklarasikan _transactions bertipe List<TransactionModel> dengan nilai awal kosong. Pada initState, dipanggil _loadTransactions, yang di dalamnya dijalankan await transactions(widget.user.id!), pemeriksaan mounted, dan setState seperti pada DashboardScreen.
  3. Pada metode build, dikembalikan Scaffold dengan AppBar "Riwayat Transaksi" berwarna Colors.amber dan foregroundColor Colors.black. Bagian body diisi SingleChildScrollView dengan padding 16 piksel yang memuat TransactionListWidget dengan seluruh _transactions tanpa take, sehingga widget dari dashboard_screen.dart digunakan kembali.
v
v
  1. Kelas ProfileScreen dideklarasikan sebagai StatelessWidget dengan field user bertipe UserModel. Pada metode build, dikembalikan Scaffold dengan AppBar "Profil" berwarna Colors.amber dan foregroundColor Colors.black, serta body berupa Padding 24 piksel yang membungkus Column dengan CrossAxisAlignment.center.
  2. Children pada Column tersebut berupa CircleAvatar dengan radius 50, backgroundColor Colors.amber, dan Icon Icons.person berukuran 60, kemudian Text user.name berukuran 24 dan bercetak tebal, serta Text user.email berukuran 16 berwarna Colors.grey, masing-masing dipisahkan SizedBox.
  3. Elemen terakhir berupa SizedBox dengan width double.infinity yang membungkus ElevatedButton "Logout" berwarna latar Colors.amber[400] dengan teks hitam dan padding vertikal 16. Pada onPressed-nya, dipanggil Navigator.pushReplacementNamed(context, '/').
v
  1. Kelas TransactionDetailScreen dari Praktikum 4 dideklarasikan sebagai StatelessWidget dengan field final title, amount, category, dan date bertipe String yang diisi melalui constructor dengan kata kunci required.
  2. Pada metode build, dikembalikan Scaffold dengan AppBar "Detail Transaksi" berwarna Colors.amber dan foregroundColor Colors.black. Bagian body diisi Padding 24 piksel yang membungkus Column dengan CrossAxisAlignment.center.
  3. Children pada Column berupa Icon Icons.receipt_long berukuran 80 berwarna Colors.amber, SizedBox setinggi 24 piksel, Text title berukuran 24 dan bercetak tebal, SizedBox setinggi 8 piksel, Text amount berukuran 32, bercetak tebal, dan berwarna Colors.amber, serta Divider dengan height 40 dan thickness 2.
  4. Setelah Divider, dipanggil tiga kali fungsi _buildDetailRow yang dipisahkan SizedBox setinggi 16 piksel, yaitu dengan argumen "Kategori" dan category, "Tanggal" dan date, serta "Status" dan "Berhasil".
  5. Fungsi _buildDetailRow dideklarasikan dengan parameter label dan value bertipe String. Di dalamnya, dikembalikan Row dengan MainAxisAlignment.spaceBetween berisi Text label berukuran 16 berwarna Colors.grey dan Text value berukuran 16 bercetak tebal.
v
  1. Output Kode Program:
v
v
v

J. Pengujian Penyimpanan Data dan Pembuatan APK

  1. Aplikasi dijalankan melalui perintah flutter run, lalu dilakukan register akun, login, dan penambahan beberapa transaksi melalui FloatingActionButton pada DashboardScreen.
  2. Aplikasi ditutup, lalu dijalankan kembali dan login dilakukan dengan akun yang sama. Transaksi dan saldo tetap ditampilkan karena data disimpan secara persisten pada berkas expense_tracker.db, sedangkan onCreate tidak dijalankan ulang karena berkas tersebut sudah ada.
v
  1. Berkas APK dibangun melalui perintah flutter build apk --release dari direktori root proyek, sehingga kode Dart dikompilasi dalam mode release.

  2. Berkas APK yang dihasilkan disimpan pada build/app/outputs/flutter-apk/app-release.apk dengan ukuran 52,4 MB

4. Conclusion

Berdasarkan praktik yang telah dilakukan, dapat disimpulkan bahwa named routes melalui initialRoute, routes, dan Navigator.pushNamed membuat perpindahan antarhalaman lebih terpusat dan rapi dibandingkan pemanggilan halaman secara langsung. BottomNavigationBar pada StatefulWidget MainScreen mengintegrasikan halaman Beranda, Riwayat, dan Profil dalam satu wadah dengan mengganti body sesuai nilai _selectedIndex, sedangkan arguments pada rute memungkinkan data seperti email atau objek UserModel dikirim dari halaman login ke halaman utama. Penyimpanan lokal SQLite melalui sqflite menjadikan data pengguna dan transaksi tetap tersimpan meskipun aplikasi ditutup, dengan tabel yang dibuat melalui onCreate serta data yang dikelola melalui fungsi insert dan query. Penggabungan hasil Praktikum 1 sampai Praktikum 5 dengan fitur register dan login menghasilkan aplikasi Expense Tracker yang utuh dan dapat dibangun menjadi berkas APK. Dengan demikian, pemahaman terhadap navigasi terpusat, pengiriman data antarhalaman, dan penyimpanan lokal menjadi dasar penting dalam pengembangan aplikasi mobile menggunakan Flutter.

References

← Back to Reports