Widget — to centralne pojęcie frameworka Flutter, opisujące konfigurację elementu interfejsu użytkownika. Każdy wizualny komponent, od przycisku po złożoną animację, jest Widget. W przeciwieństwie do innych frameworków, gdzie UI opisuje się oddzielnymi plikami XML lub maluje się imperatywnie, Flutter buduje interfejs przez kompozycję Widget — łączenie małych niepodzielnych elementów w hierarchiczne drzewo. Według Flutter Documentation (2025), biblioteka Flutter SDK zawiera ponad 260 wbudowanych Widget, obejmujących Material Design, Cupertino i niestandardowe style.
Najważniejsze
Widget we Flutter to niezmienne (immutable) opisanie części interfejsu użytkownika. Każdy Widget zawiera właściwości konfiguracyjne: rozmiar, kolor, położenie, tekst, procedury obsługi zdarzeń oraz Widgety potomne. Same Widgety nie są bezpośrednio renderowane — są przepisami (blueprints), na podstawie których Flutter Engine tworzy RenderObject — rzeczywisty obiekt graficzny na ekranie.
Filozofia Flutter mówi: „Everything is a Widget”. Oznacza to, że nie tylko widoczne elementy (Text, Image, Button) są Widget, ale także strukturalne bloki (Padding, Center, Column, Stack), behawioralne bloki (GestureDetector, AnimatedBuilder), a nawet sama aplikacja (MaterialApp, CupertinoApp). Takie podejście zapewnia jednolitość: każdy element ekranu można łączyć z dowolnym innym poprzez proste zagnieżdżanie.
Według danych Google I/O 2024 — Flutter Widgets Deep Dive, przeciętna aplikacja na Flutter zawiera od 200 do 1500 Widget w jednym momencie. Pomimo takiej liczby, Flutter utrzymuje 60 FPS nawet na budżetowych urządzeniach dzięki optymalizacjom na poziomie C++ Skia/Impeller engine. Widget to lekkie obiekty (40–80 bajtów każdy), więc ich tworzenie nie jest wąskim gardłem wydajności.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Witaj, Flutter!'),
),
),
);
}
}
Aby zrozumieć, jak działa Widget, trzeba poznać architekturę Flutter, składającą się z trzech połączonych drzew. Pierwsze — Widget tree — opisuje konfigurację UI. To lekkie drzewo, które może być całkowicie przebudowywane przy każdej klatce (garbage collector usuwa stare Widgety i tworzy nowe). Widgety są niezmienne (immutable): jeśli zmienił się kolor tekstu — tworzony jest nowy Widget Text z nowym kolorem, stary jest odrzucany.
Drugie drzewo — Element tree — łącznik między Widget a RenderObject. Element zawiera referencję do Widget (konfigurację) i do RenderObject (renderowanie). Gdy Widget się zmienia, Flutter porównuje nowy Widget ze starym Element i decyduje: zaktualizować istniejący RenderObject (jeśli Widget jest tego samego typu) lub utworzyć nowy (jeśli typ Widget się zmienił). Ten proces nazywa się Reconciliation i jest odpowiednikiem React Virtual DOM.
Trzecie drzewo — RenderObject tree — odpowiada za rzeczywiste renderowanie na ekranie. RenderObject zawiera konkretne rozmiary, pozycje i metody paint. Flutter Engine (C++ Skia lub Impeller) przechodzi przez RenderObject tree i renderuje każdy węzeł. RenderObject tree to najcięższe drzewo, dlatego Flutter minimalizuje jego zmiany, ponownie wykorzystując RenderObject przy zmianie Widget tego samego typu.
| Drzewo | Przeznaczenie | Immutable? | Cykl życia |
|---|---|---|---|
| Widget | Konfiguracja UI (przepis) | Tak | Przetwarzany przy każdym build |
| Element | Połączenie Widget ↔ RenderObject | Nie | Istnieje, dopóki widget jest w drzewie |
| RenderObject | Renderowanie i layout | Nie | Ciężki, ponownie wykorzystywany gdy to możliwe |
Flutter dzieli Widget na dwa fundamentalne typy: StatelessWidget i StatefulWidget. StatelessWidget to widget, który nie zawiera zmiennego stanu. Wygląd StatelessWidget jest całkowicie określony przez jego konstruktor i nie może się zmienić po renderowaniu. Przykłady: Text, Icon, Divider, Padding. Wszystkie właściwości StatelessWidget są deklarowane jako final w konstruktorze i dostępne tylko do odczytu.
StatefulWidget to widget ze zmiennym stanem. Składa się z dwóch klas: samego Widget (niezmienna konfiguracja, tak jak StatelessWidget) i State (zmienny stan). Oddzielenie Widget od State to kluczowa decyzja architektoniczna Flutter. Widget jest przetwarzany przy każdej kompilacji (build), ale obiekt State pozostaje żywy przez cały cykl życia widgetu w drzewie, zachowując swój stan.
Gdy wywoływana jest metoda setState(), Flutter oznacza State jako „brudny” i w następnej klatce wywołuje metodę build() w celu przebudowania poddrzewa. Ważne: setState() nie przetwarza samego Widget — tylko wyzwala wywołanie build() w istniejącym State. Oznacza to, że StatefulWidget może aktualizować UI bez utraty stanu Widgetów potomnych, dopóki klucze (Key) elementów potomnych pozostają stabilne.
// StatelessWidget — wygląd nigdy się nie zmienia
class GreetingWidget extends StatelessWidget {
const GreetingWidget({super.key, required this.name});
final String name;
@override
Widget build(BuildContext context) {
return Text('Witaj, $name');
}
}
// StatefulWidget — licznik ze zmiennym stanem
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Licznik: $_count'),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Zwiększ'),
),
],
);
}
}
Wszystkie Widget we Flutter można podzielić na trzy główne kategorie według przeznaczenia funkcjonalnego. Layout Widget — odpowiadają za rozmieszczenie elementów potomnych na ekranie. Row i Column umieszczają dzieci w linii, Stack nakłada jedno na drugie, Expanded i Flexible rozdzielają dostępne miejsce. Layout Widget nie mają własnej reprezentacji wizualnej — zarządzają położeniem i rozmiarem widgetów potomnych.
Painting Widget — odpowiadają za oprawę wizualną. Container łączy dekoracje (kolor, gradient, cień, obramowanie) z właściwościami layout. Padding dodaje odstępy, DecoratedBox rysuje tło, Transform stosuje transformacje (obrót, skalowanie). Painting Widget są elementami składowymi stylu wizualnego i często są używane w parze z layout Widget, aby uzyskać pożądany wygląd.
Interactive Widget — obsługują dane wejściowe użytkownika. GestureDetector rozpoznaje gesty (tap, swipe, pinch), InkWell dodaje efekt ripple Material, TextField przyjmuje tekst, Slider i Switch dostarczają standardowe elementy sterujące. Interactive Widget podnoszą zdarzenia przez funkcje callback, które są przekazywane do konstruktora lub obsługiwane przez dostawców stanu.
| Kategoria | Przykłady Widget | Przeznaczenie |
|---|---|---|
| Layout | Row, Column, Stack, Expanded, Flexible, Align | Rozmieszczenie i rozmiar elementów potomnych |
| Painting | Container, Padding, DecoratedBox, RotatedBox | Kolor, tło, obramowania, cienie, transformacje |
| Interactive | GestureDetector, InkWell, TextField, Slider | Obsługa dotyku, danych wejściowych, gestów |
| Platform | MaterialApp, CupertinoApp, Theme, MediaQuery | Integracja z platformą, motywy, adaptacja |
| Async | FutureBuilder, StreamBuilder, ValueListenableBuilder | Reaktywna aktualizacja z danych asynchronicznych |
Według danych Flutter Widget of the Week (Google, 2025), społeczność Flutter aktywnie używa kombinacji layout + painting + interactive Widget do budowania praktycznie każdego interfejsu. Na przykład przycisk: InkWell (interactive) + Container (painting) + Text (static) + Padding (layout). Taka modułowość pozwala ponownie wykorzystywać standardowe bloki w różnych kontekstach bez powielania kodu.
Kompozycja Widget to proces budowania UI poprzez zagnieżdżanie jednych Widget w innych. W przeciwieństwie do klasycznego dziedziczenia (extends), gdzie klasa potomna dziedziczy zachowanie rodzica, Flutter używa agregacji: każdy Widget zawiera inne Widgety przez parametr child (dla jednego) lub children (dla wielu). Takie podejście daje większą elastyczność i możliwość ponownego wykorzystania.
BuildContext to drugi najważniejszy koncept po Widget. BuildContext to deskryptor pozycji Widget w drzewie elementów. Poprzez BuildContext Widget może uzyskać dostęp do widgetów przodków (Theme.of(context), MediaQuery.of(context), Navigator.of(context)). BuildContext jest przekazywany do metody build() i używany do interakcji z elementami nadrzędnymi i potomnymi. Każdy Widget ma dokładnie jeden BuildContext, który jednoznacznie identyfikuje jego pozycję w drzewie.
Według danych Flutter Architectural Overview (Google, 2025), BuildContext jest podstawą dla InheritedWidget — mechanizmu, który pozwala przekazywać dane w dół drzewa bez jawnego przekazywania przez konstruktory. Theme, MediaQuery, Navigator i Provider używają InheritedWidget pod maską. Każdy głęboko zagnieżdżony Widget może uzyskać dostęp do danych przodka przez BuildContext.dependOnInheritedWidgetOfExactType, co czyni BuildContext kluczem do reaktywnej architektury Flutter.
// Kompozycja Widget przez zagnieżdżanie
Scaffold(
appBar: AppBar(title: const Text('Moja Aplikacja')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Witaj we Flutter',
style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () { /* nav */ },
child: const Text('Rozpocznij'),
),
],
),
),
)
// Dostęp do motywów przez BuildContext
Text(
'Stylowany Tekst',
style: Theme.of(context).textTheme.headlineMedium,
)
Pierwszy i najczęstszy błąd — używanie StatefulWidget tam, gdzie wystarczy StatelessWidget. Wielu początkujących programistów Flutter tworzy StatefulWidget dla wszystkich widgetów, nawet jeśli stan jest przechowywany w zewnętrznym dostawcy (Provider, Riverpod, BLoC). To zbędne i pogarsza wydajność. Zasada: używaj StatelessWidget, jeśli stan jest zarządzany z zewnątrz lub jeśli widget nie ma własnego zmiennego stanu.
Drugi błąd — tworzenie Widget wewnątrz metody build bez const-konstruktora. Każdy Widget utworzony bez const jest alokowany na nowo przy każdej kompilacji. Jeśli wewnątrz build() tworzyć Widget z const-konstruktorem, Flutter może ponownie wykorzystać ten sam egzemplarz, zmniejszając obciążenie garbage collectora. Dodawaj const wszędzie, gdzie to możliwe — szczególnie dla Text, Icon, SizedBox, Padding i innych stateless Widget.
Trzeci błąd — nieprawidłowa praca z kluczami (Key). Flutter używa Key do identyfikacji Widget podczas przebudowy drzewa. Jeśli lista Widget jest przebudowywana bez Key, Flutter może pomylić kolejność elementów, co prowadzi do nieprawidłowej animacji lub utraty stanu. Zawsze dodawaj Key (np. ValueKey lub ObjectKey) dla elementów na listach, szczególnie przy użyciu ListView.builder z dynamicznymi danymi.
Często zadawane pytania
StatelessWidget — widget bez zmiennego stanu, jego wygląd jest całkowicie określony przez konstruktor. StatefulWidget — widget ze zmiennym stanem, który jest przechowywany w oddzielnym obiekcie State i może być aktualizowany przez setState() bez ponownego tworzenia samego widgetu. Używaj StatelessWidget wszędzie, gdzie to możliwe, StatefulWidget — gdy wymagany jest stan lokalny.
Widget immutable to architektoniczna decyzja Flutter dla wydajności. Gdyby Widget były mutable, Flutter nie mógłby bezpiecznie porównywać starej i nowej konfiguracji przy każdej kompilacji. Immutability pozwala Flutter szybko określić, czy Widget się zmienił (przez operator ==), i ponownie wykorzystać istniejący RenderObject, minimalizując kosztowne operacje renderowania.
BuildContext to deskryptor pozycji Widget w drzewie elementów. Przez niego Widget uzyskuje dostęp do widgetów przodków (Theme, MediaQuery, Navigator) i InheritedWidget. BuildContext jest również używany do nawigacji (Navigator.of(context)), wyświetlania SnackBar i interakcji z Provider. Każdy Widget otrzymuje BuildContext przez metodę build() i przekazuje go następcom.
Używaj Row do poziomego rozmieszczenia elementów, Column — do pionowego, Stack — do nakładania elementów na siebie. Row i Column działają na zasadzie flexbox: children zajmują miejsce zgodnie z mainAxisSize, mainAxisAlignment i crossAxisAlignment. Stack używa positioned-children do dokładnego pozycjonowania względem krawędzi lub środka.
Flutter osiąga wysoką wydajność dzięki trzem mechanizmom: (1) Widget are cheap — lekkie immutable obiekty (40–80 bajtów), ich tworzenie nie obciąża GC. (2) RenderObject reuse — przy zmianie Widget tego samego typu RenderObject jest ponownie wykorzystywany, unikając kosztownego odtwarzania. (3) Skia/Impeller engine — renderowanie na poziomie C++ z minimalizacją wywołań rysowania przez repaint boundaries.
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ż