Flutter StatelessWidget vs StatefulWidget: In-Depth Understanding

Understanding the difference between StatelessWidget and StatefulWidget is fundamental to Flutter development.

This section will deeply analyze the usage scenarios and internal working mechanisms of these two types of Widgets.


Core difference

In Flutter, there are two main types of Widgets, and their fundamental difference lies in whether they can hold mutable state.

FeaturesStatelessWidgetStatefulWidget
StateImmutablemutable
Rebuild timingOnly when the parent Widget rebuildsWhen state changes + parent Widget rebuilds
PerformanceHigher (no need to listen to state)Slightly lower (requires state management)
Use CaseStatic contentDynamic interactive content

StatelessWidget Detailed Explanation

StatelessWidget is suitable for content that does not change over time. Once created, all its properties are final and cannot be modified.

Use Case

  • Display static text, images, icons
  • Displaying a data list (data source comes from the parent Widget)
  • Layout containers without user interaction
  • Read-only display sections in forms

Example: Complete StatelessWidget example

import 'package:flutter/material.dart';

// User info display component (stateless)
class UserCard extends StatelessWidget {
  // Properties are defined as final to ensure immutability
  final String name;
  final String email;
  final String avatarUrl;

  // const constructor improves performance
  const UserCard({
    super.key,
    required this.name,
    required this.email,
    this.avatarUrl = '',
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          children: [
            // Avatar
            CircleAvatar(
              backgroundImage: avatarUrl.isNotEmpty
                  ? NetworkImage(avatarUrl)
                  : null,
              child: avatarUrl.isEmpty
                  ? const Icon(Icons.person)
                  : null,
            ),
            const SizedBox(width: 16),
            // User info
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    name,
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    email,
                    style: TextStyle(
                      color: Colors.grey[600],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

StatefulWidget Detailed Explanation

StatefulWidget is suitable for scenarios that need to respond to user interactions or data changes. It consists of two parts: the Widget itself (immutable) and the State object (mutable).

Use Case

  • Dynamic data such as counters, timers
  • Form input
  • Animation Control
  • Network request status display
  • User interaction response

State object lifecycle

MethodsDescription
initStateCalled when the Widget is first created, used to initialize state.
didChangeDependenciesCalled when the Widget's dependencies change.
buildBuilds the UI, called every time the state changes.
didUpdateWidgetCalled when the parent Widget rebuilds
disposeCalled when the Widget is removed, used to clean up resources.

Example: Complete StatefulWidget example

import 'package:flutter/material.dart';

// Counter app
class CounterApp extends StatefulWidget {
  const CounterApp({super.key});

  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  // Counter state
  int _counter = 0;
  // Timer (to demonstrate dispose)
  Timer? _timer;

  // 1. initState - Initialize state
  @override
  void initState() {
    super.initState();
    // Initialize counter
    _counter = 0;
    // Start a timer (increment every second)
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _increment();
    });
  }

  // 2. didChangeDependencies - Called when dependencies change
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }

  // 3. build - Build UI
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Current count:',
              style: TextStyle(fontSize: 20),
            ),
            Text(
              '$_counter',
              style: const TextStyle(
                fontSize: 48,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
      // Increment button
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }

  // Method to increment the counter
  void _increment() {
    // Must call setState to trigger UI update
    setState(() {
      _counter++;
    });
  }

  // 4. didUpdateWidget - Called when parent widget rebuilds
  @override
  void didUpdateWidget(CounterApp oldWidget) {
    super.didUpdateWidget(oldWidget);
  }

  // 5. dispose - Clean up resources
  @override
  void dispose() {
    // Cancel timer to prevent memory leaks
    _timer?.cancel();
    super.dispose();
  }
}

Correct usage of setState

In StatefulWidget, you must notify Flutter of state changes through setState.

Correct Usage

Example: Correct setState usage

// Correct: update state in setState
void _increment() {
  setState(() {
    _counter++;
  });
}

// Correct: When state update is unrelated to UI, you don't need setState
void _someInternalLogic() {
  // Internal logic, does not affect UI
  _calculateSomething();
}

// Correct: Update state after async operation
Future<void> _loadData() async {
  final data = await fetchData();
  setState(() {
    _items = data;
  });
}

Wrong usage

Example: Incorrect setState usage

// Wrong: Modifying state without calling setState
void _increment() {
  _counter++;  // State has changed, but the UI won't update!
}

// Wrong: Calling setState in the build method
@override
Widget build(BuildContext context) {
  // Never call setState in build!
  setState(() {
    _counter++;  // This leads to an infinite loop
  });
  return Text('$_counter');
}

setState will trigger a re-invocation of the build method during the next frame rendering.

Frequent calls to setState or calling setState within the build method can both lead to performance issues.


How to choose

During development, you should choose the appropriate Widget type based on actual needs:

Choose StatelessWidget when

  • The Widget's appearance does not depend on any mutable state.
  • All required data is passed in from the parent Widget via the constructor.
  • The Widget does not need to respond to user interactions.
  • Pursuing optimal performance

Choose StatefulWidget when

  • The Widget needs to maintain internal state.
  • Needs to respond to user input (clicks, swipes, etc.)
  • Needs to respond to data changes (e.g., network request responses)
  • Need to control animations
other extensions