StatefulWidget — widget Flutter z zmiennym stanem, pozwalający UI reagować na działania użytkownika, zdarzenia asynchroniczne i strumienie danych. Według oficjalnej dokumentacji Flutter (Flutter.dev, 2026), StatefulWidget jest używany dla wszystkich interaktywnych elementów aplikacji: formularzy, animacji, pól wyboru, przełączników i ekranładujących dane z sieci. W przeciwieństwie do StatelessWidget, tworzy on oddzielny obiekt State, który utrzymuje się przez cały cykl życia i może być przebudowany bez ponownego tworzenia samego widgetu.
Najważniejsze
StatefulWidget — to klasa Flutter, która może zmieniać swój stan w odpowiedzi na działania użytkownika, zdarzenia systemowe lub operacje asynchroniczne. W przeciwieństwie do StatelessWidget, StatefulWidget nie jest wyświetlany bezpośrednio — tworzy obiekt State, który odpowiada za renderowanie. Podział na dwie klasy (Widget i State) pozwala Flutter przebudowywać UI bez ponownego tworzenia samego widgetu, co daje znaczną przewagę wydajnościową przy częstych aktualizacjach.
Architektura StatefulWidget opiera się na wzorcu «rozdzielenia zmiennego i niezmiennego»: sam widget pozostaje niezmienny (jak StatelessWidget), a cały zmienny stan jest przechowywany w oddzielnym obiekcie State. Pozwala to Flutter ponownie używać widgetów, porównując je według typu i Key, a jednocześnie zachowywać aktualny stan między przebudowami.
Według Google (Flutter Architectural Overview, 2026), StatefulWidget jest optymalny dla scenariuszy, w których stan zmienia się częściej niż raz w ciągu życia widgetu: pola tekstowe, animacje, timery, strumienie danych, asynchroniczne ładowanie. Do jednorazowej inicjalizacji wystarczy StatelessWidget.
StatefulWidget jest obowiązkowy, gdy widget musi reagować na zdarzenia zewnętrzne: naciśnięcie przycisku, zakończenie zapytania HTTP, aktualizacja danych z bazy, subskrypcja WebSocket. Jest również niezbędny dla widgetów z animacją, pól tekstowych z kontrolerami i komponentów zarządzających fokusem. Jeśli widget tylko wyświetla dane i nie generuje zdarzeń — użyj StatelessWidget.
StatefulWidget składa się z dwóch klas: właściwego StatefulWidget (lekkiego, niezmiennego) i State (ciężkiego, zmiennego). Framework tworzy State za pomocą metody createState(), wywoływanej jednokrotnie przy osadzaniu w drzewie. State otrzymuje odniesienie do widgetu przez właściwość widget i może odwoływać się do jego pól w dowolnym momencie cyklu życia.
Cykl życia StatefulWidget składa się z sześciu głównych etapów, z których każdy udostępnia metodę do nadpisywania w celu wykonywania specyficznych zadań. Zrozumienie tych etapów jest kluczowe dla prawidłowej pracy z zasobami i unikania wycieków pamięci.
createState — pierwsza metoda cyklu życia, wywoływana przy osadzaniu StatefulWidget w drzewie. Musi zwracać nową instancję State powiązaną z tym widgetem. Ta metoda jest wywoływana dokładnie raz przez cały czas życia elementu. Ważne jest, aby nie wykonywać tu ciężkich operacji — createState powinien być maksymalnie lekki.
initState — wywoływany zaraz po utworzeniu State, przed pierwszym zbudowaniem UI. Tutaj wykonuje się: inicjalizację kontrolerów (TextEditingController, AnimationController), subskrypcję strumieni danych (StreamSubscription), konfigurację timerów i początkową inicjalizację pól. Według Flutter docs (Flutter.dev, 2026), w initState nie można wywoływać BuildContext.of() — drzewo nie jest jeszcze w pełni zamontowane.
didChangeDependencies — wywoływany po initState i za każdym razem, gdy zmieniają się zależności InheritedWidget. To odpowiednie miejsce do wywołania MediaQuery.of(context) lub subskrypcji Theme — wartości, które mogą się zmienić podczas działania aplikacji. Jeśli widget używa InheritedWidget, logika inicjalizacji powinna być tutaj, a nie w initState.
build — główna metoda zwracająca drzewo widgetów. Wywoływana po initState, po didChangeDependencies i po każdym setState. didUpdateWidget wywoływany, gdy rodzic przebudowuje się i przekazuje StatefulWidget z nowymi parametrami. Tutaj można porównać stare i nowe pola widgetu i, w razie potrzeby, zaktualizować stan.
dispose — końcowy etap cyklu życia. Tutaj zwalniane są wszystkie zasoby: anulowane subskrypcje strumieni, usuwane kontrolery, anulowane timery. Brak wywołania dispose prowadzi do wycieków pamięci. Po dispose State jest uważany za martwy — wywołanie setState wewnątrz niego powoduje wyjątek.
Mechanizm działania StatefulWidget opiera się na skoordynowanej pracy trzech jednostek: Widget (lekkiego opisu), Element (pośredniej warstwy) i State (magazynu danych). Gdy Flutter napotyka StatefulWidget w opisie, tworzy StatefulElement, który wywołuje createState i przechowuje referencję do obiektu State. Przy przebudowie rodzica Flutter porównuje nowy widget z bieżącym Element — jeśli typ i Key są zgodne, Element jest aktualizowany, a State pozostaje ten sam.
Stan zmienia się tylko przez wywołanie setState, które powiadamia framework o konieczności przebudowy. Ważne jest zrozumienie: setState nie zmienia stanu automatycznie — oznacza jedynie widget jako «brudny». Deweloper samodzielnie aktualizuje pola State w callbacku przekazanym do setState. Po zakończeniu callbacka Flutter wywołuje build i aktualizuje UI.
Według Dart/Flutter team (Dart Language Specification, 2026), taki podział gwarantuje, że wszystkie zmiany stanu następują synchronicznie przed wywołaniem build, eliminując sytuację, w której UI wyświetla częściowo zaktualizowane dane. To kluczowy mechanizm spójności interfejsu we Flutter.
Rozważmy prosty StatefulWidget — licznik kliknięć przycisku. Pokazuje on podstawowy wzorzec: tworzenie State, inicjalizację pola w initState, zmianę przez setState:
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Liczba: $_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('Zwiększ'),
),
],
);
}
}
Przykład z asynchronicznym ładowaniem danych i zarządzaniem cyklem życia. StatefulWidget ładuje dane z sieci i wyświetla stan ładowania:
class UserProfilePage extends StatefulWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
State<UserProfilePage> createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
UserModel? _user;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadUser();
}
Future<void> _loadUser() async {
final user = await UserService.fetchUser(widget.userId);
setState(() {
_user = user;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const CircularProgressIndicator();
return Text('Witaj, ${_user!.name}');
}
}
W drugim przykładzie ważne jest: initState uruchamia operację asynchroniczną, ale sama metoda nie jest asynchroniczna. Asynchroniczność jest realizowana przez async/await wewnątrz oddzielnej metody _loadUser, która aktualizuje stan przez setState po zakończeniu żądania. Takie podejście gwarantuje, że widget poprawnie wyświetli wskaźnik ładowania przed otrzymaniem danych.
Wybór między StatefulWidget a StatelessWidget to nie tylko kwestia posiadania stanu. StatefulWidget zapewnia pełny cykl życia z metodami initState, didChangeDependencies, didUpdateWidget i dispose, co jest niezbędne do pracy z kontrolerami, animacjami i strumieniami. StatelessWidget z kolei nie ma tych metod i jest zawsze lżejszy dla frameworka.
Zalecenie Flutter team (Flutter docs, 2026) — minimalizować liczbę StatefulWidget w aplikacji, podnosząc stan w górę drzewa (State Hoisting) lub używając rozwiązań do zarządzania stanem (Riverpod, Bloc, Provider). Każdy StatefulWidget tworzy obiekt State, który żyje do usunięcia elementu — im więcej takich widgetów, tym większe obciążenie pamięci.
| Kryterium | StatefulWidget | StatelessWidget |
|---|---|---|
| Stan | Zmienny | Niezmienny |
| Cykl życia | 6 etapów | Tylko build |
| Obiekt State | Tworzony oddzielnie | Nie jest wymagany |
| setState | Dostępny | Niedostępny |
| Subskrypcje | initState/dispose | Nieobsługiwane |
| const konstruktor | Ograniczony | W pełni obsługiwany |
| Zużycie pamięci | Większe | Mniejsze |
StatefulWidget wymaga więcej zasobów niż StatelessWidget ze względu na konieczność tworzenia i utrzymywania obiektu State. Jednak prawidłowe używanie StatefulWidget nie prowadzi do problemów z wydajnością, jeśli przestrzega się kilku zasad. Po pierwsze, unikaj głębokiego zagnieżdżania StatefulWidget — każdy poziom dodaje narzut na przechodzenie drzewa. Po drugie, dziel złożone StatefulWidget na kilka prostych, z których każdy odpowiada za swoją część stanu.
Według badań Flutter Performance (Flutter.dev, luty 2026), najczęstszą przyczyną spadków FPS jest wywołanie setState w nadrzędnym widgecie, które przebudowuje wszystkich potomków, w tym StatelessWidget, które nie zmieniły swojego wyświetlania. Rozwiązaniem jest wydzielenie zmiennej części UI do oddzielnego StatefulWidget, aby setState przebudowywał tylko minimum niezbędnych widgetów.
Używanie const wewnątrz State to kolejna ważna technika. Jeśli podrzędne widgety są zadeklarowane jako const, Flutter nie będzie ich przebudowywać przy wywołaniu setState w rodzicu. Zmniejsza to obciążenie frameworka i skraca czas renderowania klatki.
Każde wywołanie setState uruchamia pełną przebudowę widgetu. Jeśli stan zmienia się z dużą częstotliwością (na przykład animacja lub strumień danych), rozważ użycie AnimatedBuilder, ValueListenableBuilder lub StreamBuilder zamiast ręcznego wywoływania setState. Te widgety optymalizują przebudowę, aktualizując tylko tę część UI, która rzeczywiście się zmieniła.
Pierwszym typowym błędem z StatefulWidget jest wywołanie setState po dispose. Gdy widget zostanie usunięty z drzewa, State jest uważany za martwy i każde wywołanie setState powoduje wyjątek «setState called after dispose». Najczęściej dzieje się tak, gdy operacja asynchroniczna kończy się po usunięciu widgetu. Rozwiązaniem jest sprawdzanie flagi mounted przed wywołaniem setState lub anulowanie operacji asynchronicznych w dispose.
Drugim błędem jest wykonywanie ciężkich obliczeń w metodzie build. Ponieważ build jest wywoływany przy każdym setState i przy każdej przebudowie rodzica, wszystkie obliczenia powinny być maksymalnie lekkie. Jeśli potrzebujesz wykonać operację zasobożerną — wynieś ją do oddzielnego izolatu (Isolate) lub zapisz wynik w polu State.
Trzecim błędem jest brak wywołania super.initState() i super.dispose(). Nadpisując te metody, programista ma obowiązek wywołać implementację rodzica. Jeśli tego nie zrobi, framework nie będzie mógł poprawnie zarządzać stanem Element, co doprowadzi do trudnych do wykrycia błędów.
mounted przed setState w callbackach asynchronicznychsuper.initState() i super.dispose()Często zadawane pytania
StatefulWidget może zmieniać swój stan przez setState, ma cykl życia (initState, dispose) i tworzy oddzielny obiekt State. StatelessWidget nie może zmieniać stanu i nie ma metod cyklu życia — po prostu wyświetla przekazane dane.
createState jest wywoływane dokładnie raz dla każdej instancji StatefulElement. Nawet jeśli rodzic przebudowuje się wielokrotnie, dopóki typ i Key widgetu się nie zmieniają, createState nie jest wywoływane — używany jest istniejący obiekt State.
Zasoby nie zostaną zwolnione: kontrolery będą działać w tle, subskrypcje strumieni pozostaną aktywne, timery nie zostaną anulowane. Prowadzi to do wycieków pamięci i może powodować wywołanie setState po dispose, co powoduje wyjątek.
Tak, konstruktor StatefulWidget może być const. Nie daje to jednak takiej samej korzyści jak dla StatelessWidget — obiekt State i tak zostanie utworzony przy pierwszym osadzeniu. const wpływa tylko na sam widget (lekką otoczkę), nie na State.
didUpdateWidget jest wywoływany, gdy rodzic przekazuje StatefulWidget z nowymi parametrami. Jest to potrzebne do synchronizacji stanu z nowymi danymi — na przykład, jeśli zmienił się userId w parametrach, należy załadować profil nowego użytkownika.
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ż