Praktikum ini mengenalkan dasar-dasar pengembangan UI di Flutter, mulai dari komponen paling mendasar hingga penataan layout menjadi sebuah dashboard yang utuh. Diawali dengan pengenalan dua jenis widget inti — StatelessWidget untuk tampilan statis (seperti sapaan pengguna) dan StatefulWidget untuk tampilan dinamis yang bisa berubah lewat setState() (seperti kartu saldo dengan tombol sembunyikan/tampilkan nominal). Dari situ, praktikum dilanjutkan ke penataan layout menggunakan Row (susunan horizontal, misalnya tombol Masuk/Keluar/Transfer), Column (susunan vertikal, misalnya daftar riwayat transaksi), Container (pengaturan padding, margin, dan warna), serta ListTile untuk membuat baris daftar secara instan dengan bagian leading, title, dan trailing. Semua komponen ini akhirnya digabungkan menjadi satu halaman dashboard aplikasi keuangan sederhana (Expense Tracker) yang lengkap, interaktif, dan bisa di-scroll (SingleChildScrollView) — menunjukkan alur belajar dari memahami widget dasar sampai mampu menyusunnya menjadi tampilan aplikasi yang fungsional.
ITujuan Praktikum
Setelah menyelesaikan praktikum ini, mahasiswa diharapkan mampu:
- ✓Memahami kerangka dasar aplikasi Flutter (MaterialApp & Scaffold).
- ✓Membedakan penggunaan StatelessWidget dan StatefulWidget.
- ✓Menerapkan fungsi setState() untuk UI interaktif.
- ✓Menerapkan widget Row untuk deretan tombol aksi.
- ✓Menerapkan widget Column untuk daftar transaksi.
- ✓Mengatur padding, margin, dan warna Container.
IIDasar Teori
Pengenalan Widgets
Stateless Widget
Widget statis yang tampilannya tidak pernah berubah setelah dirender (dibangun) di layar.
• Tidak memiliki data internal yang berubah (State).
• Lebih ringan dan cepat.
• Contoh: Label Teks, Ikon, Gambar statis.
Stateful Widget
Widget dinamis yang tampilannya dapat berubah sebagai respons terhadap interaksi pengguna atau data baru.
• Menyimpan objek data internal (State).
• Dapat merender ulang UI via setState().
• Contoh: Form Input, Checkbox, Animasi.
Layouting & Styling
Row adalah widget yang menyusun elemen anak secara menyamping (kiri ke kanan). Column adalah widget yang menyusun elemen anak secara menurun (atas ke bawah). Container adalah kotak yang dapat diberi margin, padding, dan batas (border).
ListTile
Widget bawaan Material Design untuk membuat baris daftar yang rapi secara instan.
• Leading: Elemen paling kiri (Ikon/Avatar).
• Title: Judul utama baris.
• Trailing: Elemen paling kanan (Nominal/Tombol).
IIILangkah Kerja Praktikum
Pengenalan Widgets
1.1 — Langkah 1: Membuat Widget Sapaan (Stateless)
Header dibuat bersifat statis untuk menampilkan nama pengguna dan ikon avatar. Row digunakan untuk menyusun elemen kiri ke kanan, sedangkan Column digunakan untuk menumpuk teks sapaan.
Kode program GreetingWidget:
class GreetingWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Halo, Budi!"),
Text("Selamat datang kembali"),
],
),
CircleAvatar(child: Icon(Icons.person)),
],
);
}
}1.2 — Langkah 2: Membuat Kartu Saldo (Stateful)
Kartu saldo harus bisa bereaksi saat ikon mata ditekan sehingga wajib menggunakan StatefulWidget. Variabel isObscure menyimpan status sensor, dan variabel balance menyimpan nilai nominal.
Kerangka BalanceCard:
class BalanceCard extends StatefulWidget {
@override
_BalanceCardState createState() => _BalanceCardState();
}
class _BalanceCardState extends State<BalanceCard> {
bool _isObscure = false;
final String _balance = "Rp 5.000.000";
@override
Widget build(BuildContext context) {
// ... Implementasi UI dilanjutkan di langkah 3
}
}1.3 — Langkah 3: Menerapkan Interaktivitas (setState)
setState() digunakan untuk memberi tahu Flutter bahwa ada perubahan data, sehingga UI dirender ulang.
Kode program logika toggle saldo:
return Card(
color: Colors.blueAccent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
// Logika Ternary untuk menyensor teks
_isObscure ? "Rp *********" : _balance,
),
IconButton(
icon: Icon(_isObscure ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
_isObscure = !_isObscure; // Membalik nilai boolean
});
},
)
],
),
);1.4 — Langkah 4: Hasil Akhir - Penggabungan ke Scaffold Utama
GreetingWidget (Stateless) dan BalanceCardWidget (Stateful) digabungkan ke dalam Scaffold utama sehingga sapaan statis tampil di bagian atas dan kartu saldo dapat merespons ketukan pada ikon mata.
Source code lengkap lib/main.dart (Modul I):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
// 1. WIDGET UTAMA
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Expense Tracker',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DashboardScreen(), // Halaman awal aplikasi
);
}
}
// 2. HALAMAN UTAMA (Scaffold)
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Praktikum 1: Widgets'),
backgroundColor: Colors.blue,
),
// Padding untuk memberikan jarak dari tepi layar
body: const Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GreetingWidget(), // Memanggil StatelessWidget
SizedBox(height: 20), // Jarak vertikal
BalanceCardWidget(), // Memanggil StatefulWidget
],
),
),
);
}
}
// 3. STATELESS WIDGET (Sapaan Pengguna)
class GreetingWidget extends StatelessWidget {
const GreetingWidget({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
// Ikon Profil
CircleAvatar(
radius: 24,
backgroundColor: Colors.blueAccent,
child: Icon(Icons.person, size: 30, color: Colors.white),
),
SizedBox(width: 12),
// Teks Sapaan
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Halo, Budi',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Selamat datang kembali!',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
],
);
}
}
// 4. STATEFUL WIDGET (Kartu Saldo dengan Toggle)
class BalanceCardWidget extends StatefulWidget {
const BalanceCardWidget({super.key});
@override
State<BalanceCardWidget> createState() => _BalanceCardWidgetState();
}
class _BalanceCardWidgetState extends State<BalanceCardWidget> {
// Variabel State: Menyimpan status apakah saldo terlihat atau disensor
bool _isBalanceVisible = true;
// Fungsi untuk mengubah state
void _toggleVisibility() {
setState(() {
_isBalanceVisible = !_isBalanceVisible; // Membalik nilai boolean (true <-> false)
});
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.blueAccent,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Baris atas kartu: Teks "Saldo Utama" & Tombol Ikon
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Saldo Utama',
style: TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
// Tombol Mata
IconButton(
icon: Icon(
// Logika if-else singkat (Ternary Operator)
_isBalanceVisible ? Icons.visibility : Icons.visibility_off,
color: Colors.white,
),
onPressed: _toggleVisibility, // Memanggil fungsi ubah state
),
],
),
const SizedBox(height: 8),
// Teks Nominal Saldo
Text(
// Logika if-else untuk menampilkan saldo atau sensor
_isBalanceVisible ? 'Rp 5.000.000' : 'Rp *********',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
);
}
}Layouting & Styling
2.1 — Langkah 1: Menyusun Tombol Aksi (Row)
Row digunakan untuk meletakkan tombol Pemasukan, Pengeluaran, dan Transfer sejajar secara horizontal. mainAxisAlignment mengatur jarak antar elemen, dan nilai spaceEvenly membagi ruang kosong sama rata.
Kode program ActionButtons:
class ActionButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButton(Icons.arrow_downward, "Masuk", Colors.green),
_buildButton(Icons.arrow_upward, "Keluar", Colors.red),
_buildButton(Icons.swap_horiz, "Transfer", Colors.blue),
],
);
}
// _buildButton didefinisikan secara terpisah
}2.2 — Langkah 2: Menyusun Riwayat (Column + ListTile)
Daftar transaksi mengurut ke bawah sehingga wajib menggunakan Column. Teks judul dan daftar ditumpuk menggunakan Column, dengan banyak ListTile berturut-turut.
Kode program daftar riwayat transaksi:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Transaksi Terakhir"),
ListTile(
leading: Icon(Icons.fastfood),
title: Text("Makan Siang"),
trailing: Text("- Rp 50.000"),
),
ListTile(
leading: Icon(Icons.work),
title: Text("Gaji Bulanan"),
trailing: Text("+ Rp 5.000.000"),
),
],
)2.3 — Langkah 3: Integrasi Dashboard (Scrollable)
Komponen dari Praktikum I dan Praktikum II digabungkan, lalu dibungkus dengan SingleChildScrollView agar layar tidak overflow saat daftar transaksi memanjang.
Kode program integrasi dashboard:
return Scaffold(
appBar: AppBar(title: Text('Praktikum 2')),
body: SingleChildScrollView( // Mengaktifkan scroll vertikal
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
GreetingWidget(), // Dari Modul 1
BalanceCardWidget(), // Dari Modul 1
ActionButtonsWidget(), // Komponen Baru Row
RecentTransactionsWidget(), // Komponen Baru Column
],
),
),
),
);2.4 — Langkah 4: Hasil Akhir - Mockup Dashboard
Hasil akhir berupa dashboard lengkap yang menggabungkan sapaan, kartu saldo, tombol aksi (Row), dan daftar transaksi (Column + ListTile) dalam satu tampilan yang bisa di-scroll.
Source code lengkap lib/main.dart (Modul II):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
// 1. WIDGET UTAMA (Root Aplikasi)
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Expense Tracker',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DashboardScreen(),
);
}
}
// 2. HALAMAN UTAMA (Scaffold) - DIPERBARUI
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Praktikum 2: Layouting'),
backgroundColor: Colors.blue,
),
// MENGGUNAKAN SingleChildScrollView agar layar bisa di-scroll
body: const SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GreetingWidget(), // Dari Modul 1
SizedBox(height: 20),
BalanceCardWidget(), // Dari Modul 1
SizedBox(height: 20),
ActionButtonsWidget(), // WIDGET BARU MODUL 2
SizedBox(height: 20),
RecentTransactionsWidget(), // WIDGET BARU MODUL 2
],
),
),
),
);
}
}
// 3. STATELESS WIDGET (Sapaan - Dari Modul 1)
class GreetingWidget extends StatelessWidget {
const GreetingWidget({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.blueAccent,
child: Icon(Icons.person, size: 30, color: Colors.white),
),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Halo, Budi', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text('Selamat datang kembali!', style: TextStyle(fontSize: 14, color: Colors.grey)),
],
),
],
);
}
}
// 4. STATEFUL WIDGET (Kartu Saldo - Dari Modul 1)
class BalanceCardWidget extends StatefulWidget {
const BalanceCardWidget({super.key});
@override
State<BalanceCardWidget> createState() => _BalanceCardWidgetState();
}
class _BalanceCardWidgetState extends State<BalanceCardWidget> {
bool _isBalanceVisible = true;
void _toggleVisibility() {
setState(() {
_isBalanceVisible = !_isBalanceVisible;
});
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
color: Colors.blueAccent,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Saldo Utama', style: TextStyle(fontSize: 16, color: Colors.white70)),
IconButton(
icon: Icon(_isBalanceVisible ? Icons.visibility : Icons.visibility_off, color: Colors.white),
onPressed: _toggleVisibility,
),
],
),
const SizedBox(height: 8),
Text(
_isBalanceVisible ? 'Rp 5.000.000' : 'Rp *********',
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white),
),
],
),
),
);
}
}
// 5. WIDGET TOMBOL AKSI (ROW) - BARU
class ActionButtonsWidget extends StatelessWidget {
const ActionButtonsWidget({super.key});
@override
Widget build(BuildContext context) {
// Menggunakan Row untuk menata tombol secara horizontal
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // Jarak dibagi rata
children: [
_buildActionButton(Icons.arrow_downward, 'Pemasukan', Colors.green),
_buildActionButton(Icons.arrow_upward, 'Pengeluaran', Colors.red),
_buildActionButton(Icons.swap_horiz, 'Transfer', Colors.blue),
],
);
}
// Fungsi pembantu untuk membuat desain tombol agar kode tidak berulang
Widget _buildActionButton(IconData icon, String label, Color color) {
return Column(
children: [
// Container untuk membungkus ikon dengan warna background
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.2), // Warna transparan
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 28),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
],
);
}
}
// 6. WIDGET DAFTAR TRANSAKSI (COLUMN & LISTTILE) - BARU
class RecentTransactionsWidget extends StatelessWidget {
const RecentTransactionsWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Transaksi Terakhir',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
// Menggunakan Card agar daftar transaksi memiliki bayangan/bingkai
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
// Item Transaksi 1
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.redAccent,
child: Icon(Icons.fastfood, color: Colors.white),
),
title: const Text('Makan Siang'),
subtitle: const Text('13 Sep 2026'), // Tanggal statis
trailing: const Text(
'- Rp 50.000',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
),
const Divider(height: 1), // Garis pemisah
// Item Transaksi 2
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.green,
child: Icon(Icons.attach_money, color: Colors.white),
),
title: const Text('Gaji Bulanan'),
subtitle: const Text('01 Sep 2026'),
trailing: const Text(
'+ Rp 5.000.000',
style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
),
),
const Divider(height: 1),
// Item Transaksi 3
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.blueAccent,
child: Icon(Icons.directions_car, color: Colors.white),
),
title: const Text('Isi Bensin'),
subtitle: const Text('10 Sep 2026'),
trailing: const Text(
'- Rp 150.000',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
),
],
),
),
],
);
}
}IVLatihan dan Tugas
Pengenalan Widgets
#1 Tugas 1: Styling - Mengubah Warna Tema BalanceCard
Warna tema BalanceCard diubah dari biru (Colors.blueAccent) menjadi hijau emerald (Colors.teal).
Perubahan kode program:
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.teal, // TUGAS 1: sebelumnya Colors.blueAccent
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
// ... isi Column tetap sama seperti sebelumnya
),
),
);#2 Tugas 2: Modifikasi UI - Menambahkan Nomor Rekening
Teks statis "No. Rekening: 1234-5678" ditambahkan di bagian bawah nominal saldo di dalam kartu.
Text(
_isBalanceVisible ? 'Rp 5.000.000' : 'Rp *********',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'No. Rekening: 1234-5678',
style: TextStyle(
color: Colors.white,
),
),Layouting & Styling
#1 Tugas 1: Styling - Warna Trailing Text Transaksi
Warna trailing text pada ListTile diatur agar transaksi pengeluaran berwarna merah dan transaksi pemasukan berwarna hijau.
Kode program styling trailing text:
// Pengeluaran -> merah
trailing: const Text(
'- Rp 50.000',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
// Pemasukan -> hijau
trailing: const Text(
'+ Rp 5.000.000',
style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
),
// Pengeluaran -> merah
trailing: const Text(
'- Rp 150.000',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),#2 Tugas 2: Tambah Data - Transaksi Fiktif Baru
Dua transaksi fiktif baru ditambahkan ke dalam daftar "Transaksi Terakhir" di bawah kode yang sudah ada.
Kode program penambahan transaksi baru:
const Divider(height: 1),
// Item Transaksi 4 (tambahan)
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.orange,
child: Icon(Icons.shopping_bag, color: Colors.white),
),
title: const Text('Belanja Bulanan'),
subtitle: const Text('05 Sep 2026'),
trailing: const Text(
'- Rp 300.000',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
),
const Divider(height: 1),
// Item Transaksi 5 (tambahan)
ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.teal,
child: Icon(Icons.laptop, color: Colors.white),
),
title: const Text('Freelance Project'),
subtitle: const Text('08 Sep 2026'),
trailing: const Text(
'+ Rp 750.000',
style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
),
),VKesimpulan
Melalui Modul I, dapat dipahami bahwa StatelessWidget digunakan untuk tampilan statis yang tidak memiliki perubahan data internal, sedangkan StatefulWidget digunakan untuk tampilan yang perlu berubah sebagai respons terhadap interaksi pengguna, dengan fungsi setState() sebagai mekanisme untuk memberi tahu Flutter agar merender ulang UI ketika data berubah. Hal ini terlihat pada implementasi kartu saldo yang dapat menampilkan atau menyembunyikan nominal saat ikon mata ditekan.
Melalui Modul II, dapat dipahami bahwa Row digunakan untuk menyusun elemen secara horizontal, Column untuk menyusun elemen secara vertikal, Container untuk mengatur padding, margin, dan warna suatu elemen, serta ListTile untuk membuat baris daftar (leading, title, trailing) secara instan sesuai standar Material Design. Penggunaan SingleChildScrollView juga penting untuk mencegah terjadinya overflow ketika konten pada layar bertambah panjang.
Secara keseluruhan, kedua modul ini saling melengkapi dalam membangun sebuah tampilan dashboard aplikasi keuangan sederhana (Expense Tracker), mulai dari komponen dasar (widget sapaan dan kartu saldo) hingga komponen layouting yang lebih kompleks (tombol aksi dan daftar riwayat transaksi) yang digabungkan menjadi satu halaman yang scrollable dan interaktif.