Skip to main content
Praktikum 2·
20 September 2026

Widgets, Layouting, dan Styling

REPOSITORY_GITHUB

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

1

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:

dart
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:

dart
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:

dart
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):

dart
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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
2

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:

dart
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:

dart
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:

dart
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):

dart
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

1

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:

dart
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.

dart
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,
  ),
),
2

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:

dart
// 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:

dart
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.