Question #211EasyFlutter Basics

Difference between InheritedWidget vs Provider vs Riverpod vs Bloc vs GetX ?

#widget#bloc#getx#riverpod#provider

Answer

InheritedWidget vs Provider vs Riverpod vs Bloc vs GetX

Understanding the differences between these state management solutions is crucial for choosing the right one for your Flutter project.

Comparison Overview

FeatureInheritedWidgetProviderRiverpodBlocGetX
ComplexityHighLowMediumHighLow
BoilerplateHighMediumLowHighMinimal
Learning CurveSteepEasyModerateSteepVery Easy
Type SafetyManualGoodExcellentGoodWeak
TestingHardMediumEasyEasyMedium
DependenciesNoneInheritedWidgetNoneStreamsNone
ImmutabilityManualManualEncouragedRequiredOptional
DevTools SupportLimitedGoodExcellentExcellentGood

InheritedWidget

Foundation of Flutter's dependency injection

dart
class CounterInherited extends InheritedWidget {
  final int count;
  final VoidCallback increment;

  const CounterInherited({
    required this.count,
    required this.increment,
    required Widget child,
  }) : super(child: child);

  static CounterInherited? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterInherited>();
  }

  
  bool updateShouldNotify(CounterInherited oldWidget) {
    return oldWidget.count != count;
  }
}

// Usage
final counter = CounterInherited.of(context);
Text('Count: ${counter?.count}')

Pros:

  • Built into Flutter
  • No dependencies
  • Very performant

Cons:

  • High boilerplate
  • Complex implementation
  • Hard to test

Provider

Wrapper around InheritedWidget

dart
class CounterNotifier extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

// Setup
ChangeNotifierProvider(
  create: (_) => CounterNotifier(),
  child: MyApp(),
)

// Usage
final counter = context.watch<CounterNotifier>();
Text('Count: ${counter.count}')

// Or with Consumer
Consumer<CounterNotifier>(
  builder: (context, counter, child) {
    return Text('Count: ${counter.count}');
  },
)

Pros:

  • Simple API
  • Officially recommended
  • Good documentation
  • Multiple provider types

Cons:

  • Requires BuildContext
  • Runtime errors possible
  • Needs careful disposal

Riverpod

Modern, compile-safe Provider

dart
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
}

// Usage - No BuildContext needed!
class CounterWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Pros:

  • Compile-time safety
  • No BuildContext needed
  • Excellent testing
  • Auto-dispose
  • DevTools integration

Cons:

  • Different syntax from Provider
  • Learning curve
  • Newer (less resources)

Bloc

Business Logic Component pattern

dart
// Events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

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

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

// Setup
BlocProvider(
  create: (context) => CounterBloc(),
  child: MyApp(),
)

// Usage
BlocBuilder<CounterBloc, CounterState>(
  builder: (context, state) {
    return Column(
      children: [
        Text('Count: ${state.count}'),
        ElevatedButton(
          onPressed: () {
            context.read<CounterBloc>().add(IncrementEvent());
          },
          child: Text('Increment'),
        ),
      ],
    );
  },
)

Pros:

  • Clear separation of concerns
  • Predictable state changes
  • Excellent testing
  • Great for complex apps
  • Strong typing

Cons:

  • High boilerplate
  • Steep learning curve
  • Verbose for simple cases
  • Event-driven complexity

GetX

All-in-one solution

dart
class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
}

// Setup (automatic)
Get.put(CounterController());

// Usage - No BuildContext!
class CounterPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return GetBuilder<CounterController>(
      builder: (controller) {
        return Column(
          children: [
            Text('Count: ${controller.count}'),
            ElevatedButton(
              onPressed: controller.increment,
              child: Text('Increment'),
            ),
          ],
        );
      },
    );
  }
}

// Or with Obx
Obx(() => Text('Count: ${Get.find<CounterController>().count}'))

Pros:

  • Minimal boilerplate
  • Very easy to learn
  • Fast development
  • Includes routing, di, utils
  • No BuildContext needed

Cons:

  • "Magic" behavior
  • Tight coupling
  • Hard to test
  • Not idiomatic Flutter
  • Type safety issues

Real-World Comparison

Simple Counter App:

SolutionLines of CodeSetup Complexity
InheritedWidget~80High
Provider~40Low
Riverpod~35Medium
Bloc~60High
GetX~25Very Low

Complex App (with testing):

SolutionMaintainabilityTestabilityScalability
InheritedWidgetMediumHardGood
ProviderGoodGoodGood
RiverpodExcellentExcellentExcellent
BlocExcellentExcellentExcellent
GetXFairFairFair

When to Use Each

Use InheritedWidget when:

  • Building a package/library
  • Need maximum control
  • No dependencies allowed
  • Learning Flutter internals

Use Provider when:

  • Building standard apps
  • Following official recommendations
  • Team familiar with Flutter
  • Need good documentation

Use Riverpod when:

  • Starting new project
  • Want compile-time safety
  • Need excellent testing
  • Building complex apps
  • Want modern architecture

Use Bloc when:

  • Enterprise applications
  • Complex business logic
  • Strict architecture needed
  • Testing is critical
  • Team from reactive background

Use GetX when:

  • Rapid prototyping
  • Small to medium apps
  • Simple requirements
  • Learning Flutter
  • Need quick results

Migration Path

dart
// From InheritedWidget to Provider
// Before
class DataInherited extends InheritedWidget { ... }

// After
class DataProvider extends ChangeNotifier { ... }

// From Provider to Riverpod
// Before
Provider(create: (_) => Service())

// After
final serviceProvider = Provider((ref) => Service())

// From GetX to Bloc
// Before
class Controller extends GetxController { ... }

// After
class Bloc extends Bloc<Event, State> { ... }

Best Practices by Solution

Provider:

  • Use appropriate provider type (ChangeNotifier, Stream, Future)
  • Dispose controllers properly
  • Use Consumer for targeted rebuilds
  • Keep providers focused

Riverpod:

  • Use family for parameterized providers
  • Leverage autoDispose for lifecycle management
  • Combine providers for derived state
  • Use StateNotifierProvider for complex state

Bloc:

  • One Bloc per feature
  • Use Equatable for state comparison
  • Implement proper event handling
  • Test blocs thoroughly

GetX:

  • Avoid global state when possible
  • Use dependency injection
  • Keep controllers focused
  • Don't mix routing and state management

Recommendation: For new projects in 2026, Riverpod and Bloc are the most recommended solutions for production apps. Provider remains a solid choice for teams already familiar with it. Avoid GetX for large-scale applications.

Learn more: