FLUTTER: Flutter State Management

State management is the practice of controlling how data flows through a Flutter application. From the simplest setState call to shared app-wide state with InheritedWidget and Provider, choosing the right state management approach is one of the most important architectural decisions in a Flutter app.

Every interactive Flutter application has state — data that changes over time and drives the UI. Managing that state correctly is the difference between a performant, maintainable app and one that is slow, buggy, and hard to extend.


What Is State?

In Flutter, state is data that represents the current condition of the app. When the state changes, Flutter rebuilds the affected parts of the widget tree to reflect the new data.

md
State Change --> Widget Rebuild --> Updated UI

State comes in two forms:

Ephemeral State

Ephemeral state (also called local or UI state) is temporary state that lives entirely within a single widget. It does not need to be shared with other widgets.

  • Whether a checkbox is checked
  • The current page in a carousel
  • Whether a password field shows or hides text

Ephemeral state is managed with StatefulWidget and setState().

App State

App state (also called shared or global state) is data that needs to be accessible across multiple widgets or screens.

  • The current authenticated user
  • The items in a shopping cart
  • User preferences and settings
  • Data fetched from a remote API

App state requires a dedicated state management approach.


Why State Management Matters

Using setState() everywhere works for small apps, but it introduces significant problems as an app grows:

  • Tight coupling — UI and business logic are mixed in the same widget
  • Redundant rebuilds — calling setState() rebuilds the entire subtree, even unchanged parts
  • Hard to test — business logic embedded in widgets cannot be easily unit tested
  • Difficult to share — passing state deep through the widget tree (prop drilling) becomes unmanageable

A proper state management approach:

  • Separates business logic from UI
  • Rebuilds only the widgets that depend on changed data
  • Enables independent testing of business logic
  • Makes shared state accessible without prop drilling

setState — The Simplest Approach

setState() is the built-in Flutter mechanism for updating ephemeral state. It is available in any StatefulWidget and triggers a rebuild of the widget's subtree.

dart
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

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

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

  void _decrement() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Count: $_counter',
              style: const TextStyle(fontSize: 32),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(onPressed: _decrement, child: const Text('-')),
                const SizedBox(width: 16),
                ElevatedButton(onPressed: _increment, child: const Text('+')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

When to use setState:

  • The state is used only by a single widget
  • The state change is simple and local
  • The app is small and the number of stateful interactions is limited

InheritedWidget — Passing State Down the Tree

InheritedWidget allows data to flow down the widget tree and react to changes. It is the low-level mechanism that powers most higher-level state management solutions.

dart
class AppData extends InheritedWidget {
  final int counter;
  final VoidCallback increment;

  const AppData({
    super.key,
    required this.counter,
    required this.increment,
    required super.child,
  });

  // Widgets that depend on this widget will rebuild when updateShouldNotify returns true
  @override
  bool updateShouldNotify(covariant AppData oldWidget) {
    return oldWidget.counter != counter;
  }

  // Convenience accessor so descendant widgets can find this data
  static AppData? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppData>();
  }
}

A widget deep in the tree can access the data without it being passed through every intermediate widget:

dart
// Anywhere in the widget subtree:
final data = AppData.of(context);
Text('Count: ${data?.counter}')

InheritedWidget is powerful but verbose to implement directly. Most real apps use a package that builds on top of it.


Provider is the most widely adopted state management package in the Flutter ecosystem. It is built on InheritedWidget and ChangeNotifier but eliminates the boilerplate.

bash
flutter pub add provider

Define a ChangeNotifier

dart
import 'package:flutter/foundation.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // Triggers rebuild of all listening widgets
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

Provide the Model

Wrap the widget tree with a ChangeNotifierProvider at the level where the state should be accessible:

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

Consume the Model

Use Consumer or context.watch to subscribe to changes:

dart
// Using Consumer (rebuilds only the Consumer's subtree)
Consumer<CounterModel>(
  builder: (context, model, child) {
    return Text('Count: ${model.count}');
  },
)

// Using context.watch (rebuilds the entire build method)
final count = context.watch<CounterModel>().count;

// Using context.read (no subscription — for actions only)
context.read<CounterModel>().increment();


State Management Comparison

ApproachComplexityBest For
setStateLowSingle-widget local state
InheritedWidgetMediumManual, low-dependency sharing
ProviderLow-MediumMost apps — simple global state
RiverpodMediumTestable, composable state
BlocHighLarge apps with complex event-driven logic

Choosing the Right Approach

md
Is the state used only by one widget?
     |
     +-- Yes --> setState()
     |
     +-- No: Is the app small / medium with simple shared state?
                  |
                  +-- Yes --> Provider
                  |
                  +-- No: Do you need testable, event-driven state machines?
                               |
                               +-- Yes --> Bloc
                               |
                               +-- No --> Riverpod

The most important principle: separate your business logic from your UI. Whether you use Provider, Riverpod, or Bloc, the goal is always the same — your widgets should describe what to show, not how to calculate it.