Mobile

Flutter untuk Pemula: Build App Cross-Platform

TOKEN

Panduan lengkap membangun aplikasi mobile cross-platform dengan Flutter β€” dari dasar Dart, widget system, stateless vs stateful, layout, navigasi, formulir, hingga HTTP request ke API

1. Pengenalan Flutter

Flutter adalah framework UI open-source yang dikembangkan oleh Google untuk membangun aplikasi mobile, web, dan desktop dari satu basis kode. Menggunakan bahasa pemrograman Dart, Flutter memungkinkan developer membangun aplikasi yang berjalan native di Android, iOS, Windows, macOS, Linux, dan web β€” hanya dengan satu codebase.

Flutter pertama kali diperkenalkan pada tahun 2017 dan sejak itu telah digunakan oleh perusahaan besar seperti Google, BMW, Alibaba, dan banyak lainnya. Keunggulan utama Flutter adalah hot reload yang memungkinkan perubahan kode langsung terlihat tanpa restart aplikasi, serta performa tinggi yang mendekati native berkat rendering engine sendiri.

Mengapa Memilih Flutter?

Keunggulan Penjelasan
Satu CodebaseTulis sekali, jalankan di Android, iOS, Web, Desktop β€” menghemat waktu dan biaya pengembangan
Hot ReloadPerubahan kode langsung terlihat dalam hitungan milidetik tanpa kehilangan state
Performa NativeDikompilasi ke ARM machine code, menggunakan rendering engine Skia β€” performa mendekati native
Widget KayaRatusan widget Material Design dan Cupertino yang sudah siap pakai
Komunitas BesarEkosistem package pub.dev yang sangat kaya, dokumentasi lengkap
Gratis & Open SourceBSD license, tidak ada biaya lisensi, bebas digunakan untuk proyek komersial
Google SupportDikembangkan dan dipelihara oleh Google, digunakan di produk-produk Google

Flutter vs Alternatif Lain

Aspek Flutter React Native Native (Swift/Kotlin)
BahasaDartJavaScript/TypeScriptSwift / Kotlin
RenderingSkia (custom engine)Native bridgeNative
Performa🟒 Sangat baik🟑 Baik🟒 Terbaik
Hot Reloadβœ… Sangat cepatβœ… Fast Refresh⚠️ Terbatas
UI ConsistencySama di semua platformNative feel per platformNative per platform
Kurva BelajarMudah-SedangMudah (jika tahu React)Sedang-Sulit
Diagram: Arsitektur Flutter
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    YOUR DART CODE                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Widgets (Material / Cupertino / Custom)         β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                          β–Ό                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Flutter Framework                               β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚   β”‚
β”‚  β”‚  β”‚Materialβ”‚ β”‚Rendering β”‚ β”‚  Anim  β”‚ β”‚Gesture β”‚ β”‚   β”‚
β”‚  β”‚  β”‚  Lib   β”‚ β”‚  Engine  β”‚ β”‚ Engine β”‚ β”‚ Detect β”‚ β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                      β–Ό                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Flutter Engine (C++) β€” Skia + Dart VM           β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                          β–Ό                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Platform Embedder (Android / iOS / Web / Desk)  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Dasar Pemrograman Dart

Sebelum mulai dengan Flutter, kita perlu memahami dasar-dasar Dart β€” bahasa pemrograman yang digunakan oleh Flutter. Dart dikembangkan oleh Google dan memiliki sintaks yang familiar bagi yang pernah menggunakan Java, JavaScript, atau C#.

Instalasi Flutter SDK

Bash
# 1. Download Flutter SDK dari https://flutter.dev
# 2. Extract dan tambahkan ke PATH

# Linux / macOS:
export PATH="$PATH:/path/to/flutter/bin"

# Windows (PowerShell):
# [Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\flutter\bin", "User")

# Verifikasi instalasi
flutter doctor

# Output:
# Doctor summary (to see all details, run flutter doctor -v):
# [βœ“] Flutter (Channel stable, 3.22.0)
# [βœ“] Android toolchain
# [βœ“] Chrome (web)
# [βœ“] Android Studio
# [βœ“] VS Code

Instalasi Flutter SDK & Buat Proyek Baru

Bash
# Buat proyek Flutter baru
flutter create my_flutter_app

# Masuk ke direktori proyek
cd my_flutter_app

# Jalankan di emulator / device
flutter run

# Jalankan di Chrome (web)
flutter run -d chrome

# Jalankan di device Android yang terhubung
flutter run -d <device_id>

# Cek device yang tersedia
flutter devices

Variabel & Tipe Data Dart

Dart
// ===== VARIABEL & TIPE DATA =====

// Deklarasi variabel
String nama = 'BeebaneLabs';
int umur = 25;
double tinggi = 175.5;
bool isActive = true;

// var β€” tipe di-inferensi otomatis
var kota = 'Jakarta';        // String
var populasi = 10000000;      // int

// final β€” nilai tidak bisa diubah setelah di-set (runtime)
final tanggalSekarang = DateTime.now();

// const β€” nilai compile-time constant
const pi = 3.14159;
const appName = 'BeebaneLabs App';

// Nullable (Dart null safety)
String? alamatOpsional = null;  // Bisa null
int? nomorRumah;                // Default null

// Collection
List<String> buah = ['Apel', 'Mangga', 'Jeruk'];
Map<String, int> harga = {'Apel': 5000, 'Mangga': 8000};
Set<int> angkaUnik = {1, 2, 3, 4, 5};

// String interpolation
print('Halo, saya $nama berumur $umur tahun');
print('Total: ${buah.length} buah');

Function & Arrow Syntax

Dart
// Function biasa
int tambah(int a, int b) {
  return a + b;
}

// Arrow function (untuk satu expression)
int kali(int a, int b) => a * b;

// Named parameters
String sapa({required String nama, String pesan = 'Halo'}) {
  return '$pesan, $nama!';
}
print(sapa(nama: 'Flutter'));  // "Halo, Flutter!"

// Optional positional parameters
double hitungLuas(double panjang, [double lebar = 0]) {
  return panjang * lebar;
}

// Async function
Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://api.example.com'));
  return response.body;
}

// Lambda / Anonymous function
var list = [1, 2, 3, 4, 5];
var genap = list.where((angka) => angka % 2 == 0).toList();
print(genap);  // [2, 4]

Class & Object-Oriented Programming

Dart
// Class dasar
class Mahasiswa {
  String nama;
  int umur;
  String jurusan;

  // Constructor
  Mahasiswa({
    required this.nama,
    required this.umur,
    this.jurusan = 'Teknik Informatika',
  });

  // Named constructor
  Mahasiswa.angkatanBaru(this.nama)
      : umur = 18,
        jurusan = 'Belum ditentukan';

  // Method
  void perkenalkan() {
    print('Halo, saya $nama, umur $umur, jurusan $jurusan');
  }

  // Getter
  String get info => '$nama ($umur tahun)';

  // Override toString
  @override
  String toString() => 'Mahasiswa($nama, $umur)';
}

// Inheritance
class MahasiswaS2 extends Mahasiswa {
  String topikRiset;

  MahasiswaS2({
    required String nama,
    required this.topikRiset,
  }) : super(nama: nama, umur: 24, jurusan: 'S2');
}

// Penggunaan
void main() {
  var mhs = Mahasiswa(nama: 'Budi', umur: 20);
  mhs.perkenalkan();

  var mhs2 = Mahasiswa.angkatanBaru('Ani');
  print(mhs2.info);
}
πŸ’‘ Tips Dart Null Safety

Dart menggunakan sound null safety sejak versi 2.12+. Semua variabel secara default tidak bisa null. Untuk membuat variabel yang bisa null, gunakan tanda ? (misalnya String?). Gunakan operator ?? (default value), ?. (safe access), dan ! (force unwrap) untuk menangani nullable values.

3. Sistem Widget Flutter

Di Flutter, semuanya adalah widget. Widget adalah blok pembangun UI yang mendeskripsikan bagaimana tampilan seharusnya terlihat berdasarkan konfigurasi dan state saat ini. Widget bisa berupa tombol, teks, gambar, layout, atau bahkan seluruh halaman.

Flutter menggunakan pendekatan declarative UI β€” Anda mendeskripsikan UI berdasarkan state saat ini, dan framework yang akan membangun dan memperbarui UI secara efisien. Ini berbeda dengan pendekatan imperative di mana Anda secara manual mengubah properti widget.

Widget Dasar yang Sering Digunakan

Dart β€” Widget Dasar
import 'package:flutter/material.dart';

// ===== WIDGET-WIDGET DASAR FLUTTER =====

// Text β€” Menampilkan teks
Text(
  'Halo, Flutter!',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

// Container β€” Box model dengan dekorasi
Container(
  width: 200,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue.shade100,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(color: Colors.black26, blurRadius: 8),
    ],
  ),
  child: Text('Ini Container'),
)

// Icon β€” Menampilkan ikon Material
Icon(Icons.home, size: 32, color: Colors.green)

// Image β€” Menampilkan gambar
Image.network('https://picsum.photos/200')
Image.asset('assets/images/logo.png')

// ElevatedButton β€” Tombol dengan elevasi
ElevatedButton(
  onPressed: () => print('Ditekan!'),
  child: Text('Tekan Saya'),
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    foregroundColor: Colors.white,
    padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  ),
)

Composite Widget

Dart β€” Composite Widgets
// Card β€” Material Design card
Card(
  elevation: 4,
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Column(
      children: [
        Text('Judul Card', style: TextStyle(fontSize: 18)),
        SizedBox(height: 8),
        Text('Deskripsi card dengan detail informasi...'),
      ],
    ),
  ),
)

// ListTile β€” Item list standar Material
ListTile(
  leading: CircleAvatar(child: Icon(Icons.person)),
  title: Text('Nama Pengguna'),
  subtitle: Text('email@example.com'),
  trailing: Icon(Icons.arrow_forward_ios),
  onTap: () => print('Item diketuk'),
)

// Scaffold β€” Struktur dasar halaman Material
Scaffold(
  appBar: AppBar(title: Text('Halaman Utama')),
  body: Center(child: Text('Konten di sini')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: Icon(Icons.add),
  ),
  bottomNavigationBar: BottomNavigationBar(
    items: [
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
      BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Cari'),
      BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profil'),
    ],
  ),
)
πŸ“– Widget Tree

Flutter membangun UI sebagai tree of widgets. Setiap widget bisa memiliki child atau children. Flutter memiliki 3 fase rendering: Build (membuat widget tree), Layout (menghitung posisi dan ukuran), dan Paint (menggambar ke layar). Optimasi dilakukan dengan hanya me-render ulang widget yang berubah.

4. Stateless vs Stateful Widget

Flutter memiliki dua jenis widget utama: StatelessWidget dan StatefulWidget. Pemahaman kapan menggunakan masing-masing sangat krusial untuk membangun aplikasi Flutter yang efisien.

StatelessWidget

Widget yang tidak memiliki internal state. Setelah dibuat, propertinya tidak berubah. Cocok untuk elemen UI yang statis seperti label teks, ikon, atau kartu informasi.

Dart β€” StatelessWidget
import 'package:flutter/material.dart';

class ProfileCard extends StatelessWidget {
  final String nama;
  final String email;
  final String avatarUrl;

  // Constructor dengan named parameters
  const ProfileCard({
    Key? key,
    required this.nama,
    required this.email,
    required this.avatarUrl,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.all(16),
      elevation: 4,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              radius: 30,
              backgroundImage: NetworkImage(avatarUrl),
            ),
            SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    nama,
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 4),
                  Text(
                    email,
                    style: TextStyle(color: Colors.grey.shade600),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// Penggunaan:
// ProfileCard(
//   nama: 'Budi Santoso',
//   email: 'budi@email.com',
//   avatarUrl: 'https://i.pravatar.cc/150',
// )

StatefulWidget

Widget yang memiliki state yang bisa berubah selama lifecycle-nya. Cocok untuk counter, form input, animasi, atau data yang berubah berdasarkan interaksi user.

Dart β€” StatefulWidget
import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key}) : super(key: key);

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;
  bool _isLoading = false;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  void _decrement() {
    setState(() {
      if (_count > 0) _count--;
    });
  }

  void _reset() {
    setState(() {
      _count = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'Counter',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 16),
            Text(
              '$_count',
              style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold,
                  color: Colors.blue),
            ),
            SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  onPressed: _decrement,
                  icon: Icon(Icons.remove_circle_outline),
                  iconSize: 36,
                ),
                SizedBox(width: 16),
                ElevatedButton(
                  onPressed: _reset,
                  child: Text('Reset'),
                ),
                SizedBox(width: 16),
                IconButton(
                  onPressed: _increment,
                  icon: Icon(Icons.add_circle_outline),
                  iconSize: 36,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
⚠️ Kapan Menggunakan setState?

setState() cocok untuk state lokal yang sederhana. Untuk state yang kompleks atau perlu dibagikan antar banyak widget, pertimbangkan state management solution seperti Provider, Riverpod, atau Bloc. Jangan panggil setState() setelah widget di-dispose β€” cek dengan mounted property.

Lifecycle StatefulWidget

Method Kapan Dipanggil Kegunaan
createState()Pertama kali widget dibuatMembuat State object
initState()Sekali setelah State dibuatInisialisasi data, listener, controller
build()Setiap setState() dipanggilMem-build widget tree
didUpdateWidget()Saat parent rebuild dengan config baruMerespon perubahan properti
dispose()Saat widget dihapus dari treeCleanup: controller, stream, listener

5. Layouts & Responsive Design

Flutter menyediakan berbagai widget layout untuk menyusun widget-child secara fleksibel. Tiga layout utama adalah Row (horizontal), Column (vertikal), dan Stack (tumpukan).

Diagram: Layout Widgets
  Row (β†’)              Column (↓)           Stack (↑)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Child1 Child2   β”‚  β”‚Child1  β”‚          β”‚ Child3 β”‚ ← paling atas
β”‚                  β”‚  β”‚Child2  β”‚          β”‚ Child2 β”‚
β”‚                  β”‚  β”‚Child3  β”‚          β”‚ Child1 β”‚ ← paling bawah
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  ListView (↓ scroll)  GridView (⊞ grid)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Item 1           β”‚  β”‚  β”Œβ”€β”€β” β”Œβ”€β”€β”      β”‚
β”‚ Item 2           β”‚  β”‚  β”‚  β”‚ β”‚  β”‚      β”‚
β”‚ Item 3           β”‚  β”‚  β””β”€β”€β”˜ β””β”€β”€β”˜      β”‚
β”‚ ...              β”‚  β”‚  β”Œβ”€β”€β” β”Œβ”€β”€β”      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  β”‚ β”‚  β”‚      β”‚
                      β”‚  β””β”€β”€β”˜ β””β”€β”€β”˜      β”‚
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Row & Column

Dart β€” Row & Column
// Row β€” Layout horizontal
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Icon(Icons.star, color: Colors.yellow, size: 40),
    Icon(Icons.star, color: Colors.yellow, size: 40),
    Icon(Icons.star, color: Colors.yellow, size: 40),
  ],
)

// Column β€” Layout vertikal
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [
    Text('Judul', style: TextStyle(fontSize: 24)),
    SizedBox(height: 8),
    Text('Subjudul', style: TextStyle(fontSize: 16)),
    SizedBox(height: 16),
    ElevatedButton(onPressed: () {}, child: Text('Aksi')),
  ],
)

// Expanded & Flexible β€” Mengisi ruang yang tersisa
Row(
  children: [
    Expanded(
      flex: 2,
      child: Container(color: Colors.blue, height: 100),
    ),
    SizedBox(width: 8),
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, height: 100),
    ),
  ],
)

ListView & GridView

Dart β€” ListView & GridView
// ListView β€” Scrollable list
ListView(
  children: [
    ListTile(title: Text('Item 1'), leading: Icon(Icons.star)),
    ListTile(title: Text('Item 2'), leading: Icon(Icons.book)),
    ListTile(title: Text('Item 3'), leading: Icon(Icons.music)),
  ],
)

// ListView.builder β€” Efficient untuk list panjang
ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item ${index + 1}'),
      leading: CircleAvatar(child: Text('${index + 1}')),
    );
  },
)

// GridView β€” Grid layout
GridView.count(
  crossAxisCount: 2,
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  padding: EdgeInsets.all(8),
  children: List.generate(6, (index) {
    return Card(
      child: Center(
        child: Text('Grid ${index + 1}',
            style: TextStyle(fontSize: 18)),
      ),
    );
  }),
)
πŸ’‘ MainAxisAlignment vs CrossAxisAlignment

MainAxisAlignment mengatur posisi sepanjang sumbu utama (Row β†’ horizontal, Column β†’ vertikal). CrossAxisAlignment mengatur posisi sepanjang sumbu silang. Nilai umum: start, center, end, spaceBetween, spaceEvenly, spaceAround.

Flutter menggunakan sistem Navigator berbasis stack untuk berpindah antar halaman (route). Setiap halaman di-push ke stack dan di-pop saat kembali.

Navigator Dasar

Dart β€” Navigasi Dasar
import 'package:flutter/material.dart';

// ===== HALAMAN UTAMA =====
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Beranda')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // Push halaman baru ke stack
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => DetailPage(
                      itemId: 42,
                      itemName: 'Produk Flutter',
                    ),
                  ),
                );
              },
              child: Text('Buka Detail'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // Push dan ganti halaman (tidak bisa kembali)
                Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(builder: (context) => LoginPage()),
                );
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

// ===== HALAMAN DETAIL (menerima data) =====
class DetailPage extends StatelessWidget {
  final int itemId;
  final String itemName;

  const DetailPage({required this.itemId, required this.itemName});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(itemName)),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            Text('ID: $itemId', style: TextStyle(fontSize: 20)),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                // Kembali dengan mengirim data
                Navigator.pop(context, 'Data dari detail');
              },
              child: Text('Kembali'),
            ),
          ],
        ),
      ),
    );
  }
}

Named Routes

Dart β€” Named Routes di MaterialApp
// Definisi named routes di MaterialApp
MaterialApp(
  title: 'Aplikasi Flutter',
  initialRoute: '/',
  routes: {
    '/': (context) => HomePage(),
    '/detail': (context) => DetailPage(itemId: 0, itemName: ''),
    '/login': (context) => LoginPage(),
    '/profile': (context) => ProfilePage(),
  },
)

// Navigasi menggunakan named route
Navigator.pushNamed(context, '/profile');

// Mengirim arguments
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {'id': 42, 'name': 'Produk A'},
);

// Menerima arguments di halaman tujuan
final args = ModalRoute.of(context)!.settings.arguments as Map;

7. Formulir & Input Handling

Flutter menyediakan widget Form dan TextFormField untuk menangani input pengguna dengan validasi built-in.

Dart β€” Formulir dengan Validasi
import 'package:flutter/material.dart';

class RegistrationForm extends StatefulWidget {
  @override
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  final _formKey = GlobalKey<FormState>();
  final _namaController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;
  String? _selectedGender;

  @override
  void dispose() {
    _namaController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      // Form valid β€” proses data
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Registrasi berhasil!')),
      );
      print('Nama: ${_namaController.text}');
      print('Email: ${_emailController.text}');
      print('Gender: $_selectedGender');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Registrasi')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Nama
              TextFormField(
                controller: _namaController,
                decoration: InputDecoration(
                  labelText: 'Nama Lengkap',
                  prefixIcon: Icon(Icons.person),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Nama tidak boleh kosong';
                  }
                  if (value.length < 3) {
                    return 'Nama minimal 3 karakter';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),

              // Email
              TextFormField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Email tidak boleh kosong';
                  }
                  if (!value.contains('@')) {
                    return 'Email tidak valid';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),

              // Password
              TextFormField(
                controller: _passwordController,
                obscureText: _obscurePassword,
                decoration: InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock),
                  suffixIcon: IconButton(
                    icon: Icon(_obscurePassword
                        ? Icons.visibility_off
                        : Icons.visibility),
                    onPressed: () {
                      setState(() {
                        _obscurePassword = !_obscurePassword;
                      });
                    },
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password tidak boleh kosong';
                  }
                  if (value.length < 8) {
                    return 'Password minimal 8 karakter';
                  }
                  return null;
                },
              ),
              SizedBox(height: 16),

              // Dropdown
              DropdownButtonFormField<String>(
                value: _selectedGender,
                decoration: InputDecoration(
                  labelText: 'Jenis Kelamin',
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                items: ['Laki-laki', 'Perempuan'].map((g) {
                  return DropdownMenuItem(value: g, child: Text(g));
                }).toList(),
                onChanged: (value) {
                  setState(() => _selectedGender = value);
                },
                validator: (value) =>
                    value == null ? 'Pilih jenis kelamin' : null,
              ),
              SizedBox(height: 24),

              // Submit Button
              ElevatedButton(
                onPressed: _submitForm,
                style: ElevatedButton.styleFrom(
                  padding: EdgeInsets.symmetric(vertical: 16),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: Text('Daftar', style: TextStyle(fontSize: 16)),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

8. HTTP Requests & API

Untuk berkomunikasi dengan REST API, Flutter menggunakan package http dari pub.dev. Kita akan memanggil API publik dan menampilkan data dalam bentuk list.

Instalasi Package HTTP

Bash
# Tambahkan dependency http
flutter pub add http

# Atau manual di pubspec.yaml:
# dependencies:
#   http: ^1.2.0

# Lalu jalankan:
flutter pub get

Model & Service API

Dart β€” Model & API Service
import 'dart:convert';
import 'package:http/http.dart' as http;

// ===== MODEL =====
class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

// ===== API SERVICE =====
class ApiService {
  static const String baseUrl = 'https://jsonplaceholder.typicode.com';

  // GET β€” Ambil semua post
  static Future<List<Post>> getPosts() async {
    final response = await http.get(Uri.parse('$baseUrl/posts'));

    if (response.statusCode == 200) {
      List data = json.decode(response.body);
      return data.map((json) => Post.fromJson(json)).toList();
    } else {
      throw Exception('Gagal memuat data');
    }
  }

  // GET β€” Ambil satu post
  static Future<Post> getPost(int id) async {
    final response = await http.get(Uri.parse('$baseUrl/posts/$id'));

    if (response.statusCode == 200) {
      return Post.fromJson(json.decode(response.body));
    } else {
      throw Exception('Post tidak ditemukan');
    }
  }

  // POST β€” Buat post baru
  static Future<Post> createPost(String title, String body) async {
    final response = await http.post(
      Uri.parse('$baseUrl/posts'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'title': title,
        'body': body,
        'userId': 1,
      }),
    );

    if (response.statusCode == 201) {
      return Post.fromJson(json.decode(response.body));
    } else {
      throw Exception('Gagal membuat post');
    }
  }
}

Menampilkan Data dari API

Dart β€” PostsPage dengan FutureBuilder
class PostsPage extends StatefulWidget {
  @override
  State<PostsPage> createState() => _PostsPageState();
}

class _PostsPageState extends State<PostsPage> {
  late Future<List<Post>> _futurePosts;

  @override
  void initState() {
    super.initState();
    _futurePosts = ApiService.getPosts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts dari API')),
      body: FutureBuilder<List<Post>>(
        future: _futurePosts,
        builder: (context, snapshot) {
          // Loading state
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          // Error state
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error_outline, size: 64, color: Colors.red),
                  SizedBox(height: 16),
                  Text('Error: ${snapshot.error}'),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () {
                      setState(() {
                        _futurePosts = ApiService.getPosts();
                      });
                    },
                    child: Text('Coba Lagi'),
                  ),
                ],
              ),
            );
          }

          // Success state
          final posts = snapshot.data!;
          return RefreshIndicator(
            onRefresh: () async {
              setState(() {
                _futurePosts = ApiService.getPosts();
              });
            },
            child: ListView.builder(
              itemCount: posts.length,
              itemBuilder: (context, index) {
                final post = posts[index];
                return Card(
                  margin: EdgeInsets.symmetric(
                      horizontal: 12, vertical: 4),
                  child: ListTile(
                    leading: CircleAvatar(
                      child: Text('${post.id}'),
                    ),
                    title: Text(
                      post.title,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    subtitle: Text(
                      post.body,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    onTap: () {
                      // Buka detail post
                    },
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}
πŸ’‘ Alternatif untuk HTTP

Untuk proyek yang lebih besar, pertimbangkan menggunakan Dio β€” HTTP client yang lebih powerful dengan fitur interceptor, request cancellation, file upload/download, dan transformasi data. Cukup jalankan flutter pub add dio untuk menginstalnya.

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Flutter:

Pertanyaan 1: Bahasa pemrograman apa yang digunakan oleh Flutter?

a) JavaScript
b) Kotlin
c) Dart
d) Swift

Pertanyaan 2: Widget apa yang digunakan untuk layout horizontal di Flutter?

a) Column
b) Row
c) Stack
d) Wrap

Pertanyaan 3: Method apa yang harus dipanggil untuk memperbarui UI pada StatefulWidget?

a) rebuild()
b) updateUI()
c) setState()
d) refresh()

Pertanyaan 4: Apa fungsi dari widget Expanded dalam Row atau Column?

a) Menambah margin pada widget
b) Membuat widget mengisi ruang yang tersisa
c) Mengatur warna latar belakang
d) Menghapus widget dari tree

Pertanyaan 5: Widget apa yang digunakan untuk menangani input formulir dengan validasi di Flutter?

a) TextField
b) Form + TextFormField
c) InputBox
d) TextFormField saja
πŸ” Zoom
100%
🎨 Tema