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: createState → initState → didChangeDependencies → build → (updates: didUpdateWidget/setState) → deactivate → dispose.
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()setelahdispose()! Setelah widget dihancurkan, memanggilsetState()akan throw error. Selalu periksamountedsebelum memanggilsetState()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:
createState→initState→didChangeDependencies→build→ (updates) →deactivate→dispose.- Gunakan
initStateuntuk inisialisasi resource satu kali. GunakandidChangeDependenciesuntuk mengakses InheritedWidget (Theme, Provider). GunakandidUpdateWidgetuntuk merespons perubahan konfigurasi. Gunakandisposeuntuk membersihkan semua resource.- Selalu periksa
mountedsebelum memanggilsetState()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
finalfield di State untuk menghindari rekonstruksi yang tidak perlu setiapbuild()dipanggil.