State Management in Flutter

State is data that changes over time. Flutter gives you several ways to manage it.

setState (Basic)

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

  @override
  State<LikeButton> createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  bool liked = false;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
      color: Colors.red,
      onPressed: () => setState(() => liked = !liked),
    );
  }
}

TextEditingController

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

  @override
  State<NameForm> createState() => _NameFormState();
}

class _NameFormState extends State<NameForm> {
  final controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();  // Always dispose controllers!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: controller,
          decoration: InputDecoration(
            labelText: 'Enter your name',
            border: OutlineInputBorder(),
          ),
        ),
        ElevatedButton(
          onPressed: () {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Hello, ${controller.text}!')),
            );
          },
          child: Text('Submit'),
        ),
      ],
    );
  }
}

FutureBuilder (Async Data)

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<String>>(
      future: fetchUsers(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        final users = snapshot.data!;
        return Column(
          children: users.map((u) => ListTile(title: Text(u))).toList(),
        );
      },
    );
  }
}

When to Use What

Approach Use when
setState Simple local state (toggle, counter, form)
FutureBuilder One-shot async data (API call)
StreamBuilder Continuous data (WebSocket, real-time)
Provider / Riverpod Shared state across many widgets
BLoC Complex business logic with streams

Tips

  • Call dispose() on controllers and streams to prevent memory leaks.
  • Keep state as close to where it's used as possible.
  • Use const constructors when possible.
  • Don't put all state in a single parent — distribute it to child widgets.

Next: navigation.