StatefulWidget #

StatefulWidget adalah widget yang memiliki state internal yang bisa berubah sepanjang hidupnya. Ketika state berubah, Flutter memanggil ulang build() untuk merender UI yang baru. Memahami StatefulWidget secara mendalam — terutama lifecycle-nya — adalah kunci untuk menulis aplikasi Flutter yang responsif sekaligus efisien.

Anatomi StatefulWidget #

StatefulWidget terdiri dari dua kelas yang bekerja bersama:

// BAGIAN 1: Widget (immutable, konfigurasi)
class KonterWidget extends StatefulWidget {
  final String judul;         // konfigurasi dari luar -- immutable
  final int initialValue;

  const KonterWidget({
    super.key,
    required this.judul,
    this.initialValue = 0,
  });

  // Satu-satunya method yang wajib: membuat State
  @override
  State<KonterWidget> createState() => _KonterWidgetState();
}

// BAGIAN 2: State (mutable, logika dan state internal)
class _KonterWidgetState extends State<KonterWidget> {
  // State internal yang bisa berubah
  late int _nilai;
  bool _sedangMenyimpan = false;

  // Akses konfigurasi widget via 'widget'
  @override
  void initState() {
    super.initState();
    _nilai = widget.initialValue;  // gunakan widget.initialValue
  }

  void _tambah() {
    setState(() {
      _nilai++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(widget.judul),         // akses konfigurasi widget di build
        Text('Nilai: $_nilai'),
        if (_sedangMenyimpan)
          const CircularProgressIndicator()
        else
          ElevatedButton(
            onPressed: _tambah,
            child: const Text('Tambah'),
          ),
      ],
    );
  }
}

Mengapa dua kelas? Karena Flutter perlu memisahkan yang berubah dari yang tidak. Widget di-recreate sering (setiap parent rebuild), tapi State bertahan selama widget ada di tree. State adalah “memori” widget.


Lifecycle Lengkap #

Lifecycle StatefulWidget berjalan dalam urutan: createStateinitStatedidChangeDependenciesbuild → (updates: didUpdateWidget/setState) → deactivatedispose.

createState()
     |
     v
initState()            -- sekali, saat State baru dibuat
     |
     v
didChangeDependencies()-- sekali setelah initState, lalu
     |                    setiap kali InheritedWidget berubah
     v
build()                -- pertama kali render
     |
     +<-- setState()   -- trigger rebuild saat state berubah
     |
     +<-- didUpdateWidget()  -- parent berubah konfigurasi
     |
     v
deactivate()           -- widget dilepas dari tree (sementara)
     |
     v
dispose()              -- widget dihancurkan permanen

Panduan Setiap Lifecycle Method #

createState() #

Dipanggil oleh Flutter saat pertama kali widget di-inflate ke dalam tree. Hanya satu tugasnya: membuat dan mengembalikan instance State.

@override
State<MyWidget> createState() => _MyWidgetState();

// Jangan lakukan apapun di sini selain membuat State
// Inisialisasi ada di initState()

initState() #

Dipanggil sekali ketika State object baru dibuat. Gunakan untuk menginisialisasi variabel, set up listener, atau melakukan pekerjaan setup satu kali.

@override
void initState() {
  super.initState();  // SELALU panggil super.initState() pertama!

  // OK: inisialisasi controller
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );

  // OK: setup subscription
  _sub = eventBus.on<UserEvent>().listen(_handleUserEvent);

  // OK: inisialisasi nilai dari widget config
  _nilai = widget.initialValue;

  // OK: fetch data (tapi gunakan Future, jangan await langsung)
  _fetchInitialData();

  // JANGAN: akses context.dependOnInheritedWidgetOfExactType di sini!
  // InheritedWidget belum tersedia -- gunakan didChangeDependencies
  // final theme = Theme.of(context);  // TIDAK AMAN di initState!
}

didChangeDependencies() #

Dipanggil setelah initState dan setiap kali inherited widget yang digunakan oleh state ini berubah. Ini adalah tempat yang aman untuk mengakses nilai inherited (Provider, Theme, MediaQuery).

String? _cachedLocale;

@override
void didChangeDependencies() {
  super.didChangeDependencies();

  // AMAN mengakses InheritedWidget di sini
  final locale = Localizations.localeOf(context).toString();

  // Gunakan cache untuk menghindari pekerjaan yang tidak perlu
  if (locale != _cachedLocale) {
    _cachedLocale = locale;
    _reloadLocalizedContent(locale);  // hanya reload saat locale benar-benar berubah
  }
}

build() #

Dipanggil setiap kali Flutter perlu merender widget: setelah initState, didChangeDependencies, setState, dan didUpdateWidget. Build bisa dipanggil sangat sering — jaga agar tetap murni dan efisien.

@override
Widget build(BuildContext context) {
  // JANGAN: komputasi berat, network call, atau side effect di sini
  // final data = hitungSesuatuYangBerat();  // dipanggil setiap rebuild!

  // BOLEH: transformasi ringan dari state yang sudah ada
  final isValid = _email.contains('@') && _password.length >= 8;
  final buttonColor = isValid
      ? Theme.of(context).colorScheme.primary
      : Colors.grey;

  return Column(
    children: [
      TextField(onChanged: (v) => setState(() => _email = v)),
      TextField(onChanged: (v) => setState(() => _password = v)),
      ElevatedButton(
        style: ElevatedButton.styleFrom(backgroundColor: buttonColor),
        onPressed: isValid ? _submit : null,
        child: const Text('Login'),
      ),
    ],
  );
}

setState() #

Mekanisme utama untuk memperbarui UI. Semua perubahan state yang harus tercermin di UI harus dibungkus dalam setState().

// BENAR -- ubah state di dalam callback setState
void _update() {
  setState(() {
    _counter++;
    _isLoading = false;
    _items = [..._items, newItem];
  });
}

// JUGA BENAR -- setState tanpa callback (tapi kurang idiomatis)
void _update() {
  _counter++;        // ubah state dulu
  _isLoading = false;
  setState(() {});   // lalu trigger rebuild
  // Flutter hanya perlu tahu "ada yang berubah" -- ia akan rebuild
}

// ANTI-PATTERN -- setState di dalam build()
@override
Widget build(BuildContext context) {
  setState(() { _counter = 0; });  // infinite loop! build -> setState -> build
  return Text('$_counter');
}

Jangan panggil setState() setelah dispose()! Setelah widget dihancurkan, memanggil setState() akan throw error. Selalu periksa mounted sebelum memanggil setState() di dalam callback async:

Future<void> _fetchData() async {
  final data = await api.getData();
  if (!mounted) return;  // widget mungkin sudah di-dispose!
  setState(() => _data = data);
}

didUpdateWidget() #

Dipanggil setiap kali parent rebuild dan memberikan konfigurasi widget yang baru (tipe sama, key sama). Gunakan untuk merespons perubahan konfigurasi dari luar.

@override
void didUpdateWidget(KonterWidget oldWidget) {
  super.didUpdateWidget(oldWidget);

  // Bandingkan konfigurasi lama vs baru
  if (widget.initialValue != oldWidget.initialValue) {
    // Konfigurasi berubah -- update state yang relevan
    setState(() {
      _nilai = widget.initialValue;
    });
  }

  if (widget.animationDuration != oldWidget.animationDuration) {
    _controller.duration = widget.animationDuration;
  }
}

// Catatan: Flutter selalu memanggil build() setelah didUpdateWidget()
// Jadi setState() di dalam didUpdateWidget() sebenarnya redundan
// tapi bisa membantu kejelasan kode

deactivate() #

Dipanggil saat State dilepas dari tree — tapi mungkin di-insert kembali. Ini terjadi misalnya saat widget dipindahkan menggunakan GlobalKey. Jarang perlu di-override.

@override
void deactivate() {
  // Lepas hubungan dengan ancestor (misal: ancestor punya pointer ke descendant RenderObject)
  // Jangan dispose resource di sini -- gunakan dispose()
  super.deactivate();
}

dispose() #

Dipanggil saat State dihancurkan permanen. Setelah dispose(), state dianggap unmounted dan mounted bernilai false. Memanggil setState() setelah ini adalah error.

@override
void dispose() {
  // SELALU dispose semua resource di sini!

  // Controllers
  _animationController.dispose();
  _scrollController.dispose();
  _pageController.dispose();
  _textController.dispose();

  // Subscriptions
  _streamSubscription.cancel();
  _focusNode.dispose();

  // Timer
  _debounceTimer?.cancel();

  // ChangeNotifier (jika di-listen secara manual)
  _viewModel.removeListener(_onViewModelChanged);

  super.dispose();  // SELALU panggil super.dispose() terakhir!
}

Dua Kategori StatefulWidget #

Ada dua kategori utama StatefulWidget. Pertama, widget yang mengalokasikan resource di initState dan membuangnya di dispose, tapi tidak bergantung pada InheritedWidget atau memanggil setState. Widget jenis ini umumnya digunakan di root aplikasi atau halaman, dan berkomunikasi dengan subwidget via ChangeNotifier, Stream, atau objek sejenis. Widget ini relatif murah karena di-build sekali dan tidak pernah diperbarui. Kedua, widget yang menggunakan setState atau bergantung pada InheritedWidget. Widget ini akan rebuild berkali-kali selama lifetime aplikasi, sehingga penting untuk meminimalkan dampak rebuild-nya.

// Kategori 1: Root/Screen widget -- build sekali, resource manager
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // Hanya setup dan cleanup -- tidak banyak setState
  late final HomeViewModel _viewModel;

  @override
  void initState() {
    super.initState();
    _viewModel = HomeViewModel(produkRepo, userRepo);
    _viewModel.init();    // fetch initial data
  }

  @override
  Widget build(BuildContext context) {
    // Delegate ke child widgets yang masing-masing manage UI-nya
    return ListenableBuilder(
      listenable: _viewModel,
      builder: (context, _) => HomeView(viewModel: _viewModel),
    );
  }

  @override
  void dispose() {
    _viewModel.dispose();
    super.dispose();
  }
}

// Kategori 2: Interactive widget -- sering setState
class SearchBar extends StatefulWidget {
  final ValueChanged<String> onSearch;
  const SearchBar({super.key, required this.onSearch});
  @override
  State<SearchBar> createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  final _controller = TextEditingController();
  bool _isEmpty = true;
  Timer? _debounce;

  void _onChanged(String value) {
    setState(() => _isEmpty = value.isEmpty);  // sering dipanggil

    _debounce?.cancel();
    _debounce = Timer(const Duration(milliseconds: 300), () {
      widget.onSearch(value);
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      onChanged: _onChanged,
      decoration: InputDecoration(
        hintText: 'Cari...',
        suffixIcon: _isEmpty
            ? null
            : IconButton(
                icon: const Icon(Icons.clear),
                onPressed: () {
                  _controller.clear();
                  setState(() => _isEmpty = true);
                  widget.onSearch('');
                },
              ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    _debounce?.cancel();
    super.dispose();
  }
}

Mengoptimasi setState #

Salah satu optimasi terpenting adalah meminimalkan scope rebuild dengan setState. Semakin kecil widget yang di-rebuild, semakin baik.

// TIDAK EFISIEN -- setState di widget besar
class _BigScreenState extends State<BigScreen> {
  bool _isLoading = false;

  Future<void> _load() 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 VeryComplexHeader(),    // rebuild meski tidak perlu
        const VeryComplexBody(),      // rebuild meski tidak perlu
        if (_isLoading)
          const CircularProgressIndicator()
        else
          ElevatedButton(onPressed: _load, child: const Text('Load')),
      ],
    );
  }
}

// LEBIH EFISIEN -- isolasi state ke widget kecil
class LoadingButton extends StatefulWidget {
  final Future<void> Function() onPressed;
  final String label;
  const LoadingButton({super.key, required this.onPressed, required this.label});
  @override
  State<LoadingButton> createState() => _LoadingButtonState();
}

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);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isLoading ? null : _handle,
      child: _isLoading
          ? const SizedBox(
              width: 16, height: 16,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : Text(widget.label),
    );
  }
}

// Di BigScreen: tidak ada setState lagi!
class BigScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const VeryComplexHeader(),
        const VeryComplexBody(),
        LoadingButton(
          label: 'Load',
          onPressed: fetchData,   // hanya LoadingButton yang rebuild
        ),
      ],
    );
  }
}

Caching Widget untuk Performa #

Jika sebuah subtree tidak berubah, cache widget yang merepresentasikan subtree tersebut dan gunakan ulang setiap kali diperlukan. Cara melakukannya: assign widget ke final state variable dan gunakan ulang di build method. Jauh lebih efisien untuk me-reuse widget daripada membuat widget baru yang identik.

class _OptimizedScreenState extends State<OptimizedScreen> {
  // Cache widget yang mahal dan tidak berubah
  late final Widget _staticSidebar;
  late final Widget _staticHeader;

  @override
  void initState() {
    super.initState();
    // Buat sekali, gunakan berkali-kali
    _staticSidebar = const ExpensiveSidebar();
    _staticHeader = const ComplexHeader();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        _staticSidebar,    // di-reuse -- tidak dibuat ulang setiap build
        Expanded(
          child: Column(
            children: [
              _staticHeader,    // di-reuse
              DynamicContent(state: _state),  // ini yang berubah
            ],
          ),
        ),
      ],
    );
  }
}

Ringkasan #

  • StatefulWidget terdiri dari dua kelas: Widget (immutable, konfigurasi) dan State (mutable, logika dan state internal). Widget sering dibuat ulang, State bertahan selama widget ada di tree.
  • Lifecycle: createStateinitStatedidChangeDependenciesbuild → (updates) → deactivatedispose.
  • Gunakan initState untuk inisialisasi resource satu kali. Gunakan didChangeDependencies untuk mengakses InheritedWidget (Theme, Provider). Gunakan didUpdateWidget untuk merespons perubahan konfigurasi. Gunakan dispose untuk membersihkan semua resource.
  • Selalu periksa mounted sebelum memanggil setState() di dalam callback async untuk menghindari error setelah widget di-dispose.
  • Ada dua kategori: widget root/screen (build sekali, resource manager) dan widget interactive (sering setState). Masing-masing perlu strategi yang berbeda.
  • Minimalkan scope rebuild dengan memindahkan state ke widget yang lebih kecil dan spesifik — hindari setState() di widget besar yang memiliki banyak child.
  • Cache widget statis sebagai final field di State untuk menghindari rekonstruksi yang tidak perlu setiap build() dipanggil.

← Sebelumnya: StatelessWidget   Berikutnya: InheritedWidget →

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