← Kembali ke Portfolio
Praktikum Aplikasi Mobile

Laporan Praktikum 2
Pengenalan Widgets, Layouting & Styling (Row, Column, Container, ListTile)

Sasya Zamora  ·  2411533014  ·  Informatika – Universitas Andalas  ·  2025/2026

Dosen Pengampu: Nurfiah, S.ST., M.Kom

1 Pendahuluan

Flutter adalah framework open-source dari Google yang digunakan untuk membangun aplikasi mobile, web, dan desktop dari satu basis kode (single codebase) menggunakan bahasa pemrograman Dart. Konsep utama dalam Flutter adalah segala sesuatu di tampilan aplikasi merupakan sebuah widget, mulai dari teks, tombol, hingga layout secara keseluruhan.

Pada praktikum ini, dipelajari dua topik dasar dalam pengembangan aplikasi Flutter. Praktikum 1 membahas pengenalan widget, khususnya perbedaan antara StatelessWidget dan StatefulWidget, serta penerapan fungsi setState() untuk membuat tampilan yang interaktif. Praktikum 2 melanjutkan dengan konsep layouting dan styling menggunakan widget Row, Column, Container, dan ListTile untuk menyusun tampilan aplikasi yang lebih rapi dan terstruktur, dengan studi kasus membangun dashboard sederhana aplikasi keuangan (Expense Tracker).

2 Tujuan Praktikum

Setelah menyelesaikan praktikum ini, mahasiswa diharapkan mampu:

  • Memahami kerangka dasar aplikasi Flutter (MaterialApp dan Scaffold).
  • Membedakan penggunaan StatelessWidget dan StatefulWidget.
  • Menerapkan fungsi setState() untuk membuat UI yang interaktif.
  • Menerapkan widget Row untuk menyusun deretan elemen secara horizontal, seperti deretan tombol aksi.
  • Menerapkan widget Column untuk menyusun elemen secara vertikal, seperti daftar transaksi.
  • Mengatur padding, margin, dan warna pada Container untuk mempercantik tampilan.
  • Menggunakan ListTile untuk membuat baris daftar (list) secara instan dan rapi.
3 Langkah-Langkah Praktikum
1 Praktikum 1 — Pengenalan Widgets (Stateless, Stateful, & Manajemen State Dasar)
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), ), // TUGAS 1: warna diubah dari Colors.blueAccent -> Colors.teal color: Colors.teal, 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, ), ), // TUGAS 2: teks statis tambahan di bawah nominal saldo const SizedBox(height: 4), const Text( 'No. Rekening: 1234-5678', style: TextStyle( fontSize: 12, color: Colors.white70, ), ), ], ), ), ); } }

Penjelasan kode:

  • void main() — Titik masuk (entry point) aplikasi. Fungsi runApp(const MyApp()) memerintahkan Flutter untuk menjalankan widget MyApp sebagai akar (root) dari seluruh aplikasi.
  • class MyApp — Merupakan StatelessWidget karena konfigurasi aplikasi (judul, tema, halaman awal) tidak pernah berubah selama aplikasi berjalan. Di dalamnya terdapat MaterialApp, yaitu widget bawaan Flutter yang menyediakan struktur dasar aplikasi bergaya Material Design. Properti home: const DashboardScreen() menentukan bahwa DashboardScreen adalah halaman yang pertama kali ditampilkan.
  • class DashboardScreen — Menggunakan Scaffold, yaitu kerangka halaman standar yang menyediakan struktur seperti AppBar (bilah judul di atas) dan body (isi halaman). Di dalam body, digunakan Padding agar konten tidak menempel ke tepi layar, lalu Column untuk menyusun GreetingWidget dan BalanceCardWidget secara vertikal, dengan SizedBox(height: 20) sebagai jarak antar keduanya.
  • class GreetingWidget — StatelessWidget yang menampilkan sapaan. Menggunakan Row agar CircleAvatar (ikon profil) dan Column (dua baris teks sapaan) tersusun sejajar dari kiri ke kanan. Karena tidak ada data yang berubah, tidak diperlukan setState() di sini.
  • class BalanceCardWidget — StatefulWidget karena kartu ini punya data yang bisa berubah, yaitu status tampil/sembunyi saldo. Kelas ini dipasangkan dengan _BalanceCardWidgetState yang menyimpan variabel _isBalanceVisible.
    • IconButton pada bagian kanan atas kartu memanggil fungsi _toggleVisibility() setiap kali ditekan.
    • Di dalam _toggleVisibility(), dipanggil setState(() { _isBalanceVisible = !_isBalanceVisible; }). Pemanggilan setState() inilah yang memberi tahu Flutter bahwa data berubah, sehingga method build() dijalankan ulang dan tampilan diperbarui secara otomatis.
    • Teks nominal saldo menggunakan operator ternary _isBalanceVisible ? 'Rp 5.000.000' : 'Rp *********' untuk menampilkan angka asli atau versi tersensor tergantung nilai boolean tersebut.
    • Sebagai tugas tambahan, warna kartu diubah menjadi Colors.teal dan ditambahkan teks statis "No. Rekening: 1234-5678" di bawah nominal saldo.

