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