Skip to content

Flutter Widgets — Complete Guide to Building UI

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Flutter Widgets. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter widgets are the building blocks of the user interface, forming a hierarchical tree where every visual element from a button to a layout container is a widget.

What You Will Learn

  • The widget tree and how Flutter renders UI
  • StatelessWidget vs StatefulWidget
  • Common Material Design widgets (AppBar, Scaffold, Card, Button)
  • Text, Image, and Icon widgets
  • Input widgets (TextField, Switch, Checkbox)
  • State management with setState
  • Widget lifecycle methods

Why It Matters

In Flutter, everything is a widget. Unlike native Android (XML layouts) or iOS (Storyboards), Flutter uses a single unified widget system for all visual elements. Understanding how widgets compose, how state updates trigger rebuilds, and how the widget lifecycle works is essential for building any Flutter application. Widgets are immutable configurations that are lightweight to create and cheap to rebuild.

Real-World Use

The DodaTech Flutter app's home screen is composed of dozens of nested widgets: a Scaffold with an AppBar, a SingleChildScrollView containing a Column of Card widgets, each with a ListTile containing Text and Icon widgets. When the user taps a card, setState updates the selection state and Flutter efficiently rebuilds only the affected widgets.

Learning Path

flowchart LR
  A[Flutter Setup] --> B[Flutter Widgets\nYou are here]
  B --> C[Flutter Layout]
  style B fill:#f90,color:#fff

Everything is a Widget

In Flutter, the entire UI is composed of widgets nested inside other widgets:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Widgets'),
        ),
        body: Center(
          child: Text('Hello, Widgets!'),
        ),
      ),
    ),
  );
}

MaterialApp is a widget that configures Material Design theming. Scaffold provides the app structure. AppBar, Center, and Text are all widgets. The entire tree is a single composable expression.

StatelessWidget

A StatelessWidget is immutable. It describes a part of the UI that does not change after creation:

import 'package:flutter/material.dart';

class GreetingWidget extends StatelessWidget {
  final String name;

  const GreetingWidget({super.key, required this.name});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      color: Colors.lightBlue.shade50,
      child: Text(
        'Hello, $name!',
        style: TextStyle(
          fontSize: 24,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

// Usage in another widget:
// GreetingWidget(name: 'Alice')

StatelessWidgets receive configuration parameters through their constructor. The build method returns the widget tree. Since the widget has no mutable state, it builds exactly once or when the parent forces a rebuild.

StatefulWidget

A StatefulWidget has mutable state that can change over time. It consists of two classes: the widget (immutable configuration) and the state (mutable):

import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Count: $_count', style: TextStyle(fontSize: 32)),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

setState tells Flutter that the state has changed and triggers a rebuild of the widget. Flutter compares the new widget tree with the old tree and updates only the parts that changed.

Widget Lifecycle

StatefulWidgets follow a lifecycle:

class LifecycleWidget extends StatefulWidget {
  const LifecycleWidget({super.key});

  @override
  State<LifecycleWidget> createState() => _LifecycleWidgetState();
}

class _LifecycleWidgetState extends State<LifecycleWidget> {
  @override
  void initState() {
    super.initState();
    print('1. initState: Widget created');
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies: Dependencies changed');
  }

  @override
  void didUpdateWidget(LifecycleWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget: Widget configuration changed');
  }

  @override
  Widget build(BuildContext context) {
    print('4. build: Building widget tree');
    return Container();
  }

  @override
  void dispose() {
    print('5. dispose: Widget removed from tree');
    super.dispose();
  }
}

The lifecycle order: initState (once), didChangeDependencies (once, then on dependency changes), build (every time the UI needs to update), didUpdateWidget (when parent rebuilds with new config), dispose (when removed).

Common Material Widgets

Flutter provides a rich set of Material Design widgets:

class WidgetShowcase extends StatelessWidget {
  const WidgetShowcase({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Widget Showcase')),
      body: ListView(
        padding: EdgeInsets.all(16),
        children: [
          // Card
          Card(
            child: ListTile(
              leading: Icon(Icons.star, color: Colors.amber),
              title: Text('Featured'),
              subtitle: Text('This is a card with a list tile'),
              trailing: Icon(Icons.chevron_right),
            ),
          ),
          SizedBox(height: 16),
          // Buttons
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () {},
                child: Text('Elevated'),
              ),
              OutlinedButton(
                onPressed: () {},
                child: Text('Outlined'),
              ),
              TextButton(
                onPressed: () {},
                child: Text('Text'),
              ),
            ],
          ),
          SizedBox(height: 16),
          // Input
          TextField(
            decoration: InputDecoration(
              labelText: 'Enter your name',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.person),
            ),
          ),
          SizedBox(height: 16),
          // Switch
          SwitchListTile(
            title: Text('Enable notifications'),
            value: true,
            onChanged: (value) {},
          ),
          SizedBox(height: 16),
          // Chip
          Wrap(
            spacing: 8,
            children: [
              Chip(label: Text('Dart'), avatar: Icon(Icons.code)),
              Chip(label: Text('Flutter'), avatar: Icon(Icons.phone_android)),
              Chip(label: Text('Widgets'), avatar: Icon(Icons.widgets)),
            ],
          ),
        ],
      ),
    );
  }
}

Material widgets follow the Material Design specification out of the box. They include proper elevation, ink splash effects, and Accessibility support.

