Flutter Docs
Flutter Best Practices

Pengantar

Widget composition adalah salah satu konsep paling fundamental dalam Flutter. Berbeda dengan framework UI lain yang mengandalkan inheritance untuk membangun komponen kompleks, Flutter menggunakan pendekatan composition — menggabungkan widget-widget kecil dan sederhana untuk membentuk UI yang kompleks.

Di halaman ini, Anda akan mempelajari prinsip-prinsip komposisi widget, cara memecah widget besar menjadi komponen kecil, kapan menggunakan StatelessWidget vs StatefulWidget, serta berbagai pattern untuk membangun widget yang reusable dan terstruktur.

Prinsip Composition Over Inheritance

Dalam Flutter, semuanya adalah widget. Tombol, teks, padding, bahkan layout — semuanya widget. Flutter mendorong Anda untuk membangun UI dengan menggabungkan (compose) widget-widget kecil, bukan dengan mewarisi (inherit) widget yang sudah ada.

// ❌ Pendekatan inheritance — TIDAK direkomendasikan di Flutter
// Jangan buat class yang extends widget lain untuk menambah fitur
class MySpecialButton extends ElevatedButton {
  // Ini bukan cara Flutter bekerja
}

// ✅ Pendekatan composition — cara Flutter yang benar
// Bungkus widget yang ada dengan widget lain
class PrimaryButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;

  const PrimaryButton({
    super.key,
    required this.label,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Theme.of(context).colorScheme.onPrimary,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
      child: Text(label),
    );
  }
}

Mengapa Composition Lebih Baik?

Composition memberikan beberapa keuntungan dibanding inheritance:

  • Fleksibilitas — Anda bisa menggabungkan widget dalam berbagai cara tanpa batasan hierarki class
  • Reusability — Widget kecil lebih mudah digunakan ulang di berbagai konteks
  • Testability — Widget yang terisolasi lebih mudah diuji secara independen
  • Readability — Kode lebih mudah dibaca karena setiap widget memiliki tanggung jawab yang jelas
// Composition dalam aksi — membangun UI kompleks dari widget sederhana
class UserProfileCard extends StatelessWidget {
  final String name;
  final String email;
  final String avatarUrl;

  const UserProfileCard({
    super.key,
    required this.name,
    required this.email,
    required this.avatarUrl,
  });

  @override
  Widget build(BuildContext context) {
    // Compose beberapa widget sederhana menjadi satu komponen
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              radius: 30,
              backgroundImage: NetworkImage(avatarUrl),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    name,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    email,
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Memecah Widget Besar Menjadi Widget Kecil

Salah satu kesalahan paling umum developer Flutter pemula adalah membuat satu widget raksasa yang menangani semuanya. Widget yang terlalu besar sulit dibaca, sulit di-maintain, dan menyebabkan rebuild yang tidak perlu.

Tanda Widget Terlalu Besar

  • Method build() lebih dari 50-80 baris
  • Widget memiliki banyak tanggung jawab yang tidak terkait
  • Sulit menemukan bagian tertentu dari UI dalam kode
  • Perubahan kecil di satu bagian mempengaruhi bagian lain

Contoh: Sebelum dan Sesudah Refactoring

// ❌ Widget monolitik — semua UI di satu tempat
class ProductDetailPage extends StatelessWidget {
  final Product product;

  const ProductDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product.name)),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Image section — 20+ baris
            SizedBox(
              height: 300,
              width: double.infinity,
              child: Image.network(product.imageUrl, fit: BoxFit.cover),
            ),
            // Info section — 30+ baris
            Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(product.name,
                      style: Theme.of(context).textTheme.headlineSmall),
                  const SizedBox(height: 8),
                  Text('Rp ${product.price}',
                      style: const TextStyle(
                          fontSize: 24, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      const Icon(Icons.star, color: Colors.amber, size: 20),
                      Text(' ${product.rating} (${product.reviewCount} ulasan)'),
                    ],
                  ),
                ],
              ),
            ),
            // Description section — 15+ baris
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Text(product.description),
            ),
            // Action buttons — 20+ baris
            Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.favorite_border),
                      label: const Text('Wishlist'),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    flex: 2,
                    child: ElevatedButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.shopping_cart),
                      label: const Text('Tambah ke Keranjang'),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
// ✅ Setelah refactoring — dipecah menjadi widget-widget kecil
class ProductDetailPage extends StatelessWidget {
  final Product product;

  const ProductDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product.name)),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            ProductImageSection(imageUrl: product.imageUrl),
            ProductInfoSection(product: product),
            ProductDescription(description: product.description),
            ProductActionButtons(
              onAddToWishlist: () {},
              onAddToCart: () {},
            ),
          ],
        ),
      ),
    );
  }
}

// Widget terpisah untuk bagian gambar
class ProductImageSection extends StatelessWidget {
  final String imageUrl;

  const ProductImageSection({super.key, required this.imageUrl});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 300,
      width: double.infinity,
      child: Image.network(imageUrl, fit: BoxFit.cover),
    );
  }
}

// Widget terpisah untuk informasi produk
class ProductInfoSection extends StatelessWidget {
  final Product product;

  const ProductInfoSection({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            product.name,
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 8),
          Text(
            'Rp ${product.price}',
            style: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),
          ProductRating(
            rating: product.rating,
            reviewCount: product.reviewCount,
          ),
        ],
      ),
    );
  }
}

// Widget kecil untuk rating — bisa digunakan ulang di tempat lain
class ProductRating extends StatelessWidget {
  final double rating;
  final int reviewCount;

  const ProductRating({
    super.key,
    required this.rating,
    required this.reviewCount,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Icon(Icons.star, color: Colors.amber, size: 20),
        Text(' $rating ($reviewCount ulasan)'),
      ],
    );
  }
}

// Widget terpisah untuk deskripsi
class ProductDescription extends StatelessWidget {
  final String description;

  const ProductDescription({super.key, required this.description});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Text(description),
    );
  }
}

// Widget terpisah untuk tombol aksi
class ProductActionButtons extends StatelessWidget {
  final VoidCallback onAddToWishlist;
  final VoidCallback onAddToCart;

  const ProductActionButtons({
    super.key,
    required this.onAddToWishlist,
    required this.onAddToCart,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          Expanded(
            child: OutlinedButton.icon(
              onPressed: onAddToWishlist,
              icon: const Icon(Icons.favorite_border),
              label: const Text('Wishlist'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            flex: 2,
            child: ElevatedButton.icon(
              onPressed: onAddToCart,
              icon: const Icon(Icons.shopping_cart),
              label: const Text('Tambah ke Keranjang'),
            ),
          ),
        ],
      ),
    );
  }
}

Perhatikan bagaimana setiap widget sekarang memiliki satu tanggung jawab yang jelas. ProductRating bahkan bisa digunakan ulang di halaman lain seperti daftar produk atau halaman review.

StatelessWidget vs StatefulWidget

Memilih antara StatelessWidget dan StatefulWidget adalah keputusan penting yang mempengaruhi performa dan maintainability kode Anda.

StatelessWidget — Untuk UI yang Tidak Berubah

Gunakan StatelessWidget ketika widget tidak memiliki state internal yang berubah. Widget ini hanya bergantung pada data yang diterima melalui constructor.

// StatelessWidget — output selalu sama untuk input yang sama
class GreetingCard extends StatelessWidget {
  final String name;
  final String message;

  const GreetingCard({
    super.key,
    required this.name,
    required this.message,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Halo, $name!',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(message),
          ],
        ),
      ),
    );
  }
}

StatefulWidget — Untuk UI yang Berubah

Gunakan StatefulWidget ketika widget memiliki state internal yang bisa berubah selama lifetime-nya, misalnya animasi, form input, atau toggle.

// StatefulWidget — memiliki state internal yang berubah
class ExpandableCard extends StatefulWidget {
  final String title;
  final String content;

  const ExpandableCard({
    super.key,
    required this.title,
    required this.content,
  });

  @override
  State<ExpandableCard> createState() => _ExpandableCardState();
}

class _ExpandableCardState extends State<ExpandableCard> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: () {
          setState(() {
            _isExpanded = !_isExpanded;
          });
        },
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    widget.title,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  Icon(
                    _isExpanded
                        ? Icons.keyboard_arrow_up
                        : Icons.keyboard_arrow_down,
                  ),
                ],
              ),
              if (_isExpanded) ...[
                const SizedBox(height: 12),
                Text(widget.content),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

Panduan Memilih

SituasiPilihanAlasan
Menampilkan data statisStatelessWidgetTidak ada state yang berubah
Tombol, label, ikonStatelessWidgetHanya menerima data dari parent
Form inputStatefulWidgetTextEditingController butuh state
AnimasiStatefulWidgetAnimationController butuh state
Toggle, expand/collapseStatefulWidgetState boolean internal
Data dari state managementStatelessWidgetState dikelola di luar widget

Aturan praktis: Mulai dengan StatelessWidget. Ubah ke StatefulWidget hanya ketika Anda benar-benar membutuhkan setState().

Membuat Widget Reusable dengan Parameter

Widget yang reusable adalah widget yang bisa digunakan di berbagai konteks dengan perilaku yang bisa dikustomisasi melalui parameter. Kunci utamanya adalah mendesain API widget (constructor parameters) yang fleksibel namun tetap sederhana.

Contoh: Widget Tombol yang Reusable

import 'package:flutter/material.dart';

enum AppButtonVariant { primary, secondary, outline, text }
enum AppButtonSize { small, medium, large }

class AppButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final AppButtonVariant variant;
  final AppButtonSize size;
  final IconData? icon;
  final bool isLoading;

  const AppButton({
    super.key,
    required this.label,
    this.onPressed,
    this.variant = AppButtonVariant.primary,
    this.size = AppButtonSize.medium,
    this.icon,
    this.isLoading = false,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    // Tentukan padding berdasarkan ukuran
    final padding = switch (size) {
      AppButtonSize.small =>
        const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      AppButtonSize.medium =>
        const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
      AppButtonSize.large =>
        const EdgeInsets.symmetric(horizontal: 28, vertical: 16),
    };

    // Tentukan text style berdasarkan ukuran
    final textStyle = switch (size) {
      AppButtonSize.small => theme.textTheme.labelSmall,
      AppButtonSize.medium => theme.textTheme.labelLarge,
      AppButtonSize.large => theme.textTheme.titleMedium,
    };

    // Bangun child widget (loading indicator atau label + icon)
    final child = isLoading
        ? const SizedBox(
            width: 20,
            height: 20,
            child: CircularProgressIndicator(strokeWidth: 2),
          )
        : Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (icon != null) ...[
                Icon(icon, size: size == AppButtonSize.small ? 16 : 20),
                const SizedBox(width: 8),
              ],
              Text(label, style: textStyle),
            ],
          );

    // Render variant yang sesuai
    return switch (variant) {
      AppButtonVariant.primary => ElevatedButton(
          onPressed: isLoading ? null : onPressed,
          style: ElevatedButton.styleFrom(padding: padding),
          child: child,
        ),
      AppButtonVariant.secondary => FilledButton.tonal(
          onPressed: isLoading ? null : onPressed,
          style: FilledButton.styleFrom(padding: padding),
          child: child,
        ),
      AppButtonVariant.outline => OutlinedButton(
          onPressed: isLoading ? null : onPressed,
          style: OutlinedButton.styleFrom(padding: padding),
          child: child,
        ),
      AppButtonVariant.text => TextButton(
          onPressed: isLoading ? null : onPressed,
          style: TextButton.styleFrom(padding: padding),
          child: child,
        ),
    };
  }
}

// Penggunaan — satu widget, banyak variasi
class ButtonExamples extends StatelessWidget {
  const ButtonExamples({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Tombol utama
        AppButton(
          label: 'Simpan',
          onPressed: () {},
        ),
        // Tombol dengan ikon
        AppButton(
          label: 'Tambah Item',
          icon: Icons.add,
          onPressed: () {},
        ),
        // Tombol outline kecil
        AppButton(
          label: 'Batal',
          variant: AppButtonVariant.outline,
          size: AppButtonSize.small,
          onPressed: () {},
        ),
        // Tombol loading
        const AppButton(
          label: 'Memproses...',
          isLoading: true,
        ),
      ],
    );
  }
}

