# Flutter State

> When to activate: Flutter state management, setState, ValueNotifier, ChangeNotifier, BLoC, Riverpod, Provider, state comparison

- Skill: `mattakushi432/flutter-state` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/flutter-state`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/flutter-state/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/flutter-state

---

# Flutter State Management

## Decision Guide

| Scope | Solution |
|-------|----------|
| Local UI state (expand/collapse, tab index) | `setState` |
| Single value shared across siblings | `ValueNotifier` + `ValueListenableBuilder` |
| Domain model shared across features | `ChangeNotifier` + `ListenableBuilder` |
| Complex app state, async, DI | Riverpod (recommended) or BLoC |
| Legacy apps | `provider` package |

## setState — Local Only

```dart
class ToggleButton extends StatefulWidget {
  const ToggleButton({super.key});
  @override
  State<ToggleButton> createState() => _ToggleButtonState();
}

class _ToggleButtonState extends State<ToggleButton> {
  bool _on = false;

  @override
  Widget build(BuildContext context) => Switch(
    value: _on,
    onChanged: (v) => setState(() => _on = v),
  );
}
```

## ValueNotifier — Lightweight Observable

```dart
final counter = ValueNotifier<int>(0);

// Widget
ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, _) => Text('$value'),
);

// Mutate
counter.value++;

// Dispose when done (in State.dispose or Provider)
counter.dispose();
```

## ChangeNotifier — Multiple Fields

```dart
class CartNotifier extends ChangeNotifier {
  final List<Item> _items = [];
  List<Item> get items => List.unmodifiable(_items);
  int get count => _items.length;
  double get total => _items.fold(0, (s, i) => s + i.price);

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(Item item) {
    _items.remove(item);
    notifyListeners();
  }
}

// With provider package
ChangeNotifierProvider(create: (_) => CartNotifier()),

// Usage
ListenableBuilder(
  listenable: cart,
  builder: (context, _) => Text('${cart.count} items'),
);
```

## Riverpod — Recommended

```dart
// Define
@riverpod
class Cart extends _$Cart {
  @override
  List<Item> build() => [];

  void add(Item item) => state = [...state, item];
  void remove(Item item) => state = state.where((i) => i.id != item.id).toList();
}

// Consume
class CartIcon extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(cartProvider).length;
    return Badge(label: Text('$count'), child: const Icon(Icons.shopping_cart));
  }
}
```

## BLoC — Event-Driven

```dart
// Events
sealed class CartEvent {}
class AddItemEvent extends CartEvent { final Item item; AddItemEvent(this.item); }
class RemoveItemEvent extends CartEvent { final Item item; RemoveItemEvent(this.item); }

// State
class CartState { final List<Item> items; const CartState(this.items); }

// BLoC
class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc() : super(const CartState([])) {
    on<AddItemEvent>((event, emit) {
      emit(CartState([...state.items, event.item]));
    });
    on<RemoveItemEvent>((event, emit) {
      emit(CartState(state.items.where((i) => i.id != event.item.id).toList()));
    });
  }
}

// Widget
BlocBuilder<CartBloc, CartState>(
  builder: (context, state) => Text('${state.items.length} items'),
);

context.read<CartBloc>().add(AddItemEvent(item));
```

## Inherited State Anti-Patterns

```dart
// BAD: Lifting state too high unnecessarily
class AppState extends ChangeNotifier {
  bool isMenuOpen = false; // local nav state doesn't belong at app level
}

// BAD: Calling setState after async without checking mounted
void _load() async {
  final data = await fetch();
  setState(() => _data = data); // may crash if widget is disposed
}

// GOOD:
void _load() async {
  final data = await fetch();
  if (!mounted) return;
  setState(() => _data = data);
}
```

## Reactive Streams

```dart
// StreamBuilder for live data
StreamBuilder<List<Message>>(
  stream: chatService.messages,
  builder: (context, snapshot) {
    if (snapshot.hasError) return Text('Error: ${snapshot.error}');
    if (!snapshot.hasData) return const CircularProgressIndicator();
    return MessageList(messages: snapshot.data!);
  },
);

// FutureBuilder
FutureBuilder<User>(
  future: userService.fetchCurrentUser(),
  builder: (context, snapshot) => switch (snapshot.connectionState) {
    ConnectionState.waiting => const CircularProgressIndicator(),
    ConnectionState.done when snapshot.hasData => UserCard(user: snapshot.data!),
    _ => const Text('Error'),
  },
);
```