Text and Styling

The Text widget renders text with configurable styling:

Text(
  'Styled Text Example',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
    letterSpacing: 2.0,
    shadows: [
      Shadow(
        offset: Offset(2, 2),
        blurRadius: 4,
        color: Colors.black26,
      ),
    ],
  ),
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
);

Use Theme.of(context).textTheme to access the app's typography theme instead of hardcoding styles. This ensures consistency and supports accessibility features like font scaling.

Images and Icons

Display images and icons in Flutter:

import 'package:flutter/material.dart';

class MediaWidget extends StatelessWidget {
  const MediaWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Network image
        Image.network(
          'https://picsum.photos/200/150',
          width: 200,
          height: 150,
          fit: BoxFit.cover,
          loadingBuilder: (context, child, loadingProgress) {
            if (loadingProgress == null) return child;
            return Center(
              child: CircularProgressIndicator(
                value: loadingProgress.expectedTotalBytes != null
                    ? loadingProgress.cumulativeBytesLoaded /
                        loadingProgress.expectedTotalBytes!
                    : null,
              ),
            );
          },
          errorBuilder: (context, error, stackTrace) {
            return Icon(Icons.error, color: Colors.red);
          },
        ),
        SizedBox(height: 16),
        // Icons
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Icon(Icons.home, size: 40, color: Colors.blue),
            Icon(Icons.favorite, size: 40, color: Colors.red),
            Icon(Icons.settings, size: 40, color: Colors.grey),
            Icon(Icons.person, size: 40, color: Colors.green),
          ],
        ),
        SizedBox(height: 16),
        // Local image (from assets)
        // Image.asset('assets/images/logo.png'),
      ],
    );
  }
}

Use loadingBuilder for network images to show progress indicators. Use errorBuilder to handle load failures gracefully.

Buttons and Callbacks

Buttons accept callback functions for user interaction:

class ButtonExamples extends StatelessWidget {
  const ButtonExamples({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton(
          onPressed: () {
            print('Elevated button pressed');
          },
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue,
            foregroundColor: Colors.white,
            padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
          ),
          child: Text('Elevated Button'),
        ),
        SizedBox(height: 12),
        IconButton(
          onPressed: () {},
          icon: Icon(Icons.favorite),
          color: Colors.red,
          tooltip: 'Add to favorites',
        ),
        SizedBox(height: 12),
        FloatingActionButton(
          onPressed: () {},
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}

Setting onPressed to null disables the button. Always provide tooltips for IconButton for accessibility.

Input Fields

TextField collects user input:

class InputExample extends StatefulWidget {
  const InputExample({super.key});

  @override
  State<InputExample> createState() => _InputExampleState();
}

class _InputExampleState extends State<InputExample> {
  final _nameController = TextEditingController();
  final _formKey = GlobalKey<FormState>();

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            TextFormField(
              controller: _nameController,
              decoration: InputDecoration(
                labelText: 'Full Name',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your name';
                }
                return null;
              },
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Hello, ${_nameController.text}!')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Always dispose TextEditingController in dispose(). Use Form and TextFormField with validators for form validation.

Common Mistakes

  1. Creating widgets in build method that depend on mutable state: State should be held in StatefulWidget or state management solutions, not recreated in build.

  2. Using setState outside the State class: setState is only available inside State<T>. Callbacks from child widgets should use callback functions.

  3. Not disposing controllers and streams: TextEditingController, AnimationController, and stream subscriptions must be disposed to prevent memory leaks.

  4. Building deep widget trees in a single build method: Break large build methods into smaller private methods or separate widgets.

  5. Using const constructors: Always add const to widgets that do not change. This allows Flutter to reuse the widget instance and skip rebuilding.

Practice Questions

  1. What is the difference between StatelessWidget and StatefulWidget?
  2. How does setState trigger a UI update?
  3. Why must you dispose controllers and subscriptions?
  4. What is the widget lifecycle order in StatefulWidget?
  5. Challenge: Build a login screen with email and password TextField widgets, a login button, and a loading indicator that shows while "logging in". Use setState to toggle between the form and loading state.

Mini Project

Build a profile card app:

  • ProfileCard StatelessWidget showing a name, email, and avatar
  • EditProfileScreen StatefulWidget with text fields for name and email
  • Navigation between screens using Navigator.push
  • Theme switching between light and dark mode
  • Form validation for email format

FAQ

Can I use StatelessWidget with state management?

Yes. State management solutions like Provider, Riverpod, and BLoC hold state outside the widget. The StatelessWidget rebuilds when the observed state changes.

What is the build method?

The build method returns the widget tree. It is called whenever the widget needs to render. For StatefulWidget, it is called after initState and after every setState call.

How does Flutter efficiently rebuild widgets?

Flutter compares the new widget tree with the old one using diffing. Only widgets that changed are rebuilt. This is called the element tree reconciliation.

What are Keys in Flutter?

Keys preserve state when widgets move within the tree. Use Key (value key) or PageStorageKey when the widget position can change.

Can I use HTML-like markup in Flutter?

No. Flutter uses a declarative widget tree expressed in Dart code. There is no HTML or XML markup. Third-party packages provide HTML rendering.

What is Next

Now that you understand widgets, learn about layout and arrangement. Proceed to Flutter Layout for Row, Column, Stack, and other layout widgets. Then explore Flutter Scrolling for ListView and GridView.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro