setState() — kluczowa metoda klasy State we Flutter, powiadamiająca framework o zmianie danych i uruchamiająca przebudowę interfejsu. Według oficjalnej dokumentacji Flutter (Flutter.dev, 2026), setState jest głównym mechanizmem reaktywności w StatefulWidget: bez jego wywołania UI nie dowie się o zmianach pól State i pozostanie w poprzednim stanie. Metoda przyjmuje callback VoidCallback, wewnątrz którego programista modyfikuje zmienne pola, po czym Flutter automatycznie wywołuje build w celu przebudowania widgetu.
Najważniejsze
setState() — wbudowana metoda klasy State we Flutter, przeznaczona do powiadamiania frameworka o tym, że wewnętrzny stan widgetu uległ zmianie i konieczna jest przebudowa UI. Bez wywołania setState Flutter nie wie o zmianach — nawet jeśli pola State zostały zmodyfikowane, interfejs pozostanie niezmieniony aż do następnego wymuszonego przebudowania przez rodzica.
Sygnatura metody: void setState(VoidCallback fn). Callback jest wykonywany synchronicznie wewnątrz setState i dopiero po jego zakończeniu State jest oznaczany jako dirty. Gwarantuje to, że wszystkie zmiany są stosowane atomowo przed przebudową. Według Dart Language Specification (Dart Team, 2026), atomowość setState zapobiega stanom wyścigu, w których build mógłby zobaczyć częściowo zaktualizowany stan.
setState nie przyjmuje argumentów, nie zwraca wartości i nie może być nadpisany. Jest to ostateczna (sealed) metoda klasy State. Programista nie może zmienić jej zachowania — może tylko używać jej zgodnie z przeznaczeniem. Próba wywołania setState poza State (np. z innej klasy) jest niemożliwa, ponieważ metoda jest zadeklarowana w klasie State.
Częste błędne przekonanie — uważać, że setState sam zmienia stan. To nieprawda. setState jedynie wywołuje przekazany callback (w którym programista zmienia pola) i następnie sygnalizuje frameworkowi konieczność wykonania build. Callback jest obowiązkowy — przekazanie null lub pustego callbacka spowoduje błąd.
Mechanizm działania setState() można podzielić na cztery etapy. Pierwszy — wywołanie metody z callbackiem. Drugi — synchroniczne wykonanie callbacka, wewnątrz którego zmieniane są pola State. Trzeci — State jest oznaczany jako dirty (brudny) w specjalnym polu _dirty. Czwarty — na koniec bieżącego mikrozadania (microtask) Flutter przegląda wszystkie dirty-elementy i wywołuje ich build w kolejności występowania w drzewie.
Ważny szczegół: setState nie wywołuje build natychmiastowo. Flutter stosuje strategię aktualizacji wsadowej: wszystkie dirty-elementy są zbierane i przebudowywane w jednej klatce. Oznacza to, że jeśli setState został wywołany wielokrotnie w ramach jednego synchronicznego bloku, build wykona się tylko raz — po zakończeniu wszystkich zmian. Taka optymalizacja zapobiega wielokrotnym przebudowom w jednej klatce.
Według Flutter Engine Team (Google, 2025), mechanizm dirty-flagów opiera się na przejściu przez BuildOwner._dirtyElements. Każdy dirty StatefulElement jest dodawany do listy i przetwarzany na etapie aktualizacji klatki. Jeśli widget został usunięty z drzewa przed przetworzeniem, jest automatycznie wykluczany z listy dirty-elementów.
Podstawowy przykład setState() z inkrementacją licznika. Demonstruje poprawne użycie: zmiana pola wewnątrz callbacka:
class _CounterState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++; // mutowanie pola wewnątrz callbacka
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _increment,
child: Text('$_count'),
);
}
}
Przykład z polem tekstowym i kontrolerem — setState() do zarządzania widocznością hasła:
class _PasswordFieldState extends State<PasswordField> {
bool _obscured = true;
final _controller = TextEditingController();
void _toggleVisibility() {
setState(() {
_obscured = !_obscured;
});
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
obscureText: _obscured,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(_obscured ? Icons.visibility : Icons.visibility_off),
onPressed: _toggleVisibility,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
W tym przykładzie setState() zmienia tylko pole logiczne _obscured, co powoduje przebudowę TextField z nową ikoną i trybem wyświetlania. Kontroler tekstowy nie jest przy tym ponownie tworzony — jest inicjalizowany raz w initState i zwalniany w dispose.
Jeśli trzeba zmienić kilka pól, wszystkie zmiany wykonuje się wewnątrz jednego setState. Gwarantuje to, że build zobaczy spójny stan:
setState(() {
_isLoading = false;
_items = newItems;
_error = null;
});
Trzy pola są zmieniane w jednym callbacku — build wykona się raz i zobaczy wszystkie zmiany jednocześnie. Gdyby każde wywołanie było osobnym setState, build i tak wykonałby się jednokrotnie dzięki wsadowemu przetwarzaniu dirty-elementów.
Jeden z najważniejszych niuansów setState() — jego zachowanie z operacjami asynchronicznymi. Callback setState jest wykonywany synchronicznie, ale jeśli wewnątrz niego wywołano await, kod po await zostanie wykonany już po zakończeniu działania setState. Oznacza to, że zmiany pól po await nie zostaną przechwycone przez bieżący setState.
Prawidłowe podejście: operacja asynchroniczna jest wykonywana poza setState, a setState jest wywoływany po jej zakończeniu. Cały kod między otrzymaniem wyniku a wywołaniem setState jest wykonywany w kontekście synchronicznym po await:
// POPRAWNIE: await poza setState
Future<void> _loadData() async {
final result = await ApiService.fetchData();
setState(() {
_data = result;
_isLoading = false;
});
}
// ŹLE: await wewnątrz setState — brak gwarancji aktualizacji
void _loadDataWrong() {
setState(() async {
_data = await ApiService.fetchData(); // setState kończy działanie przed zakończeniem await
_isLoading = false; // ten kod nie jest przechwytywany przez setState
});
}
Według Flutter docs (Dart async patterns, 2026), przekazywanie async-callbacka do setState jest antypatternem, ponieważ setState oczekuje VoidCallback (funkcji synchronicznej), a funkcja async zwraca Future, który jest ignorowany. Zmiany po pierwszym await w takim callbacku nie zostaną poprawnie przetworzone przez framework.
Przed wywołaniem setState() po operacji asynchronicznej zawsze sprawdzaj mounted:
if (mounted) {
setState(() => _data = data);
}
Jeśli widget został usunięty z drzewa podczas wykonywania operacji asynchronicznej, mounted stanie się false i setState nie zostanie wywołany. Zapobiega to wyjątkom i wyciekom zasobów.
setState() — wygodny, ale potencjalnie kosztowny mechanizm, jeśli używać go bezmyślnie. Każde wywołanie setState przebudowuje cały widget i wszystkich jego potomków (jeśli nie są const). W głębokich drzewach lub przy częstych wywołaniach może to prowadzić do spadków FPS.
Główne strategie optymalizacji: minimalizować obszar przebudowy (wyodrębniać zmienne części UI do osobnych StatefulWidget), używać const dla niezmiennych potomków i unikać wywoływania setState w widgetach nadrzędnych, jeśli zmienił się tylko mały szczegół UX. Jeśli stan jest aktualizowany z wysoką częstotliwością (animacja, strumień danych), rozważ AnimatedBuilder lub ValueListenableBuilder.
Według Flutter Performance Best Practices (Flutter.dev, luty 2026), profilowanie rzeczywistych aplikacji pokazuje, że do 40% wszystkich wywołań setState można zastąpić const-widgetami potomnymi lub reaktywnymi builderami (StreamBuilder, FutureBuilder). Zmniejsza to średni czas budowy klatki o 15–25%.
| Scenariusz | Alternatywa | Zaleta |
|---|---|---|
| Animacja | AnimatedBuilder | Przebudowuje tylko animowany widget |
| Strumień danych | StreamBuilder | Reaguje na każdy element strumienia |
| Przyszły wynik | FutureBuilder | Zarządza stanami ładowania/błędu |
| Lokalna wartość | ValueListenableBuilder | Reaguje na zmiany jednej wartości |
Pomimo uniwersalności setState(), w dużych projektach stosuje się go głównie do stanu lokalnego. Do stanu globalnego lub współdzielonego używane są specjalistyczne rozwiązania, z których każde zastępuje lub opakowuje setState.
Provider używa ChangeNotifier + notifyListeners jako odpowiednika setState, ale z możliwością subskrypcji przez wiele widgetów. Bloc używa Streams — stan zmienia się poprzez dodawanie zdarzeń do StreamController. Riverpod łączy podejścia, oferując zarówno lokalne (StateProvider), jak i asynchroniczne (AsyncNotifier) zarządzanie bez powiązania z StatefulWidget. Wszystkie trzy podejścia eliminują konieczność ręcznego wywoływania setState — aktualizacja UI następuje automatycznie przy zmianie danych.
Według Flutter Community Survey 2025 (Flutter Foundation, grudzień 2025), 74% programistów używa co najmniej jednego narzędzia do zarządzania stanem oprócz setState. Jednocześnie 92% nadal używa setState do lokalnych danych pola tekstowego, checkboxa lub prostego licznika — uważa się to za best practice.
Pierwszy i najgroźniejszy błąd — wywołanie setState po dispose. Operacja asynchroniczna rozpoczęła się w initState, użytkownik opuścił ekran, widget został usunięty, a callback operacji asynchronicznej wywołuje setState — aplikacja pada z wyjątkiem. Rozwiązanie — zawsze sprawdzaj mounted przed wywołaniem.
Drugi błąd — wywołanie setState wewnątrz build. Prowadzi to do nieskończonej pętli: build → setState → dirty → build → setState → ... Flutter nie blokuje takiego wywołania (otrzymasz StackOverflowError). setState może być wywoływany tylko w odpowiedzi na zdarzenie (naciśnięcie przycisku, zakończenie Future, otrzymanie danych ze strumienia).
Trzeci błąd — zmiana pól State bez wywołania setState. Programista pisze _count++ i oczekuje, że UI się zaktualizuje. Flutter nie potrafi śledzić zmian pól automatycznie — potrzebuje jawnego sygnału przez setState. To fundamentalna różnica w stosunku do reaktywnych frameworków takich jak Vue.js, gdzie zmiana danych automatycznie wyzwala aktualizację.
Czwarty błąd — wywołanie setState z asynchronicznym callbackiem (async-lambda). Jak opisano w sekcji o asynchroniczności, zmiany po await nie zostaną przechwycone, co prowadzi do trudnych do odtworzenia błędów. Używaj synchronicznego callbacka i wywołuj setState po await.
mounted w asynchronicznych callbackachCzęsto zadawane pytania
setState() powiadamia Flutter, że wewnętrzne dane StatefulWidget uległy zmianie i UI wymaga przebudowy. Metoda przyjmuje callback, wykonuje go synchronicznie, oznacza widget jako dirty i planuje wywołanie build w następnej klatce.
UI nie zaktualizuje się. Flutter nie śledzi zmian pól automatycznie. Wartość pola zmieni się w pamięci, ale widget pozostanie w poprzednim stanie aż do następnego wymuszonego przebudowania przez rodzica.
Nie można. Doprowadzi to do nieskończonej pętli: build wywołuje setState, który oznacza widget jako dirty i ponownie wywołuje build. Flutter nie blokuje takiej sytuacji — aplikacja padnie z StackOverflowError.
Build wykona się jeden raz. Flutter zbiera wszystkie dirty-elementy i przebudowuje je wsadowo na koniec klatki. Drugi setState przed przetworzeniem pierwszego po prostu dodaje element do tej samej listy dirty-elementów — ponowny build nie nastąpi.
mounted — flaga logiczna wskazująca, że widget wciąż znajduje się w drzewie. Jeśli po operacji asynchronicznej wywołasz setState bez sprawdzenia mounted, a widget został już usunięty — aplikacja padnie z wyjątkiem „setState called after dispose”.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również