Home
Dart
OOP
Flutter
Contact

State Management Concepts

Compare Provider vs BLoC and learn architectural patterns for state management.

Local vs Global State

State management is one of the most important architectural decisions in any Flutter app. At its core, it's about answering one question: where does this data live, and who can access it? Getting this wrong leads to tangled code, hard-to-fix bugs, and poor performance. Getting it right makes your app predictable, testable, and easy to maintain.

The key insight is that not all state is equal. A text field's current value is local state — it only matters to that one widget. But the user's authentication status is global state — every screen might need it. Here's how to think about the different scopes:

ScopeExampleTool
Local StateForm input, toggle, animationsetState()
Screen StateTab index, scroll positionValueNotifier
Global StateAuth, cart, themeProvider, BLoC, Riverpod
💡 Rule of Thumb Start with setState(). Only reach for global state management when data must be shared across multiple widgets or screens.

Provider Pattern

Provider is the recommended state management solution for small-to-medium apps:

counter_provider.dart
import 'package:flutter/material.dart';

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

  void increment() {
    _count++;
    notifyListeners();  // Triggers widget rebuild
  }

  void decrement() {
    if (_count > 0) {
      _count--;
      notifyListeners();
    }
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

// Wrap MaterialApp with ChangeNotifierProvider
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: const MyApp(),
    ),
  );
}

// Access state in widgets
class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var counter = Provider.of<CounterProvider>(context);
    return Text('${counter.count}',
      style: const TextStyle(fontSize: 64));
  }
}

BLoC Pattern

Business Logic Component — separates UI from business logic using streams and events:

counter_bloc.dart
enum CounterEvent { increment, decrement, reset }

class CounterBloc {
  int _count = 0;

  final _eventController = StreamController<CounterEvent>.broadcast();
  final _stateController = StreamController<int>.broadcast();

  Stream<int> get stream => _stateController.stream;
  Sink<CounterEvent> get eventSink => _eventController.sink;

  CounterBloc() {
    _eventController.stream.listen(_handleEvent);
  }

  void _handleEvent(CounterEvent event) {
    switch (event) {
      case CounterEvent.increment: _count++; break;
      case CounterEvent.decrement: if (_count > 0) _count--; break;
      case CounterEvent.reset: _count = 0; break;
    }
    _stateController.add(_count);
  }

  void dispose() {
    _eventController.close();
    _stateController.close();
  }
}

When to Use Which Pattern

FactorProviderBLoC
Learning CurveLow — intuitive APIHigher — streams, events, states
BoilerplateMinimalMore (events, states, bloc files)
TestabilityGood with mock providersExcellent — events in, states out
ScalabilityGood for small-medium appsExcellent for large teams
DebuggingBasic loggingBLoCObserver for full trace
💡 Decision Rule of Thumb Start with Provider for most projects. It's simpler, faster to implement, and covers 80% of use cases. Switch to BLoC when you need strict separation of concerns, complex event-driven flows, or when working on a large team.

Provider vs BLoC

AspectProviderBLoC
ComplexityLowMedium-High
Learning CurveEasySteeper
TestabilityGoodExcellent (streams)
BoilerplateMinimalMore
Best ForSmall-medium appsLarge, complex apps
ℹ️ Recommendation Start with Provider. Migrate to BLoC (or Riverpod) when your app grows complex enough to need strict separation of concerns.

Real-World Example: Counter (Side by Side)

comparison.dart
// Provider approach
class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Consumer<CounterProvider>(
        builder: (ctx, counter, _) => Text('${counter.count}',
          style: const TextStyle(fontSize: 48)),
      ),
      ElevatedButton(
        onPressed: () => Provider.of<CounterProvider>(context, listen: false).increment(),
        child: const Text('Increment'),
      ),
    ]);
  }
}

// BLoC approach
class CounterScreen extends StatefulWidget {
  final CounterBloc bloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      StreamBuilder<int>(
        stream: bloc.stream,
        builder: (ctx, snapshot) => Text('${snapshot.data ?? 0}',
          style: const TextStyle(fontSize: 48)),
      ),
      ElevatedButton(
        onPressed: () => bloc.eventSink.add(CounterEvent.increment),
        child: const Text('Increment'),
      ),
    ]);
  }
}

Summary

  • setState() for local, ephemeral state
  • Provider: simple, reactive, best for most apps
  • BLoC: stream-based, excellent testability, for complex apps
  • Choose based on app complexity and team experience
🚀 Next Step Continue to Navigation & Routing.
K
Kesavaraja Murugesan
Flutter Developer & Educator

Flutter developer with 3 years of experience building production-grade mobile applications. Passionate about teaching clean architecture patterns and helping developers write maintainable, scalable code.

← Complex UIs Next: Navigation →