StatelessWidget #

StatelessWidget adalah widget paling sederhana dan paling sering digunakan di Flutter. Ia mendeskripsikan bagian dari UI yang hanya bergantung pada konfigurasi yang diberikan — tidak ada state internal yang berubah. Kesederhanaannya bukan berarti ia terbatas: sebagian besar UI Flutter yang kompleks dibangun dari komposisi StatelessWidget yang kecil-kecil.

Anatomi StatelessWidget #

import 'package:flutter/material.dart';

class KartuProduk extends StatelessWidget {
  // 1. Properties -- semua final, immutable
  final String nama;
  final double harga;
  final String? imageUrl;
  final VoidCallback? onTap;

  // 2. Const constructor -- wajib untuk optimasi
  const KartuProduk({
    super.key,                    // selalu terima Key dari parent
    required this.nama,           // required: wajib diisi
    required this.harga,
    this.imageUrl,                // opsional, nullable
    this.onTap,
  });

  // 3. Build method -- satu-satunya method yang wajib
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (imageUrl != null)
              Image.network(imageUrl!, fit: BoxFit.cover),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    nama,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'Rp ${harga.toStringAsFixed(0)}',
                    style: TextStyle(
                      color: Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Beberapa hal yang diperhatikan dari struktur di atas: semua field dideklarasikan final, constructor menggunakan const, dan build() adalah pure function — tidak ada side effect, tidak ada setState, tidak ada network call.


Kapan Menggunakan StatelessWidget #

StatelessWidget adalah pilihan default. Gunakan StatefulWidget hanya jika benar-benar dibutuhkan.

Gunakan StatelessWidget jika:
  ✓ Widget hanya menampilkan data yang diterima dari parent
  ✓ Tidak ada interaksi yang perlu mengubah tampilan widget itu sendiri
  ✓ Konfigurasi widget tidak berubah setelah dibuat

Gunakan StatefulWidget jika:
  ✗ Widget perlu menyimpan nilai yang berubah (counter, form input)
  ✗ Ada animasi yang dikelola oleh widget ini
  ✗ Widget perlu melakukan inisialisasi async (fetch data, setup)
  ✗ Ada resource yang perlu di-dispose (controller, subscription)

Pola yang sangat umum: StatefulWidget di level tinggi (screen/page) yang mengelola state, dengan StatelessWidget sebagai building block UI yang menerima data dari parent:

// StatefulWidget sebagai "container" state
class ProdukScreen extends StatefulWidget {
  const ProdukScreen({super.key});
  @override
  State<ProdukScreen> createState() => _ProdukScreenState();
}

class _ProdukScreenState extends State<ProdukScreen> {
  List<Produk> _produk = [];
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadProduk();
  }

  Future<void> _loadProduk() async {
    final data = await produkApi.getAll();
    setState(() {
      _produk = data;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return const Center(child: CircularProgressIndicator());

    return ListView.builder(
      itemCount: _produk.length,
      itemBuilder: (context, index) {
        // KartuProduk adalah StatelessWidget -- menerima data dari parent
        return KartuProduk(
          key: ValueKey(_produk[index].id),
          nama: _produk[index].nama,
          harga: _produk[index].harga,
          imageUrl: _produk[index].imageUrl,
        );
      },
    );
  }
}

Const Constructor — Optimasi Paling Mudah #

Gunakan const constructor pada widget sebanyak mungkin, karena ini memungkinkan Flutter untuk short-circuit sebagian besar pekerjaan rebuild.

Bagaimana cara kerjanya? Ketika Flutter merekonsiliasi widget tree, ia memeriksa apakah widget yang baru identik dengan yang lama. Untuk widget const, Flutter langsung tahu widget tersebut identik tanpa perlu memeriksa setiap properti:

// TANPA const: Flutter perlu membandingkan setiap properti
Text('Label')       // buat instance baru setiap rebuild
SizedBox(height: 16)
Icon(Icons.star)

// DENGAN const: Flutter skip rebuild sepenuhnya
const Text('Label')       // instance yang sama selalu
const SizedBox(height: 16)
const Icon(Icons.star)

Aturan const Constructor #

Agar sebuah widget bisa di-construct dengan const, semua kondisi berikut harus terpenuhi:

// BISA const: semua nilai diketahui saat compile time
const Text('Halo Flutter')
const EdgeInsets.all(16)
const Color(0xFF2196F3)
const Duration(milliseconds: 300)

// TIDAK BISA const: ada nilai runtime
const Text(variabelRuntime)     // ERROR -- variabel tidak const
const EdgeInsets.all(padding)   // ERROR -- padding tidak const

// Solusi: const hanya pada bagian yang bisa
Text(
  'Halo $nama',                 // runtime -- tidak bisa const
  style: const TextStyle(       // TextStyle bisa const
    fontSize: 16,
    fontWeight: FontWeight.bold,
  ),
)

Pengaruh const pada Widget Tree #

class ParentWidget extends StatefulWidget {
  @override
  State<ParentWidget> createState() => _ParentState();
}

class _ParentState extends State<ParentWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $_counter'),   // rebuild setiap counter berubah

        const HeavyStaticWidget(),    // TIDAK pernah rebuild!
                                      // Flutter melihat instance const
                                      // yang sama -- skip seluruh subtree

        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('Tambah'),  // child ini juga tidak rebuild
        ),
      ],
    );
  }
}

Menggunakan const untuk widget, TextStyle, Color, dan objek lain kapan pun memungkinkan, membiarkan Flutter melakukan optimasi agresif dengan reusing objek-objek ini, menghemat memori dan CPU cycles.


Widget vs Helper Function — Pilih Widget #

Salah satu kesalahan umum adalah membangun bagian UI sebagai helper function biasa alih-alih sebagai widget terpisah:

// ANTI-PATTERN: helper function
class MyScreen extends StatefulWidget { ... }

class _MyScreenState extends State<MyScreen> {
  int _nilai = 0;

  // Ini adalah fungsi biasa, bukan widget
  Widget _buildHeader() {
    return Container(
      color: Colors.blue,
      child: const Text('Header Statis'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildHeader(),   // dipanggil setiap build() -- selalu rebuild!
        Text('Nilai: $_nilai'),
      ],
    );
  }
}
// POLA YANG BENAR: widget terpisah
class HeaderWidget extends StatelessWidget {
  const HeaderWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: const Text('Header Statis'),
    );
  }
}

class _MyScreenState extends State<MyScreen> {
  int _nilai = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const HeaderWidget(),  // const widget -- tidak pernah rebuild!
        Text('Nilai: $_nilai'),
      ],
    );
  }
}

Ketika mencoba membuat potongan UI yang dapat digunakan ulang, lebih baik menggunakan widget daripada helper method. Jika sebuah fungsi digunakan untuk membangun widget, panggilan State.setState akan memaksa Flutter untuk sepenuhnya membangun ulang widget wrapper yang dikembalikan. Jika Widget digunakan sebagai gantinya, Flutter dapat secara efisien me-render ulang hanya bagian yang benar-benar perlu diperbarui. Bahkan lebih baik, jika widget yang dibuat adalah const, Flutter akan short-circuit sebagian besar pekerjaan rebuild.


Teknik Memecah Widget #

StatelessWidget yang terlalu besar adalah tanda bahwa widget tersebut perlu dipecah. Manfaatnya: setiap bagian bisa di-const secara independen, lebih mudah diuji, dan lebih mudah di-reuse.

// TERLALU BESAR -- satu widget untuk semua
class ProfilScreen extends StatelessWidget {
  final User user;
  final List<Post> posts;
  const ProfilScreen({super.key, required this.user, required this.posts});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 50 baris untuk header
        // 30 baris untuk statistik
        // 40 baris untuk grid foto
      ],
    );
  }
}

// LEBIH BAIK -- dipecah menjadi widget yang fokus
class ProfilScreen extends StatelessWidget {
  final User user;
  final List<Post> posts;
  const ProfilScreen({super.key, required this.user, required this.posts});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ProfilHeader(user: user),      // widget terpisah
        ProfilStatistik(user: user),   // widget terpisah
        ProfilFotoGrid(posts: posts),  // widget terpisah
      ],
    );
  }
}

class ProfilHeader extends StatelessWidget {
  final User user;
  const ProfilHeader({super.key, required this.user});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        CircleAvatar(backgroundImage: NetworkImage(user.fotoUrl)),
        const SizedBox(width: 12),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(user.nama, style: const TextStyle(fontWeight: FontWeight.bold)),
            Text(user.bio ?? ''),
          ],
        ),
      ],
    );
  }
}

Menghindari Pekerjaan Mahal di build() #

Method build di Flutter bisa dipanggil sangat sering, jadi penting untuk menjaganya tetap efisien. Hindari melakukan komputasi berat atau memulai network request di dalam build method.

// SALAH -- komputasi berat di dalam build
@override
Widget build(BuildContext context) {
  // Ini dipanggil setiap rebuild!
  final stats = hitungStatistikKompleks(data);   // berat: O(n²)
  final sorted = [...data]..sort(komparatorKompleks); // copy + sort

  return Column(children: [...]);
}

// BENAR -- precompute di luar build atau di parent
class ProdukList extends StatelessWidget {
  final List<Produk> produk;
  // Sudah diproses sebelum dikirim ke widget
  final Map<String, double> statistik;
  final List<Produk> terurut;

  const ProdukList({
    super.key,
    required this.produk,
    required this.statistik,
    required this.terurut,
  });

  @override
  Widget build(BuildContext context) {
    // build() ringan -- hanya menyusun widget
    return Column(children: [...]);
  }
}

Pola Komposisi Umum di Flutter #

Slot Pattern #

// Widget yang menerima "slot" dari luar
class PageLayout extends StatelessWidget {
  final Widget header;
  final Widget body;
  final Widget? footer;        // opsional
  final List<Widget>? actions; // list slot

  const PageLayout({
    super.key,
    required this.header,
    required this.body,
    this.footer,
    this.actions,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: header,
        actions: actions,
      ),
      body: body,
      bottomNavigationBar: footer,
    );
  }
}

// Penggunaan
PageLayout(
  header: const Text('Beranda'),
  body: const ProdukGrid(),
  footer: const NavBar(),
  actions: [
    IconButton(icon: const Icon(Icons.search), onPressed: _search),
    IconButton(icon: const Icon(Icons.cart), onPressed: _openCart),
  ],
)

Builder Pattern #

Untuk widget yang perlu men-expose context dari subtree-nya:

class ConditionalWrapper extends StatelessWidget {
  final bool condition;
  final Widget Function(BuildContext, Widget) builder;
  final Widget child;

  const ConditionalWrapper({
    super.key,
    required this.condition,
    required this.builder,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    if (!condition) return child;
    return builder(context, child);
  }
}

// Penggunaan
ConditionalWrapper(
  condition: isAdmin,
  builder: (context, child) => Tooltip(
    message: 'Admin only',
    child: child,
  ),
  child: const AdminButton(),
)

Ringkasan #

  • StatelessWidget adalah pilihan default — gunakan StatefulWidget hanya ketika benar-benar membutuhkan state internal yang berubah.
  • Selalu buat const constructor untuk StatelessWidget agar widget bisa di-const saat digunakan, memungkinkan Flutter skip rebuild sepenuhnya.
  • Widget const tidak pernah di-rebuild meskipun parent-nya rebuild — ini adalah optimasi performa yang paling mudah dan paling berdampak di Flutter.
  • Gunakan widget terpisah daripada helper function untuk bagian UI yang bisa di-reuse — Flutter bisa mengoptimasi widget jauh lebih baik dari fungsi biasa.
  • Pecah widget yang terlalu besar menjadi widget-widget yang lebih kecil dan fokus — setiap bagian bisa di-const secara independen.
  • Jangan lakukan komputasi berat di dalam build() — precompute di parent atau di level state management, bukan di dalam widget build method.
  • Pelajari pola slot dan builder untuk membuat widget yang fleksibel dan reusable tanpa mengorbankan type safety.

← Sebelumnya: Widget Overview   Berikutnya: StatefulWidget →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact