Widget Anti-Pattern #

Setelah mempelajari cara yang benar membangun widget di Flutter, penting juga memahami pola-pola yang harus dihindari. Anti-pattern adalah solusi yang terlihat benar di permukaan tapi menyebabkan masalah nyata — performa buruk, memory leak, rebuild berlebihan, atau kode yang sulit dipelihara. Artikel ini mengumpulkan anti-pattern paling umum yang ditemukan di codebase Flutter nyata, lengkap dengan diagnosis dan perbaikannya.

1. Tidak Menggunakan const #

Salah satu perbaikan performa paling mudah dan paling sering diabaikan. Menggunakan const untuk widget yang memenuhi syarat bisa mengurangi widget rebuild hingga 70%. Flutter dapat meng-cache dan mereuse instance yang sama alih-alih membuat objek baru setiap rebuild.

// ANTI-PATTERN: widget tanpa const meskipun bisa di-const
class _MyScreenState extends State<MyScreen> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $_counter'),
        SizedBox(height: 16),       // dibuat ulang setiap rebuild!
        Icon(Icons.star),            // dibuat ulang setiap rebuild!
        Padding(
          padding: EdgeInsets.all(16),  // dibuat ulang setiap rebuild!
          child: Text('Label statis'),   // dibuat ulang setiap rebuild!
        ),
      ],
    );
  }
}

// BENAR: const pada semua yang tidak bergantung pada state
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('Counter: $_counter'),  // tidak bisa const (runtime value)
      const SizedBox(height: 16),  // const -- tidak pernah rebuild
      const Icon(Icons.star),      // const -- tidak pernah rebuild
      const Padding(
        padding: EdgeInsets.all(16),
        child: Text('Label statis'),  // const -- tidak pernah rebuild
      ),
    ],
  );
}
Aktifkan lint rule prefer_const_constructors dan prefer_const_literals_to_create_immutables di analysis_options.yaml. Flutter DevTools Performance tab menunjukkan widget mana yang paling sering rebuild — mulai dari sana.

2. Widget Monolitik — Satu Widget untuk Segalanya #

Membuat widget besar yang menangani banyak tanggung jawab adalah anti-pattern yang mempersulit pemeliharaan, pengujian, dan optimasi rebuild.

// ANTI-PATTERN: satu widget besar yang menangani segalanya
class ProfilScreen extends StatefulWidget { ... }

class _ProfilScreenState extends State<ProfilScreen> {
  User? _user;
  List<Post> _posts = [];
  bool _isFollowing = false;
  // ... banyak state

  @override
  Widget build(BuildContext context) {
    // 200+ baris build method yang menangani:
    // - header dengan foto profil
    // - statistik (follower, following, post count)
    // - tombol follow/unfollow
    // - grid foto/video
    // - highlights/story
    // Semuanya dalam satu build() -- setiap setState rebuild semuanya!
    return Column(children: [
      /* 200 baris... */
    ]);
  }
}

// BENAR: pecah menjadi widget kecil 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 CustomScrollView(
      slivers: [
        SliverToBoxAdapter(child: ProfilHeader(user: user)),
        SliverToBoxAdapter(child: ProfilStats(user: user)),
        SliverToBoxAdapter(child: ProfilActions(user: user)),
        SliverGrid(
          delegate: SliverChildBuilderDelegate(
            (context, i) => PostThumbnail(post: posts[i]),
            childCount: posts.length,
          ),
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
          ),
        ),
      ],
    );
  }
}

// Setiap widget kecil hanya rebuild ketika datanya berubah
class ProfilHeader extends StatelessWidget { ... }    // hanya rebuild saat user berubah
class ProfilStats extends StatelessWidget { ... }     // hanya rebuild saat stats berubah
class ProfilActions extends StatefulWidget { ... }    // state follow/unfollow terisolasi

3. Helper Function sebagai Pengganti Widget #

Anti-pattern yang sangat umum: menggunakan fungsi biasa untuk membangun bagian UI alih-alih membuat widget terpisah. Hasilnya: konten tidak bisa di-const, tidak bisa di-cache, dan selalu di-rebuild bersama parent.

// ANTI-PATTERN: helper function yang mengembalikan Widget
class _HomeState extends State<HomeScreen> {
  int _counter = 0;

  // Fungsi ini SELALU dipanggil ulang setiap kali _counter berubah
  Widget _buildHeader() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blue,
      child: const Column(
        children: [
          CircleAvatar(radius: 40, child: Icon(Icons.person, size: 40)),
          SizedBox(height: 8),
          Text('Header Statis yang Mahal'),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildHeader(),      // dipanggil ulang setiap counter berubah!
        Text('$_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('Tambah'),
        ),
      ],
    );
  }
}

// BENAR: widget terpisah yang bisa di-const
class AppHeader extends StatelessWidget {
  const AppHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blue,
      child: const Column(
        children: [
          CircleAvatar(radius: 40, child: Icon(Icons.person, size: 40)),
          SizedBox(height: 8),
          Text('Header Statis yang Mahal'),
        ],
      ),
    );
  }
}

class _HomeState extends State<HomeScreen> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const AppHeader(),   // TIDAK pernah rebuild meskipun counter berubah!
        Text('$_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('Tambah'),
        ),
      ],
    );
  }
}

4. setState() yang Terlalu Luas #

Memanggil setState() di widget besar menyebabkan seluruh subtree di-rebuild, meskipun yang berubah hanya sebagian kecil UI.

// ANTI-PATTERN: setState di widget besar yang punya banyak child
class _ScreenState extends State<BigScreen> {
  bool _isLoading = false;

  Future<void> _refresh() async {
    setState(() => _isLoading = true);  // rebuild SELURUH BigScreen!
    await fetchData();
    setState(() => _isLoading = false); // rebuild SELURUH BigScreen lagi!
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveHeader(),   // rebuild tidak perlu!
        const ExpensiveContent(),  // rebuild tidak perlu!
        const ExpensiveFooter(),   // rebuild tidak perlu!
        if (_isLoading)
          const LinearProgressIndicator(),  // ini yang seharusnya berubah
      ],
    );
  }
}

// BENAR: isolasi state ke widget sekecil mungkin
class _ScreenState extends State<BigScreen> {
  Future<void> _refresh() => fetchData();  // tidak ada setState di sini

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveHeader(),   // tidak pernah rebuild
        const ExpensiveContent(),  // tidak pernah rebuild
        const ExpensiveFooter(),   // tidak pernah rebuild
        LoadingButton(             // hanya widget ini yang rebuild
          onPressed: _refresh,
          child: const Text('Refresh'),
        ),
      ],
    );
  }
}

// Widget kecil dengan state terisolasi
class LoadingButton extends StatefulWidget { ... }
class _LoadingButtonState extends State<LoadingButton> {
  bool _isLoading = false;

  Future<void> _handle() async {
    setState(() => _isLoading = true);   // hanya LoadingButton yang rebuild
    try { await widget.onPressed(); }
    finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }
  // ...
}

5. Memory Leak — Tidak Dispose Resource #

Tidak memanggil dispose() pada controller dan subscription adalah penyebab memory leak paling umum di Flutter. Resource tetap hidup di memory meskipun widget sudah dihancurkan.

// ANTI-PATTERN: resource tidak di-dispose
class _VideoPlayerState extends State<VideoPlayer> {
  late AnimationController _controller;
  late StreamSubscription _subscription;
  late TextEditingController _textController;
  late ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _subscription = dataStream.listen(_handleData);
    _textController = TextEditingController();
    _scrollController = ScrollController();
    // LUPA dispose -- semua resource bocor ke memory!
  }

  // dispose() tidak ada atau tidak lengkap

  @override
  Widget build(BuildContext context) => Container();
}

// BENAR: dispose SEMUA resource
class _VideoPlayerState extends State<VideoPlayer> {
  late AnimationController _controller;
  late StreamSubscription _subscription;
  late TextEditingController _textController;
  late ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _subscription = dataStream.listen(_handleData);
    _textController = TextEditingController();
    _scrollController = ScrollController();
  }

  @override
  void dispose() {
    _controller.dispose();        // AnimationController
    _subscription.cancel();       // StreamSubscription
    _textController.dispose();    // TextEditingController
    _scrollController.dispose();  // ScrollController
    super.dispose();              // SELALU panggil terakhir
  }

  @override
  Widget build(BuildContext context) => Container();
}

6. setState() Setelah Widget Di-dispose #

Memanggil setState() setelah widget dihancurkan menyebabkan error: setState() called after dispose(). Ini sering terjadi di callback async.

// ANTI-PATTERN: tidak cek mounted sebelum setState
class _DataState extends State<DataWidget> {
  List<Item> _items = [];

  Future<void> _loadData() async {
    final data = await api.fetchItems();  // async -- butuh waktu
    // Widget mungkin sudah di-dispose saat ini!
    setState(() => _items = data);         // ERROR jika sudah dispose!
  }

  // ...
}

// BENAR: selalu cek mounted sebelum setState di callback async
class _DataState extends State<DataWidget> {
  List<Item> _items = [];

  Future<void> _loadData() async {
    final data = await api.fetchItems();
    if (!mounted) return;                  // cek dulu!
    setState(() => _items = data);         // aman
  }

  // ...
}

7. shrinkWrap: true + NeverScrollableScrollPhysics #

Kombinasi ini terlihat seperti solusi mudah untuk menempatkan ListView di dalam ListView, tapi sangat berbahaya untuk performa — semua item dibangun sekaligus tanpa lazy loading.

// ANTI-PATTERN: shrinkWrap yang membunuh lazy loading
Column(
  children: [
    const HeaderWidget(),
    ListView.builder(
      shrinkWrap: true,                             // BAHAYA untuk list panjang
      physics: const NeverScrollableScrollPhysics(), // BAHAYA
      itemCount: 10000,   // semua 10.000 item dibangun sekaligus!
      itemBuilder: (context, index) => ListTile(
        title: Text('Item $index'),
      ),
    ),
    const FooterWidget(),
  ],
)

// BENAR: gunakan CustomScrollView dengan Sliver
CustomScrollView(
  slivers: [
    const SliverToBoxAdapter(child: HeaderWidget()),
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ListTile(title: Text('Item $index')),
        childCount: 10000,  // lazy loading otomatis -- hanya yang terlihat dibangun
      ),
    ),
    const SliverToBoxAdapter(child: FooterWidget()),
  ],
)

8. Opacity Widget pada Animasi #

Menggunakan Opacity widget untuk animasi adalah anti-pattern karena ia menyebabkan saveLayer() — operasi GPU yang mahal yang mengalokasikan offscreen buffer.

// ANTI-PATTERN: Opacity di dalam animasi
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Opacity(
      opacity: _controller.value,  // saveLayer() setiap frame!
      child: child,
    );
  },
  child: const MyWidget(),
)

// BENAR: gunakan FadeTransition atau AnimatedOpacity
// FadeTransition menggunakan layer compositing -- jauh lebih efisien
FadeTransition(
  opacity: _controller,  // tidak ada saveLayer()
  child: const MyWidget(),
)

// Atau untuk implicit:
AnimatedOpacity(
  opacity: _isVisible ? 1.0 : 0.0,
  duration: const Duration(milliseconds: 300),
  child: const MyWidget(),
)

Hindari widget Opacity, dan terutama hindari di dalam animasi. Gunakan AnimatedOpacity atau FadeInImage sebagai gantinya. Untuk efek opacity sederhana tanpa animasi, pertimbangkan menggunakan warna dengan nilai alpha alih-alih Opacity widget.


9. Logika Bisnis di dalam build() #

Melakukan komputasi berat, pemformatan kompleks, atau operasi yang tidak perlu di dalam build() menyebabkan pekerjaan yang berulang setiap kali widget di-rebuild.

// ANTI-PATTERN: komputasi berat di build()
@override
Widget build(BuildContext context) {
  // Dipanggil setiap rebuild -- operasi O(n²)!
  final filtered = produk
      .where((p) => p.harga < _maxHarga)
      .toList()
      ..sort((a, b) => a.nama.compareTo(b.nama));

  // Pemformatan yang tidak perlu diulang
  final formattedDate = DateFormat('EEEE, dd MMMM yyyy', 'id').format(DateTime.now());

  // Regex yang dikompilasi ulang setiap rebuild
  final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+');

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

// BENAR: precompute di luar build atau cache hasilnya
class _ProdukState extends State<ProdukScreen> {
  List<Produk> _produk = [];
  double _maxHarga = 1000000;
  List<Produk>? _cachedFiltered;  // cache hasil filter

  // Recompute hanya saat data atau filter berubah
  List<Produk> get _filteredProduk {
    return _cachedFiltered ??= _produk
        .where((p) => p.harga < _maxHarga)
        .toList()
      ..sort((a, b) => a.nama.compareTo(b.nama));
  }

  void _setMaxHarga(double harga) {
    setState(() {
      _maxHarga = harga;
      _cachedFiltered = null;  // invalidate cache
    });
  }

  // Buat satu kali, gunakan berulang -- letakkan di luar build atau sebagai const
  static final _emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+');
  static final _dateFormatter = DateFormat('EEEE, dd MMMM yyyy', 'id');

  @override
  Widget build(BuildContext context) {
    // build() sekarang ringan -- hanya menyusun widget
    return ProdukList(produk: _filteredProduk);
  }
}

10. GlobalKey yang Berlebihan #

GlobalKey sangat powerful tapi mahal — ia menonaktifkan optimasi widget tree dan menyebabkan widget selalu di-rebuild dari awal saat dipindahkan. Penggunaan berlebihan bisa menjadi bottleneck signifikan.

// ANTI-PATTERN: GlobalKey untuk tujuan yang tidak memerlukannya
class _FormState extends State<MyForm> {
  // GlobalKey untuk setiap field -- sangat mahal!
  final _namaKey = GlobalKey<FormFieldState>();
  final _emailKey = GlobalKey<FormFieldState>();
  final _phoneKey = GlobalKey<FormFieldState>();
  final _addressKey = GlobalKey<FormFieldState>();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextFormField(key: _namaKey),    // GlobalKey di setiap field
        TextFormField(key: _emailKey),
        TextFormField(key: _phoneKey),
        TextFormField(key: _addressKey),
      ],
    );
  }
}

// BENAR: satu GlobalKey untuk Form, controller untuk kontrol field
class _FormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();  // satu GlobalKey untuk Form
  final _namaController = TextEditingController();
  final _emailController = TextEditingController();

  void _submit() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      // proses data
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,  // hanya satu GlobalKey
      child: Column(
        children: [
          TextFormField(controller: _namaController),
          TextFormField(controller: _emailController),
        ],
      ),
    );
  }
}

11. Border.all() di dalam build() #

Border.all() membuat objek Border baru setiap kali widget di-rebuild karena Border bersifat immutable. Untuk widget yang sering di-rebuild, ini menciptakan banyak objek tak berguna.

// ANTI-PATTERN: Border.all() di dalam build()
@override
Widget build(BuildContext context) {
  return Container(
    decoration: BoxDecoration(
      border: Border.all(color: Colors.grey, width: 1),  // objek baru setiap rebuild!
    ),
    child: const Text('Konten'),
  );
}

// BENAR: definisikan border sebagai konstanta
static const _border = Border.fromBorderSide(
  BorderSide(color: Colors.grey, width: 1),
);

@override
Widget build(BuildContext context) {
  return Container(
    decoration: const BoxDecoration(border: _border),  // reuse objek yang sama
    child: const Text('Konten'),
  );
}

12. StatefulWidget Tanpa Alasan #

Menggunakan StatefulWidget saat StatelessWidget sudah cukup menambahkan kompleksitas tanpa manfaat. State object tetap hidup di memory selama widget ada di tree.

// ANTI-PATTERN: StatefulWidget yang tidak punya state internal
class UserAvatar extends StatefulWidget {
  final String imageUrl;
  final double radius;

  const UserAvatar({super.key, required this.imageUrl, this.radius = 24});

  @override
  State<UserAvatar> createState() => _UserAvatarState();
}

class _UserAvatarState extends State<UserAvatar> {
  // Tidak ada state internal sama sekali!
  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      radius: widget.radius,
      backgroundImage: NetworkImage(widget.imageUrl),
    );
  }
}

// BENAR: StatelessWidget sudah cukup
class UserAvatar extends StatelessWidget {
  final String imageUrl;
  final double radius;

  const UserAvatar({super.key, required this.imageUrl, this.radius = 24});

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      radius: radius,
      backgroundImage: NetworkImage(imageUrl),
    );
  }
}

Ringkasan Cepat #

Anti-PatternDampakSolusi
Tidak pakai constRebuild tidak perluTambahkan const di semua tempat yang memungkinkan
Widget monolitikRebuild masif, susah di-maintainPecah menjadi widget kecil yang fokus
Helper functionTidak bisa di-const, selalu rebuildBuat widget terpisah
setState di widget besarRebuild subtree besarIsolasi state ke widget terkecil
Tidak dispose resourceMemory leakDispose di dispose() tanpa kecuali
Tidak cek mountedRuntime errorif (!mounted) return; sebelum setState async
shrinkWrap: trueSemua item dibuat sekaligusGanti dengan CustomScrollView + Sliver
Opacity di animasisaveLayer() setiap frameGunakan FadeTransition atau AnimatedOpacity
Logika berat di build()Komputasi berulangPrecompute atau cache di luar build
GlobalKey berlebihanNonaktifkan optimasiGunakan seperlunya, controller untuk kontrol field
Border.all() di buildObjek baru setiap rebuildDefinisikan sebagai static const
StatefulWidget tanpa stateKompleksitas tidak perluGunakan StatelessWidget
Gunakan Flutter DevTools Performance tab dengan fitur “Track Widget Rebuilds” untuk mengidentifikasi widget yang paling sering rebuild. Mulai perbaikan dari widget dengan rebuild count tertinggi — dampaknya paling besar.

← Sebelumnya: Theming   Berikutnya: Best Practice →

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