Lesson 17 of 51 beginner

StatelessWidget

Simple Widgets That Never Change Their Mind

Open interactive version (quiz + challenge)

Real-world analogy

A StatelessWidget is like a printed poster on a wall. Once it is printed, the content does not change. If you want different text, you print a new poster and replace the old one. A StatelessWidget describes UI based only on the data it receives through its constructor -- it has no internal memory. In team_mvp_kit, many UI components like labels, icons, cards, and display widgets are stateless because they just render whatever data is passed to them.

What is it?

A StatelessWidget is a widget that describes part of the UI based solely on its configuration (constructor parameters) and the BuildContext. It has no mutable state -- once created, it cannot change itself. When the parent provides new data, Flutter creates a new instance and calls build again. StatelessWidgets are the workhorse of Flutter apps: simple, predictable, and efficient. They are easy to test because given the same inputs, they always produce the same output.

Real-world relevance

In team_mvp_kit, many custom widgets are stateless. Display components like UserAvatar, ErrorMessage, LoadingIndicator, and EmptyState are StatelessWidgets because they just render data they receive. Even screens that show dynamic data can be StatelessWidgets when paired with BlocBuilder -- the BLoC owns the state, and the widget just renders whatever state it receives. This separation of concerns is a key benefit of Clean Architecture: widgets stay simple and testable.

Key points

Code example

import 'package:flutter/material.dart';

class UserProfileCard extends StatelessWidget {
  final String name;
  final String email;
  final String? avatarUrl;
  final VoidCallback? onTap;

  const UserProfileCard({
    super.key,
    required this.name,
    required this.email,
    this.avatarUrl,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      elevation: 2,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              CircleAvatar(
                radius: 28,
                backgroundImage: avatarUrl != null
                    ? NetworkImage(avatarUrl!)
                    : null,
                child: avatarUrl == null
                    ? Text(
                        name[0].toUpperCase(),
                        style: theme.textTheme.titleLarge,
                      )
                    : null,
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      name,
                      style: theme.textTheme.titleMedium,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      email,
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                      ),
                    ),
                  ],
                ),
              ),
              if (onTap != null)
                const Icon(Icons.chevron_right),
            ],
          ),
        ),
      ),
    );
  }
}

Line-by-line walkthrough

  1. 1. Import the Flutter material library
  2. 2. Define UserProfileCard extending StatelessWidget
  3. 3. Declare final String name -- the user's display name, required
  4. 4. Declare final String email -- the user's email, required
  5. 5. Declare final String? avatarUrl -- optional avatar image URL
  6. 6. Declare final VoidCallback? onTap -- optional tap handler for navigation
  7. 7. Const constructor with super.key and named parameters
  8. 8. Override the build method, receiving BuildContext
  9. 9. Get the current theme for consistent styling
  10. 10. Return a Card widget with elevation for a shadow effect
  11. 11. Wrap content in InkWell for a tap ripple effect, only active if onTap is provided
  12. 12. Set borderRadius on InkWell to match the card's rounded corners
  13. 13. Add Padding of 16 pixels on all sides
  14. 14. Use a Row to lay out avatar, text, and chevron horizontally
  15. 15. Create a CircleAvatar with radius 28 for the profile picture
  16. 16. If avatarUrl is not null, use NetworkImage to load the avatar
  17. 17. If avatarUrl is null, show the first letter of the name as fallback
  18. 18. Add a SizedBox with width 16 for spacing between avatar and text
  19. 19. Wrap the text Column in Expanded so it takes remaining horizontal space
  20. 20. Set crossAxisAlignment to start so text aligns left
  21. 21. Display the name with titleMedium text style
  22. 22. Add a small SizedBox for vertical spacing between name and email
  23. 23. Display email with bodyMedium style in a muted color
  24. 24. Conditionally show a chevron_right icon only if onTap is provided

Spot the bug

class NameTag extends StatelessWidget {
  String name;

  NameTag({required this.name});

  @override
  Widget build(BuildContext context) {
    name = name.toUpperCase();
    return Text(name);
  }
}
Need a hint?
StatelessWidget properties should be immutable. What two things violate this rule?
Show answer
Two bugs: (1) name is not final -- StatelessWidget properties must be final because widgets are immutable. (2) The build method tries to modify name with name = name.toUpperCase(), but you should never mutate widget properties. Fix: make name final and compute the uppercase version in a local variable: final display = name.toUpperCase(); then use display in Text.

Explain like I'm 5

Imagine a name tag at a party. Someone writes your name on it and sticks it to your shirt. The name tag does not think or change on its own -- it just shows whatever was written on it. If you want a different name, you need a whole new name tag. That is a StatelessWidget. It gets information when it is created (your name), shows it (the text on the tag), and that is it. If the information needs to change, the old name tag is tossed and a new one is made. Simple, right?

Fun fact

The Flutter team at Google recommends that about 80% of your custom widgets should be StatelessWidgets. When you use state management solutions like BLoC, Provider, or Riverpod, even screens with dynamic data are rendered by stateless widgets that subscribe to external state. The fewer StatefulWidgets you have, the easier your app is to debug and test.

Hands-on challenge

Build a reusable StatelessWidget called InfoCard that takes: title (String), description (String), icon (IconData), and an optional onTap (VoidCallback?). It should display as a Card with a ListTile-like layout: icon on the left, title and description stacked on the right, and a chevron arrow on the right if onTap is provided. Use Theme.of(context) for styling. Then create a screen that shows three InfoCard widgets in a Column.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart