Introduction

State management in Flutter is one of those topics that seems simple at first, but gets complicated fast. If you've ever struggled to figure out the best way to share and manage data between widgets or across your app, you're not alone. With Flutter's reactive UI model, choosing the right approach can make your app faster, more maintainable, and easier to test.

This guide will walk you through:

What is State?

In Flutter, state refers to any data that affects the UI. For example:

If data changes and your UI needs to update, its state.

Types of State

Flutter state can be categorized into:

1. Local State

Use setState()

2. App-Wide (Shared) State

Use: A state management solution like Provider, Riverpod, or Bloc

Popular State Management Approaches

1. Best for Local UI State

Simple counter example:

  
    class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;
  void _increment() {
    setState(() {
      _count++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(child: Text('Count: \$_count')),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}
  

Pros

Cons

2. Provider - Flutter's Official Recommendation

Setup

  
    # pubspec.yaml
dependencies:
flutter:sdk: flutter
provider: ^6.0.0
  

Create the Provider

  
    class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() {
    _count++;
    notifyListeners();
  }
}
  

Use in app

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

In widget

  
    final count = context.watch<Counter>().count;
context.read<Counter>().increment();
  

Pros

Cons

3. Riverpod - A Better Provider
Setup

 # pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.1

Define a Provider

final counterProvider = StateProvider<int>((ref) => 0);

Wrap your app

void main() {
   runApp(
);
ProviderScope(child: MyApp()),
}

Use in widgets

class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(
child: Text('Count: \$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}

Pros

Cons

4. Bloc - Best for Large-Scale Projects

Bloc (Business Logic Component) uses streams to separate logic and UI.

Setup

# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.1
bloc: ^8.1.0

Create Bloc

class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}

Use BlocProvider and BlocBuilder:

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

BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('Count: \$count'),
)

Pros

Cons

5. GetX - Quick and Minimal

Setup

# pubspec.yaml
dependencies:
get: ^4.6.5

Controller

class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}

Use in UI

final controller = Get.put(CounterController());
Obx(() => Text('Count: \${controller.count}'))

Pros

Cons

Summary Table

Use caseBest option
Simple UI UpdatessetState()
Shared App-wide stateProvider or Riverpod
Enterprise app structureBloc
Quick MVP or PrototypeGetX