Hasilnya:

Gambar 1. Hasil Praktikum 1: Widgets
Gambar 1. Tampilan hasil Praktikum 1 — sapaan pengguna dan kartu saldo dengan toggle visibility

Hasil menunjukkan sebuah kartu berwarna (awalnya biru, setelah dimodifikasi menjadi teal) di bawah sapaan "Halo, Budi!". Kartu tersebut menampilkan tulisan "Total Saldo" beserta nominalnya dan sebuah ikon mata di pojok kanan. Saat ikon mata ditekan, nominal saldo berpindah antara ditampilkan penuh ("Rp 5.000.000") dan disensor ("Rp *********"), membuktikan bahwa StatefulWidget dan setState() berhasil membuat tampilan bereaksi terhadap interaksi pengguna, sementara bagian sapaan di atasnya tetap statis (tidak berubah) karena dibangun dengan StatelessWidget.

2 Praktikum 2 — Layouting & Styling (Row, Column, Container, dan ListTile)
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), ), ], ); } } // Model bantuan sederhana untuk satu baris transaksi, // supaya menambah transaksi baru (Tugas 2) tidak perlu menulis ulang ListTile+Divider. class _TransaksiItem { final IconData icon; final Color iconColor; final String title; final String tanggal; final String nominal; final bool isPengeluaran; // true = pengeluaran (merah), false = pemasukan (hijau) const _TransaksiItem({ required this.icon, required this.iconColor, required this.title, required this.tanggal, required this.nominal, required this.isPengeluaran, }); } // 6. WIDGET DAFTAR TRANSAKSI (COLUMN & LISTTILE) - BARU class RecentTransactionsWidget extends StatelessWidget { const RecentTransactionsWidget({super.key}); // TUGAS 2: 2 transaksi fiktif baru ditambahkan di akhir daftar (Belanja Bulanan, Terima Bonus) static const List<_TransaksiItem> _daftarTransaksi = [ _TransaksiItem( icon: Icons.fastfood, iconColor: Colors.redAccent, title: 'Makan Siang', tanggal: '13 Sep 2026', nominal: '- Rp 50.000', isPengeluaran: true, ), _TransaksiItem( icon: Icons.attach_money, iconColor: Colors.green, title: 'Gaji Bulanan', tanggal: '01 Sep 2026', nominal: '+ Rp 5.000.000', isPengeluaran: false, ), _TransaksiItem( icon: Icons.directions_car, iconColor: Colors.blueAccent, title: 'Isi Bensin', tanggal: '10 Sep 2026', nominal: '- Rp 150.000', isPengeluaran: true, ), // ----- Transaksi fiktif baru (Tugas 2) ----- _TransaksiItem( icon: Icons.shopping_cart, iconColor: Colors.orange, title: 'Belanja Bulanan', tanggal: '15 Sep 2026', nominal: '- Rp 300.000', isPengeluaran: true, ), _TransaksiItem( icon: Icons.card_giftcard, iconColor: Colors.green, title: 'Terima Bonus', tanggal: '17 Sep 2026', nominal: '+ Rp 750.000', isPengeluaran: false, ), ]; @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: [ for (int i = 0; i < _daftarTransaksi.length; i++) ...[ ListTile( leading: CircleAvatar( backgroundColor: _daftarTransaksi[i].iconColor, child: Icon(_daftarTransaksi[i].icon, color: Colors.white), ), title: Text(_daftarTransaksi[i].title), subtitle: Text(_daftarTransaksi[i].tanggal), // TUGAS 1: warna merah untuk pengeluaran, hijau untuk pemasukan trailing: Text( _daftarTransaksi[i].nominal, style: TextStyle( color: _daftarTransaksi[i].isPengeluaran ? Colors.red : Colors.green, fontWeight: FontWeight.bold, ), ), ), if (i != _daftarTransaksi.length - 1) const Divider(height: 1), ], ], ), ), ], ); } }

Penjelasan kode:

  • class DashboardScreen (diperbarui) — body kini dibungkus dengan SingleChildScrollView sebelum Padding dan Column. Ini diperlukan karena daftar widget yang ditampilkan bertambah banyak (gabungan Praktikum 1 dan 2), sehingga tanpa SingleChildScrollView, layar akan mengalami overflow (konten terpotong) ketika tinggi total melebihi tinggi layar.
  • class ActionButtonsWidget — StatelessWidget yang menyusun tiga tombol aksi (Pemasukan, Pengeluaran, Transfer) menggunakan Row dengan mainAxisAlignment: MainAxisAlignment.spaceEvenly, sehingga jarak antar tombol maupun jarak ke tepi terbagi sama rata. Setiap tombol dibuat melalui fungsi bantu _buildActionButton() agar kode tidak ditulis berulang untuk tiga tombol berbeda. Di dalamnya, Container digunakan untuk membungkus ikon dengan padding, latar warna transparan (color.withOpacity(0.2)), dan sudut membulat (borderRadius).
  • class RecentTransactionsWidget — StatelessWidget yang menampilkan daftar riwayat transaksi. Judul "Transaksi Terakhir" dan daftar transaksi disusun ke bawah menggunakan Column. Daftar transaksi dibungkus Card agar memiliki bayangan (elevation) dan sudut membulat.
    • Setiap baris transaksi memakai ListTile dengan tiga bagian: leading (ikon dalam CircleAvatar), title (nama transaksi), subtitle (tanggal), dan trailing (nominal transaksi).
    • Antar baris transaksi dipisahkan dengan Divider(height: 1) agar daftar terlihat rapi.
    • Sebagai tugas tambahan, warna teks pada trailing diatur otomatis lewat properti isPengeluaran: merah (Colors.red) jika transaksi adalah pengeluaran, dan hijau (Colors.green) jika pemasukan. Selain itu, ditambahkan dua transaksi fiktif baru ("Belanja Bulanan" dan "Terima Bonus") ke dalam daftar yang sama tanpa perlu menulis ulang ListTile secara manual, karena data transaksi disimpan dalam satu list (_daftarTransaksi) dan ditampilkan menggunakan perulangan for.

Hasilnya:

Gambar 2. Hasil Praktikum 2: Layouting
Gambar 2. Tampilan hasil Praktikum 2 — dashboard lengkap dengan tombol aksi dan daftar transaksi terakhir

Hasil menunjukkan tampilan dashboard yang lebih lengkap: bagian sapaan dan kartu saldo dari Praktikum 1 tetap ada di bagian atas, diikuti oleh tiga tombol aksi (Masuk, Keluar, Transfer) yang tersusun sejajar dengan ikon berwarna berbeda (hijau, merah, biru), dan di bawahnya daftar "Transaksi Terakhir" yang menampilkan setiap transaksi lengkap dengan ikon, judul, tanggal, dan nominal. Nominal pengeluaran (misalnya "Makan Siang", "Isi Bensin", "Belanja Bulanan") tampil berwarna merah dengan tanda minus, sedangkan nominal pemasukan (misalnya "Gaji Bulanan", "Terima Bonus") tampil berwarna hijau dengan tanda plus. Keseluruhan halaman dapat digulir (scroll) tanpa error overflow, membuktikan SingleChildScrollView bekerja dengan baik menampung seluruh komponen dari Praktikum 1 dan 2 dalam satu layar.

4 Kesimpulan

Berdasarkan praktikum yang telah dilakukan, dapat disimpulkan bahwa Flutter menyediakan dua jenis widget utama dengan fungsi yang berbeda, yaitu StatelessWidget untuk tampilan statis dan StatefulWidget untuk tampilan yang dapat berubah secara dinamis melalui fungsi setState(). Selain itu, penyusunan tata letak (layout) pada Flutter sangat bergantung pada kombinasi widget dasar seperti Row untuk penyusunan horizontal, Column untuk penyusunan vertikal, Container untuk pengaturan padding/margin/warna, dan ListTile untuk membuat daftar yang rapi secara instan. Dengan menggabungkan widget-widget tersebut, dapat dibangun tampilan dashboard aplikasi yang informatif, interaktif, dan dapat menyesuaikan diri terhadap perubahan data maupun panjang konten, seperti yang diterapkan pada studi kasus aplikasi Expense Tracker dalam praktikum ini.