How to manage state in Flutter

Choosing and using state management approaches in Flutter — setState, Provider, Riverpod, Bloc, and Signal

, updated

State management is the core challenge of any Flutter app. Here are the most common approaches, from simplest to most robust.

setState — local widget state

Best for: small, single-widget state that doesn’t need to be shared.

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

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

Best for: most apps. Simple, well-documented, built on InheritedWidget.

Add the dependency in pubspec.yaml:

dependencies:
  provider: ^6.1.0

Define a change notifier:

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

Wrap your app and consume:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Column(
      children: [
        Text('Count: ${counter.count}'),
        ElevatedButton(
          onPressed: () => context.read<CounterModel>().increment(),
          child: Text('Increment'),
        ),
      ],
    );
  }
}
  • Use context.watch<T>() to listen (rebuilds on change)
  • Use context.read<T>() to access without listening (e.g. in onPressed)

Riverpod — compile-time safe, testable

Best for: apps that want compile-time safety and less boilerplate than Provider.

dependencies:
  flutter_riverpod: ^2.5.0
// Define a provider
final counterProvider = StateProvider<int>((ref) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Bloc — event-driven, scalable

Best for: larger apps with complex state transitions and dedicated teams.

dependencies:
  flutter_bloc: ^8.1.0
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// State
class CounterState {
  final int count;
  CounterState(this.count);
}

// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<Increment>((event, emit) {
      emit(CounterState(state.count + 1));
    });
  }
}

// Widget
class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Column(
            children: [
              Text('Count: ${state.count}'),
              ElevatedButton(
                onPressed: () => context.read<CounterBloc>().add(Increment()),
                child: Text('Increment'),
              ),
            ],
          );
        },
      ),
    );
  }
}

Which should you use?

Approach Complexity Best for
setState Low Single-widget, ephemeral state
Provider Medium Most apps, shared state
Riverpod Medium Apps wanting compile-time safety
Bloc High Complex state machines, large teams