Contoh: Widget Card yang Fleksibel

class InfoCard extends StatelessWidget {
  final String title;
  final String? subtitle;
  final Widget? leading;
  final Widget? trailing;
  final VoidCallback? onTap;
  final EdgeInsetsGeometry padding;
  final Color? backgroundColor;

  const InfoCard({
    super.key,
    required this.title,
    this.subtitle,
    this.leading,
    this.trailing,
    this.onTap,
    this.padding = const EdgeInsets.all(16),
    this.backgroundColor,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      color: backgroundColor,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: padding,
          child: Row(
            children: [
              if (leading != null) ...[
                leading!,
                const SizedBox(width: 16),
              ],
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    if (subtitle != null) ...[
                      const SizedBox(height: 4),
                      Text(
                        subtitle!,
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                    ],
                  ],
                ),
              ),
              if (trailing != null) trailing!,
            ],
          ),
        ),
      ),
    );
  }
}

// Penggunaan — satu widget, berbagai konteks
class CardExamples extends StatelessWidget {
  const CardExamples({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Card sederhana
        const InfoCard(title: 'Pengaturan Akun'),

        // Card dengan subtitle dan ikon
        InfoCard(
          title: 'Notifikasi',
          subtitle: 'Kelola preferensi notifikasi',
          leading: const Icon(Icons.notifications_outlined),
          trailing: const Icon(Icons.chevron_right),
          onTap: () {},
        ),

        // Card dengan widget kustom
        InfoCard(
          title: 'Mode Gelap',
          subtitle: 'Aktifkan tema gelap',
          leading: const Icon(Icons.dark_mode_outlined),
          trailing: Switch(value: true, onChanged: (_) {}),
        ),
      ],
    );
  }
}

Widget Builder Pattern

Builder pattern memungkinkan Anda membuat widget yang mendelegasikan pembuatan child widget ke pemanggil melalui callback function. Pattern ini sangat berguna ketika widget perlu menyediakan data atau konteks ke child-nya.

Contoh: Generic List Builder

// Widget generic yang bisa menampilkan list apapun
class DataListView<T> extends StatelessWidget {
  final List<T> items;
  final Widget Function(BuildContext context, T item, int index) itemBuilder;
  final Widget? emptyWidget;
  final Widget? separatorBuilder;
  final EdgeInsetsGeometry? padding;

  const DataListView({
    super.key,
    required this.items,
    required this.itemBuilder,
    this.emptyWidget,
    this.separatorBuilder,
    this.padding,
  });

  @override
  Widget build(BuildContext context) {
    if (items.isEmpty) {
      return emptyWidget ??
          const Center(
            child: Text('Tidak ada data'),
          );
    }

    return ListView.separated(
      padding: padding ?? const EdgeInsets.all(16),
      itemCount: items.length,
      separatorBuilder: (_, __) =>
          separatorBuilder ?? const SizedBox(height: 8),
      itemBuilder: (context, index) {
        return itemBuilder(context, items[index], index);
      },
    );
  }
}

// Penggunaan — pemanggil menentukan tampilan setiap item
class UserListPage extends StatelessWidget {
  final List<User> users;

  const UserListPage({super.key, required this.users});

  @override
  Widget build(BuildContext context) {
    return DataListView<User>(
      items: users,
      emptyWidget: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.people_outline, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text('Belum ada pengguna'),
          ],
        ),
      ),
      itemBuilder: (context, user, index) {
        return ListTile(
          leading: CircleAvatar(child: Text(user.name[0])),
          title: Text(user.name),
          subtitle: Text(user.email),
          onTap: () {
            // Navigasi ke detail user
          },
        );
      },
    );
  }
}

Contoh: Async Data Builder

// Widget yang menangani loading, error, dan data states
class AsyncDataBuilder<T> extends StatelessWidget {
  final Future<T> future;
  final Widget Function(BuildContext context, T data) builder;
  final Widget Function(BuildContext context, Object error)? errorBuilder;
  final Widget? loadingWidget;

  const AsyncDataBuilder({
    super.key,
    required this.future,
    required this.builder,
    this.errorBuilder,
    this.loadingWidget,
  });

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<T>(
      future: future,
      builder: (context, snapshot) {
        // State: loading
        if (snapshot.connectionState == ConnectionState.waiting) {
          return loadingWidget ??
              const Center(child: CircularProgressIndicator());
        }

        // State: error
        if (snapshot.hasError) {
          if (errorBuilder != null) {
            return errorBuilder!(context, snapshot.error!);
          }
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.error_outline, size: 48, color: Colors.red),
                const SizedBox(height: 16),
                Text('Error: ${snapshot.error}'),
              ],
            ),
          );
        }

        // State: data tersedia
        return builder(context, snapshot.data as T);
      },
    );
  }
}

// Penggunaan — bersih dan deklaratif
class ProductPage extends StatelessWidget {
  const ProductPage({super.key});

  @override
  Widget build(BuildContext context) {
    return AsyncDataBuilder<List<Product>>(
      future: ProductService().fetchProducts(),
      builder: (context, products) {
        return ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            return ProductCard(product: products[index]);
          },
        );
      },
      errorBuilder: (context, error) {
        return Center(
          child: ElevatedButton(
            onPressed: () {
              // Retry logic
            },
            child: const Text('Coba Lagi'),
          ),
        );
      },
    );
  }
}

Callback Pattern untuk Komunikasi Antar Widget

Ketika widget child perlu berkomunikasi dengan widget parent (misalnya memberitahu bahwa tombol ditekan atau data berubah), gunakan callback pattern — parent meneruskan function ke child melalui constructor.

Contoh: Komunikasi Parent-Child

// Child widget — tidak tahu apa yang terjadi saat item dipilih
// Hanya memanggil callback yang diberikan parent
class CategoryChip extends StatelessWidget {
  final String label;
  final bool isSelected;
  final ValueChanged<String> onSelected;

  const CategoryChip({
    super.key,
    required this.label,
    required this.isSelected,
    required this.onSelected,
  });

  @override
  Widget build(BuildContext context) {
    return FilterChip(
      label: Text(label),
      selected: isSelected,
      onSelected: (_) => onSelected(label),
    );
  }
}

// Parent widget — mengelola state dan menentukan apa yang terjadi
class CategoryFilter extends StatefulWidget {
  final List<String> categories;
  final ValueChanged<String> onCategoryChanged;

  const CategoryFilter({
    super.key,
    required this.categories,
    required this.onCategoryChanged,
  });

  @override
  State<CategoryFilter> createState() => _CategoryFilterState();
}

class _CategoryFilterState extends State<CategoryFilter> {
  String _selectedCategory = '';

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: widget.categories.map((category) {
        return CategoryChip(
          label: category,
          isSelected: _selectedCategory == category,
          onSelected: (selected) {
            setState(() {
              _selectedCategory = selected;
            });
            // Teruskan ke parent di atasnya
            widget.onCategoryChanged(selected);
          },
        );
      }).toList(),
    );
  }
}

// Penggunaan di level yang lebih tinggi
class ShopPage extends StatelessWidget {
  const ShopPage({super.key});

  @override
  Widget build(BuildContext context) {
    return CategoryFilter(
      categories: const ['Semua', 'Elektronik', 'Fashion', 'Makanan'],
      onCategoryChanged: (category) {
        // Filter produk berdasarkan kategori
        debugPrint('Kategori dipilih: $category');
      },
    );
  }
}

Contoh: Form dengan Callback Validasi

// Widget form field reusable dengan callback
class ValidatedTextField extends StatelessWidget {
  final String label;
  final String? errorText;
  final TextEditingController controller;
  final ValueChanged<String>? onChanged;
  final bool obscureText;
  final TextInputType keyboardType;

  const ValidatedTextField({
    super.key,
    required this.label,
    required this.controller,
    this.errorText,
    this.onChanged,
    this.obscureText = false,
    this.keyboardType = TextInputType.text,
  });

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: controller,
      obscureText: obscureText,
      keyboardType: keyboardType,
      onChanged: onChanged,
      decoration: InputDecoration(
        labelText: label,
        errorText: errorText,
        border: const OutlineInputBorder(),
      ),
    );
  }
}

// Parent yang mengelola validasi
class RegistrationForm extends StatefulWidget {
  final void Function(String name, String email) onSubmit;

  const RegistrationForm({super.key, required this.onSubmit});

  @override
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  String? _nameError;
  String? _emailError;

  void _validateAndSubmit() {
    setState(() {
      _nameError =
          _nameController.text.isEmpty ? 'Nama tidak boleh kosong' : null;
      _emailError = !_emailController.text.contains('@')
          ? 'Email tidak valid'
          : null;
    });

    if (_nameError == null && _emailError == null) {
      widget.onSubmit(_nameController.text, _emailController.text);
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ValidatedTextField(
          label: 'Nama',
          controller: _nameController,
          errorText: _nameError,
          onChanged: (_) {
            if (_nameError != null) {
              setState(() => _nameError = null);
            }
          },
        ),
        const SizedBox(height: 16),
        ValidatedTextField(
          label: 'Email',
          controller: _emailController,
          errorText: _emailError,
          keyboardType: TextInputType.emailAddress,
          onChanged: (_) {
            if (_emailError != null) {
              setState(() => _emailError = null);
            }
          },
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: _validateAndSubmit,
          child: const Text('Daftar'),
        ),
      ],
    );
  }
}

Jenis-Jenis Callback yang Umum

// Callback tanpa parameter — untuk aksi sederhana
final VoidCallback? onPressed;
final VoidCallback? onTap;

// Callback dengan satu parameter — untuk mengirim data
final ValueChanged<String> onChanged;    // typedef void ValueChanged<T>(T value)
final ValueChanged<bool> onToggled;

// Callback dengan return value — untuk validasi atau konfirmasi
final FormFieldValidator<String>? validator;  // String? Function(String?)

// Callback kustom — untuk kasus yang lebih kompleks
final void Function(String id, int quantity) onItemUpdated;
final Future<bool> Function() onConfirm;

Best Practices Widget Composition

1. Gunakan Const Constructors

const constructor memungkinkan Flutter mengoptimalkan rebuild — widget dengan const tidak akan di-rebuild jika parent-nya rebuild.

// ✅ Gunakan const kapanpun memungkinkan
class AppHeader extends StatelessWidget {
  const AppHeader({super.key}); // const constructor

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text(
        'Selamat Datang',
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      ),
    );
  }
}

// Penggunaan — const di depan widget
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(
        children: [
          AppHeader(), // Tidak akan rebuild saat HomePage rebuild
          // ... widget lain
        ],
      ),
    );
  }
}

2. Gunakan Key dengan Benar

Key membantu Flutter mengidentifikasi widget secara unik, terutama dalam list atau ketika widget berpindah posisi.

// ✅ Gunakan key pada item dalam list
class TodoList extends StatelessWidget {
  final List<Todo> todos;

  const TodoList({super.key, required this.todos});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        final todo = todos[index];
        return TodoTile(
          key: ValueKey(todo.id), // Key unik berdasarkan ID
          todo: todo,
        );
      },
    );
  }
}

// ✅ Gunakan key saat widget berpindah posisi
class AnimatedList extends StatelessWidget {
  final bool showFirst;

  const AnimatedList({super.key, required this.showFirst});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (showFirst)
          const InfoCard(
            key: ValueKey('card-a'),
            title: 'Card A',
          ),
        const InfoCard(
          key: ValueKey('card-b'),
          title: 'Card B',
        ),
      ],
    );
  }
}

3. Jaga Widget Tetap Kecil dan Fokus

Setiap widget sebaiknya melakukan satu hal dengan baik. Jika widget mulai menangani banyak tanggung jawab, pecah menjadi widget-widget yang lebih kecil.

// ✅ Widget kecil dan fokus
class PriceTag extends StatelessWidget {
  final double price;
  final double? discountPrice;

  const PriceTag({
    super.key,
    required this.price,
    this.discountPrice,
  });

  @override
  Widget build(BuildContext context) {
    final hasDiscount = discountPrice != null && discountPrice! < price;

    return Row(
      children: [
        if (hasDiscount) ...[
          Text(
            'Rp ${price.toStringAsFixed(0)}',
            style: const TextStyle(
              decoration: TextDecoration.lineThrough,
              color: Colors.grey,
            ),
          ),
          const SizedBox(width: 8),
        ],
        Text(
          'Rp ${(hasDiscount ? discountPrice! : price).toStringAsFixed(0)}',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 18,
            color: hasDiscount ? Colors.red : null,
          ),
        ),
      ],
    );
  }
}

4. Prefer Widget Class daripada Helper Method

Gunakan widget class terpisah daripada helper method di dalam widget. Widget class mendapatkan optimasi rebuild dari Flutter, sedangkan helper method selalu di-rebuild bersama parent.

// ❌ Helper method — selalu rebuild bersama parent
class MyPage extends StatelessWidget {
  const MyPage({super.key});

  // Method ini dipanggil setiap kali MyPage rebuild
  Widget _buildHeader() {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Header', style: TextStyle(fontSize: 24)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildHeader(), // Rebuild setiap kali
      ],
    );
  }
}

// ✅ Widget class terpisah — Flutter bisa mengoptimalkan rebuild
class PageHeader extends StatelessWidget {
  const PageHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Header', style: TextStyle(fontSize: 24)),
    );
  }
}

class MyPage extends StatelessWidget {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        PageHeader(), // Flutter bisa skip rebuild jika tidak berubah
      ],
    );
  }
}

5. Gunakan Named Constructors untuk Variasi

Jika widget memiliki beberapa variasi umum, gunakan named constructors untuk menyederhanakan penggunaan.

class StatusBadge extends StatelessWidget {
  final String label;
  final Color backgroundColor;
  final Color textColor;
  final IconData? icon;

  const StatusBadge({
    super.key,
    required this.label,
    required this.backgroundColor,
    required this.textColor,
    this.icon,
  });

  // Named constructors untuk variasi umum
  const StatusBadge.success({super.key})
      : label = 'Berhasil',
        backgroundColor = const Color(0xFFE8F5E9),
        textColor = const Color(0xFF2E7D32),
        icon = Icons.check_circle;

  const StatusBadge.warning({super.key})
      : label = 'Menunggu',
        backgroundColor = const Color(0xFFFFF3E0),
        textColor = const Color(0xFFE65100),
        icon = Icons.access_time;

  const StatusBadge.error({super.key})
      : label = 'Gagal',
        backgroundColor = const Color(0xFFFFEBEE),
        textColor = const Color(0xFFC62828),
        icon = Icons.error;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: backgroundColor,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (icon != null) ...[
            Icon(icon, size: 16, color: textColor),
            const SizedBox(width: 4),
          ],
          Text(
            label,
            style: TextStyle(
              color: textColor,
              fontWeight: FontWeight.w600,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

// Penggunaan — bersih dan ekspresif
class OrderStatus extends StatelessWidget {
  const OrderStatus({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        StatusBadge.success(),
        StatusBadge.warning(),
        StatusBadge.error(),
      ],
    );
  }
}

Ringkasan

PrinsipPenjelasan
Composition over inheritanceGabungkan widget kecil, jangan extend widget yang ada
Pecah widget besarSetiap widget sebaiknya punya satu tanggung jawab
StatelessWidget firstMulai dengan StatelessWidget, ubah ke StatefulWidget hanya jika perlu
Parameter yang fleksibelDesain API widget dengan required dan optional parameters
Builder patternDelegasikan pembuatan child ke pemanggil melalui callback
Callback patternGunakan callback untuk komunikasi child ke parent
Const constructorsGunakan const untuk optimasi rebuild
Widget class vs methodPrefer widget class terpisah daripada helper method

Dengan menerapkan prinsip-prinsip komposisi widget ini, Anda akan membangun UI Flutter yang modular, mudah di-maintain, dan performant. Setiap widget menjadi building block yang bisa digunakan ulang dan diuji secara independen.

Selanjutnya

Lanjutkan ke halaman Performance Optimization untuk mempelajari cara mengoptimalkan performa aplikasi Flutter — termasuk menghindari rebuild yang tidak perlu, menggunakan const constructors secara efektif, dan strategi lazy loading.

On this page