Widget — co to jest, rodzaje i kompozycja we Flutter

Autor: IT Sectr Opublikowano: 2026-06-30 Czas czytania: 10 min

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 — podstawowy element budowlany UI we Flutter, opisujący konfigurację elementu.
  • Kompozycja — UI buduje się przez zagnieżdżanie Widget w sobie nawzajem, a nie przez dziedziczenie.
  • StatelessWidget — widget, który nie zmienia się po renderowaniu (tekst, ikona, odstęp).
  • StatefulWidget — widget ze zmiennym stanem (formularze, animacje, listy z danymi).
  • Element tree — Flutter utrzymuje trzy drzewa: Widget, Element i RenderObject.

Czym jest Widget we Flutter

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.

dart
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!'),
        ),
      ),
    );
  }
}

Trzy drzewa Flutter: Widget, Element i RenderObject

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.

DrzewoPrzeznaczenieImmutable?Cykl życia
WidgetKonfiguracja UI (przepis)TakPrzetwarzany przy każdym build
ElementPołączenie Widget ↔ RenderObjectNieIstnieje, dopóki widget jest w drzewie
RenderObjectRenderowanie i layoutNieCiężki, ponownie wykorzystywany gdy to możliwe

StatelessWidget vs StatefulWidget

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.

dart
// 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'),
        ),
      ],
    );
  }
}

Rodzaje Widget: layout, painting, interactive

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.

KategoriaPrzykłady WidgetPrzeznaczenie
LayoutRow, Column, Stack, Expanded, Flexible, AlignRozmieszczenie i rozmiar elementów potomnych
PaintingContainer, Padding, DecoratedBox, RotatedBoxKolor, tło, obramowania, cienie, transformacje
InteractiveGestureDetector, InkWell, TextField, SliderObsługa dotyku, danych wejściowych, gestów
PlatformMaterialApp, CupertinoApp, Theme, MediaQueryIntegracja z platformą, motywy, adaptacja
AsyncFutureBuilder, StreamBuilder, ValueListenableBuilderReaktywna 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 i BuildContext

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.

dart
// 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,
)

Typowe błędy z Widget

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

Jaka jest różnica między StatelessWidget a StatefulWidget?

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.

Dlaczego Widget nazywane są immutable?

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.

Czym jest BuildContext i do czego służy?

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.

Jak wybrać między Row, Column a Stack dla layout?

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.

Jak Flutter utrzymuje 60 FPS z tysiącami Widget?

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

  • Widget — immutable konfiguracja części UI we Flutter, podstawowy element budowlany, opisujący wygląd i zachowanie.
  • Trzy drzewa — Flutter używa Widget tree (konfiguracja), Element tree (połączenie), RenderObject tree (renderowanie) dla optymalnego wyświetlania.
  • Stateless vs Stateful — StatelessWidget bez stanu, StatefulWidget ze zmiennym State i metodą setState() do aktualizacji UI.
  • Kompozycja — UI buduje się przez zagnieżdżanie Widget przez child/children, bez dziedziczenia, co zapewnia elastyczność i możliwość ponownego wykorzystania.
  • BuildContext — deskryptor pozycji Widget w drzewie do dostępu do Theme, Navigator, MediaQuery i InheritedWidget.
  • Wydajność — Widget są przetwarzane przy każdym build (60 FPS), ale RenderObject jest ponownie wykorzystywany przy zgodności typu.
  • Kategorie — Widget dzielą się na layout (Row, Column, Stack), painting (Container, Padding), interactive (GestureDetector, TextField) i platform (MaterialApp, Theme).

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ż