setState() — istota, mechanizm działania i zastosowanie

Autor: IT Sectr Opublikowano: 2026-07-01 Czas czytania: 9 min

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() — metoda State, oznaczająca widget jako brudny (dirty) i planująca przebudowę UI w następnej klatce
  • Callback — setState przyjmuje VoidCallback, wewnątrz którego powinny znajdować się wszystkie zmiany pól State wpływających na interfejs
  • Asynchroniczność — setTimeout lub Future wewnątrz setState nie gwarantują synchronizacji; mutacje po await muszą być wewnątrz innego setState
  • Wydajność — każde wywołanie setState przebudowuje cały widget; aby zminimalizować, używaj const dla widgetów potomnych
  • mounted — przed wywołaniem setState w asynchronicznych callbackach koniecznie sprawdzaj mounted, w przeciwnym razie — wyjątek

Czym jest setState()?

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.

setState nie zmienia stanu — zmieniasz go Ty

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.

Jak działa setState()?

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.

Gwarancje setState

  • Callback jest wykonywany synchronicznie przed oznaczeniem dirty
  • build jest wywoływany nie więcej niż raz na klatkę (nawet przy wielokrotnych setState)
  • Aktualizacja UI następuje w następnej klatce (zwykle ~16ms przy 60 FPS)
  • Po dispose wywołanie jest zabronione — zgłaszany jest wyjątek
  • Podczas build wywołanie setState jest zabronione — nieskończona pętla

Przykłady kodu w Dart

Podstawowy przykład setState() z inkrementacją licznika. Demonstruje poprawne użycie: zmiana pola wewnątrz callbacka:

dart
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:

dart
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.

Wielokrotne mutacje w jednym setState

Jeśli trzeba zmienić kilka pól, wszystkie zmiany wykonuje się wewnątrz jednego setState. Gwarantuje to, że build zobaczy spójny stan:

dart
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.

Asynchroniczność i setState

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:

dart
// 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.

Sprawdzanie mounted w scenariuszach asynchronicznych

Przed wywołaniem setState() po operacji asynchronicznej zawsze sprawdzaj mounted:

dart
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.

Wydajność i optymalizacja

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%.

Kiedy setState jest zbędny

ScenariuszAlternatywaZaleta
AnimacjaAnimatedBuilderPrzebudowuje tylko animowany widget
Strumień danychStreamBuilderReaguje na każdy element strumienia
Przyszły wynikFutureBuilderZarządza stanami ładowania/błędu
Lokalna wartośćValueListenableBuilderReaguje na zmiany jednej wartości

Alternatywy dla setState

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.

Kiedy zostawić setState

  • Stan jest używany tylko przez jeden widget
  • Prosta wartość logiczna lub liczbowa (fokus, widoczność, licznik)
  • Prototypowanie i szybkie eksperymenty
  • Kontrolery (TextEditingController, PageController) i tak wymagają StatefulWidget

Typowe błędy

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.

Porada dotycząca bezpiecznego setState

  • Zawsze sprawdzaj mounted w asynchronicznych callbackach
  • Nie wywołuj setState wewnątrz build
  • Nie przekazuj async-lambdy do setState
  • Nie zmieniaj pól State poza setState
  • Jeśli zmieniasz kilka pól — rób to w jednym setState

Często zadawane pytania

Co robi setState() we Flutter?

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.

Co się stanie, jeśli nie wywołam setState po zmianie pola?

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.

Czy można wywołać setState wewnątrz build?

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.

Ile razy wykona się build przy dwóch kolejnych setState?

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.

Czym jest mounted i dlaczego jest ważny dla setState?

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

  • setState() — metoda State, powiadamiająca Flutter o zmianie danych i uruchamiająca przebudowę UI w następnej klatce
  • Mechanizm działania — synchroniczne wykonanie callbacka, oznaczenie State jako dirty, wsadowa przebudowa wszystkich dirty-elementów na koniec klatki
  • Asynchroniczność — async-callbacki w setState nie działają; await musi być na zewnątrz, a setState — po otrzymaniu wyniku
  • mounted — obowiązkowe sprawdzenie przed setState w operacjach asynchronicznych w celu zapobieżenia wyjątkowi
  • Optymalizacja — minimalizuj obszar przebudowy przez const widgety potomne i wyodrębniaj animacje do AnimatedBuilder
  • Alternatywy — do stanu globalnego używaj Riverpod, Bloc lub Provider; setState zostawiaj dla danych lokalnych
  • Zasada — nie wywołuj setState wewnątrz build, nie przekazuj async-lambd, zawsze sprawdzaj mounted

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.

Omów projekt

Przeczytaj również