Flutter Widgets Fundamentals

StatelessWidget in Depth

45 min Lesson 1 of 18

What Is a StatelessWidget?

In Flutter, everything you see on screen is a widget. A StatelessWidget is a widget that does not change once it is built. It has no mutable state — meaning its appearance and behavior are determined entirely by the configuration passed to it through its constructor. Once Flutter calls the build() method, the widget renders and remains the same until the parent rebuilds it with new parameters.

Think of a StatelessWidget as a pure function of its inputs. Given the same constructor arguments, it always produces the same UI output. This makes it predictable, easy to test, and highly efficient.

Note: StatelessWidget does not mean the screen never changes. It means the widget itself does not manage internal state. The parent can still rebuild it with different parameters, causing it to display differently.

The build() Method

Every StatelessWidget must override the build() method. This method receives a BuildContext and returns a widget tree that describes what should appear on screen.

Basic StatelessWidget

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return const Text(
      'Welcome to Flutter!',
      style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
    );
  }
}

The BuildContext parameter gives the widget access to its position in the widget tree. You can use it to look up inherited widgets, themes, media queries, and more. We will explore this in later lessons.

Immutability — Why It Matters

All fields in a StatelessWidget must be final. This is enforced by the Dart analyzer. Because the widget is immutable, Flutter can safely cache and reuse it, which improves performance.

Immutable Fields

class UserBadge extends StatelessWidget {
  final String username;
  final int level;

  const UserBadge({
    super.key,
    required this.username,
    required this.level,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Icon(Icons.person, size: 20),
        const SizedBox(width: 8),
        Text('\$username (Lv. \$level)'),
      ],
    );
  }
}
Warning: Never declare a non-final field in a StatelessWidget. The analyzer will flag it, and it violates Flutter’s contract that stateless widgets are immutable. If you need mutable state, use a StatefulWidget instead.

When to Use StatelessWidget

Use a StatelessWidget when your widget:

  • Displays static content that depends only on constructor parameters
  • Does not need to track user interactions internally
  • Does not need lifecycle callbacks like initState or dispose
  • Does not call setState

Common real-world examples include: labels, icons, static cards, display-only list items, dividers, decorative elements, and layout wrappers.

Constructor Parameters & Named Parameters

Flutter widgets use Dart’s named parameters extensively. The required keyword ensures callers provide essential values, while optional parameters can have defaults.

Required and Optional Parameters

class ProductCard extends StatelessWidget {
  final String name;
  final double price;
  final String? imageUrl;
  final bool showBorder;

  const ProductCard({
    super.key,
    required this.name,
    required this.price,
    this.imageUrl,
    this.showBorder = true,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        border: showBorder
            ? Border.all(color: Colors.grey.shade300)
            : null,
        borderRadius: BorderRadius.circular(12),
      ),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (imageUrl != null)
            Image.network(imageUrl!, height: 120, fit: BoxFit.cover),
          const SizedBox(height: 8),
          Text(name, style: const TextStyle(
            fontSize: 18, fontWeight: FontWeight.w600,
          )),
          Text('\\$\${price.toStringAsFixed(2)}',
            style: TextStyle(
              fontSize: 16, color: Colors.green.shade700,
            ),
          ),
        ],
      ),
    );
  }
}
Tip: Always mark your constructors as const when possible. This allows Flutter to optimize widget rebuilds by reusing identical widget instances. The super.key pattern (introduced in Dart 2.17) is the modern way to forward the key parameter.

const Constructors & Performance

When all fields in a widget are compile-time constants, you can declare the constructor as const. This tells Dart to create the widget at compile time rather than runtime, which has two key benefits:

  • Memory efficiency: Identical const widgets share the same instance in memory
  • Rebuild optimization: Flutter skips rebuilding const widgets because it knows they haven’t changed

Using const Constructors

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

  @override
  Widget build(BuildContext context) {
    return const Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(Icons.flutter_dash, size: 64, color: Colors.blue),
        SizedBox(height: 8),
        Text(
          'My Flutter App',
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Colors.blue,
          ),
        ),
      ],
    );
  }
}

// Usage - both are identical instances in memory
const logo1 = AppLogo();
const logo2 = AppLogo(); // Same instance as logo1

Widget Composition

Flutter encourages building complex UIs by composing small, focused widgets together rather than creating one large widget. This is called widget composition, and it is one of Flutter’s most powerful patterns.

Composing Widgets Together

class InfoRow extends StatelessWidget {
  final IconData icon;
  final String label;
  final String value;

  const InfoRow({
    super.key,
    required this.icon,
    required this.label,
    required this.value,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(icon, size: 20, color: Colors.blue),
          const SizedBox(width: 12),
          Text(label, style: const TextStyle(
            fontWeight: FontWeight.w500, color: Colors.grey,
          )),
          const Spacer(),
          Text(value, style: const TextStyle(
            fontWeight: FontWeight.w600,
          )),
        ],
      ),
    );
  }
}

class UserProfile extends StatelessWidget {
  final String name;
  final String email;
  final String phone;
  final String location;

  const UserProfile({
    super.key,
    required this.name,
    required this.email,
    required this.phone,
    required this.location,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(name, style: const TextStyle(
              fontSize: 22, fontWeight: FontWeight.bold,
            )),
            const Divider(height: 24),
            InfoRow(icon: Icons.email, label: 'Email', value: email),
            InfoRow(icon: Icons.phone, label: 'Phone', value: phone),
            InfoRow(icon: Icons.location_on, label: 'Location', value: location),
          ],
        ),
      ),
    );
  }
}
Tip: If your build() method is longer than ~50 lines, consider extracting parts into separate widgets. This improves readability, reusability, and performance (Flutter can skip rebuilding unchanged sub-widgets).

Practical Example: Greeting Card Widget

Let’s build a complete, reusable greeting card widget that demonstrates all the concepts we have covered.

Greeting Card Widget

class GreetingCard extends StatelessWidget {
  final String recipientName;
  final String message;
  final Color backgroundColor;
  final bool showIcon;

  const GreetingCard({
    super.key,
    required this.recipientName,
    required this.message,
    this.backgroundColor = Colors.white,
    this.showIcon = true,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: backgroundColor,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 10,
            offset: const Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (showIcon)
            const Icon(Icons.celebration, size: 40, color: Colors.amber),
          if (showIcon)
            const SizedBox(height: 12),
          Text(
            'Hello, \$recipientName!',
            style: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),
          Text(
            message,
            style: TextStyle(
              fontSize: 16,
              color: Colors.grey.shade700,
              height: 1.5,
            ),
          ),
        ],
      ),
    );
  }
}

// Usage
GreetingCard(
  recipientName: 'Ahmed',
  message: 'Wishing you a wonderful day!',
  backgroundColor: Colors.blue.shade50,
)

Practical Example: Reusable Label Widget

Labels and tags are common UI elements. Here is a flexible, reusable label widget:

Reusable Label Widget

class StatusLabel extends StatelessWidget {
  final String text;
  final Color color;
  final IconData? icon;
  final double fontSize;

  const StatusLabel({
    super.key,
    required this.text,
    this.color = Colors.blue,
    this.icon,
    this.fontSize = 12,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: color.withOpacity(0.15),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: color.withOpacity(0.3)),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (icon != null) ...[
            Icon(icon, size: fontSize + 2, color: color),
            const SizedBox(width: 4),
          ],
          Text(
            text,
            style: TextStyle(
              fontSize: fontSize,
              fontWeight: FontWeight.w600,
              color: color,
            ),
          ),
        ],
      ),
    );
  }
}

// Usage examples
const StatusLabel(text: 'Active', color: Colors.green)
const StatusLabel(text: 'Pending', color: Colors.orange, icon: Icons.schedule)
const StatusLabel(text: 'Error', color: Colors.red, icon: Icons.error_outline)

Practical Example: Info Display Widget

A common pattern in apps is displaying key-value information in a structured layout:

Info Display Widget

class InfoDisplay extends StatelessWidget {
  final String title;
  final List<MapEntry<String, String>> items;
  final IconData? headerIcon;

  const InfoDisplay({
    super.key,
    required this.title,
    required this.items,
    this.headerIcon,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: Colors.grey.shade200),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              if (headerIcon != null) ...[
                Icon(headerIcon, color: Colors.blue),
                const SizedBox(width: 8),
              ],
              Text(title, style: const TextStyle(
                fontSize: 18, fontWeight: FontWeight.bold,
              )),
            ],
          ),
          const Divider(height: 24),
          ...items.map((entry) => Padding(
            padding: const EdgeInsets.symmetric(vertical: 4),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(entry.key, style: TextStyle(
                  color: Colors.grey.shade600,
                )),
                Text(entry.value, style: const TextStyle(
                  fontWeight: FontWeight.w500,
                )),
              ],
            ),
          )),
        ],
      ),
    );
  }
}

// Usage
InfoDisplay(
  title: 'Order Summary',
  headerIcon: Icons.receipt_long,
  items: [
    const MapEntry('Subtotal', '\\$45.00'),
    const MapEntry('Tax', '\\$3.60'),
    const MapEntry('Total', '\\$48.60'),
  ],
)

Summary

In this lesson, you learned:

  • StatelessWidget is an immutable widget whose output depends only on its constructor parameters
  • The build() method returns a widget tree and receives a BuildContext
  • All fields must be final to enforce immutability
  • Use const constructors for better performance and memory efficiency
  • Widget composition is the preferred way to build complex UIs from simple parts
  • Use required and optional named parameters for flexible widget APIs
What’s Next: In the next lesson, we will explore StatefulWidget and its lifecycle methods, which allow widgets to manage internal state and respond to changes over time.