## Learning Objectives

By the end of this chapter, you will be able to:

- Understand what state is and why it matters in Flutter
- Use setState() correctly for local widget state
- Differentiate between ephemeral and app state
- Implement state lifting patterns to share state between widgets
- Use ValueNotifier and ValueListenableBuilder for simple reactive updates
- Avoid common setState pitfalls and anti-patterns

1

## Understanding State in Flutter

### What is State?

State is any data that can change over time and affects what the user sees. When state changes, the UI needs to rebuild to reflect the new data.

### Types of State

- Ephemeral State: Local to a single widget (e.g., current tab index, text field value, animation progress). Use `setState()`.
- App State: Shared across multiple widgets or persists across screens (e.g., user authentication, shopping cart, theme preference). Requires state management solutions.

Key principle: Keep state as local as possible. Only lift state up when multiple widgets need to access it.

2

## Using setState() for Local State

### Basic setState Pattern

`setState()` tells Flutter that the widget's state has changed and the UI needs to rebuild. It should only be used in StatefulWidget.

### Simple Counter Example

    class CounterWidget extends StatefulWidget {
      @override
      _CounterWidgetState createState() => _CounterWidgetState();
    }

    class _CounterWidgetState extends State {
      int _count = 0;

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

      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Text('Count: $_count'),
            ElevatedButton(
              onPressed: _increment,
              child: Text('Increment'),
            ),
          ],
        );
      }
    }

### setState Best Practices

- Only mutate state variables inside the `setState()` callback
- Keep the callback lightweight; do heavy computation before calling setState
- Don't call setState in `build()` method
- Check `mounted` before setState in async callbacks

3

## State Lifting Pattern

### When to Lift State

When multiple widgets need to access or modify the same state, lift it to their nearest common ancestor.

### Example: Shared Counter

    // Parent widget holds the state
    class CounterScreen extends StatefulWidget {
      @override
      _CounterScreenState createState() => _CounterScreenState();
    }

    class _CounterScreenState extends State {
      int _count = 0;

      void _increment() => setState(() => _count++);
      void _decrement() => setState(() => _count--);

      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            CounterDisplay(count: _count),  // Child reads state
            CounterControls(
              onIncrement: _increment,      // Child modifies state
              onDecrement: _decrement,
            ),
          ],
        );
      }
    }

    // Child widget receives state as parameter
    class CounterDisplay extends StatelessWidget {
      final int count;
      const CounterDisplay({required this.count});

      @override
      Widget build(BuildContext context) {
        return Text('Count: $count');
      }
    }

    // Child widget receives callbacks
    class CounterControls extends StatelessWidget {
      final VoidCallback onIncrement;
      final VoidCallback onDecrement;

      const CounterControls({
        required this.onIncrement,
        required this.onDecrement,
      });

      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            ElevatedButton(onPressed: onDecrement, child: Text('-')),
            ElevatedButton(onPressed: onIncrement, child: Text('+')),
          ],
        );
      }
    }

4

## ValueNotifier and ValueListenableBuilder

### Simple Reactive Updates

For simple state that needs to be shared, `ValueNotifier` provides a lightweight solution without external packages.

### ValueNotifier Example

    class ThemeNotifier {
      final ValueNotifier isDarkMode = ValueNotifier(false);

      void toggleTheme() {
        isDarkMode.value = !isDarkMode.value;
      }

      void dispose() {
        isDarkMode.dispose();
      }
    }

    // Usage in widget
    class SettingsScreen extends StatefulWidget {
      @override
      _SettingsScreenState createState() => _SettingsScreenState();
    }

    class _SettingsScreenState extends State {
      final _themeNotifier = ThemeNotifier();

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

      @override
      Widget build(BuildContext context) {
        return ValueListenableBuilder(
          valueListenable: _themeNotifier.isDarkMode,
          builder: (context, isDark, child) {
            return Switch(
              value: isDark,
              onChanged: (_) => _themeNotifier.toggleTheme(),
            );
          },
        );
      }
    }

### When to Use ValueNotifier

- Simple state that doesn't require complex logic
- State shared between a few related widgets
- Prefer over setState when state needs to be accessed from multiple widgets
- Remember to dispose ValueNotifier to prevent memory leaks

5

## Common setState Pitfalls

### Pitfall 1: Calling setState in build()

Wrong: Calling setState inside build() causes infinite rebuild loops.

    // DON'T DO THIS
    @override
    Widget build(BuildContext context) {
      setState(() { _count++; });  // ❌ Infinite loop!
      return Text('Count: $_count');
    }

### Pitfall 2: setState in Async Callbacks

Wrong: Calling setState after widget is disposed causes errors.

    // DON'T DO THIS
    Future _loadData() async {
      final data = await fetchData();
      setState(() { _items = data; });  // ❌ May be called after dispose
    }

Correct: Check if widget is still mounted.

    // DO THIS
    Future _loadData() async {
      final data = await fetchData();
      if (mounted) {  // ✅ Check before setState
        setState(() { _items = data; });
      }
    }

### Pitfall 3: Heavy Computation in setState

Wrong: Doing heavy work inside setState blocks the UI.

    // DON'T DO THIS
    void _processData() {
      setState(() {
        _result = expensiveComputation();  // ❌ Blocks UI
      });
    }

Correct: Compute before setState.

    // DO THIS
    void _processData() {
      final result = expensiveComputation();  // ✅ Compute first
      setState(() {
        _result = result;  // ✅ Just assign
      });
    }

6

## State Management Decision Guide

### When to Use Each Approach

- setState: Local widget state that doesn't need to be shared (e.g., form field values, toggle states, animation progress)
- State Lifting: State shared between parent and children widgets (e.g., selected item in a list, form state)
- ValueNotifier: Simple shared state between a few widgets (e.g., theme preference, simple counters)
- Provider/State Management Packages: Complex app-wide state, business logic, async operations (covered in Chapter 12)

7

## Practical Example: Todo List

### Complete Example

    class TodoItem {
      final String id;
      final String text;
      bool isCompleted;

      TodoItem({required this.id, required this.text, this.isCompleted = false});
    }

    class TodoScreen extends StatefulWidget {
      @override
      _TodoScreenState createState() => _TodoScreenState();
    }

    class _TodoScreenState extends State {
      final List _todos = [];
      final _textController = TextEditingController();

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

      void _addTodo(String text) {
        if (text.trim().isEmpty) return;
        setState(() {
          _todos.add(TodoItem(
            id: DateTime.now().toString(),
            text: text,
          ));
        });
        _textController.clear();
      }

      void _toggleTodo(String id) {
        setState(() {
          final todo = _todos.firstWhere((t) => t.id == id);
          todo.isCompleted = !todo.isCompleted;
        });
      }

      void _deleteTodo(String id) {
        setState(() {
          _todos.removeWhere((t) => t.id == id);
        });
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('Todo List')),
          body: Column(
            children: [
              Padding(
                padding: EdgeInsets.all(16),
                child: Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: _textController,
                        decoration: InputDecoration(hintText: 'Add todo'),
                        onSubmitted: _addTodo,
                      ),
                    ),
                    IconButton(
                      icon: Icon(Icons.add),
                      onPressed: () => _addTodo(_textController.text),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: ListView.builder(
                  itemCount: _todos.length,
                  itemBuilder: (context, index) {
                    final todo = _todos[index];
                    return ListTile(
                      title: Text(
                        todo.text,
                        style: TextStyle(
                          decoration: todo.isCompleted
                            ? TextDecoration.lineThrough
                            : null,
                        ),
                      ),
                      leading: Checkbox(
                        value: todo.isCompleted,
                        onChanged: (_) => _toggleTodo(todo.id),
                      ),
                      trailing: IconButton(
                        icon: Icon(Icons.delete),
                        onPressed: () => _deleteTodo(todo.id),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        );
      }
    }

8

## Exercises

### 1. Counter with History

Create a counter app that tracks the count history. Display the current count and a list of all previous counts. Add buttons to increment, decrement, and reset. Use setState to manage the state.

### 2. Temperature Converter

Build a temperature converter that converts between Celsius and Fahrenheit. Use state lifting: parent widget holds the temperature value, child widgets display and modify it. Include input validation.

### 3. Shopping Cart with ValueNotifier

Create a simple shopping cart using ValueNotifier to manage cart items. Display items, quantities, and total price. Use ValueListenableBuilder to update the UI when cart changes. Include add/remove functionality.