Skip to content

Comparison

Side-by-side comparison with other Flutter state management solutions.

vs. Riverpod

AspectRiverpodPiper
Dependenciesref.watch, ref.readConstructor
Learning curveProvider types, modifiers, scopingPlain Dart
StateProviders + notifiersstate()
Reactivityref.watch auto-tracksWatch / computed auto-track
AsyncAsyncValueAsyncState
Code generationOptionalNone
TestingProviderContainer.test()Plain tests

Riverpod

dart
@riverpod
Future<User> user(Ref ref) async {
  return ref.watch(userRepositoryProvider).getUser();
}

class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(userProvider).when(
      data: (user) => Text(user.name),
      loading: () => CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
    );
  }
}

Shown here in Riverpod's code-generated syntax. A manual, codegen-free syntax is equally supported — since the Dart macros cancellation, Riverpod's docs treat code generation as optional rather than the default.

Piper

dart
class UserViewModel extends ViewModel {
  UserViewModel(this._repo);
  final UserRepository _repo;

  late final user = asyncState<User>();
  void loadUser() => load(user, () => _repo.getUser());
}

class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final vm = context.vm<UserViewModel>();
    return vm.user.build(
      (state) => switch (state) {
        AsyncData(:final data) => Text(data.name),
        AsyncLoading() => CircularProgressIndicator(),
        AsyncError(:final message) => Text('Error: $message'),
        _ => SizedBox.shrink(),
      },
    );
  }
}

vs. Bloc

The bloc package ships two primitives, and they compare differently. Cubit needs only a state plus methods — close to a ViewModel in shape. Bloc adds an event hierarchy and handlers, buying traceability and event transformers.

AspectCubitBlocPiper
State changesMethods → emit()Events → handlers → statesMethods → State
BoilerplateState classEvent + State classesMethods
Asyncemit() in methodsemit() in handlerslaunch(), load()
CancellationManualbloc_concurrency restartable()cancellation.wait
TestingblocTest()blocTest()Plain tests

Cubit

dart
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}

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

Bloc

dart
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

BlocBuilder<CounterBloc, int>(
  builder: (context, count) => Text('$count'),
)

Piper

dart
class CounterViewModel extends ViewModel {
  late final count = state(0);
  void increment() => count.update((c) => c + 1);
  void decrement() => count.update((c) => c - 1);
}

vm.count.build((count) => Text('$count'))

vs. Provider

AspectProviderPiper
StateChangeNotifierStateHolder
LifecycleManual/ProxyProviderAutomatic
AsyncManualBuilt-in
StreamsStreamProviderbind()

When to Choose

Choose Piper if you:

  • Prefer constructor injection
  • Want minimal boilerplate
  • Like plain Dart / testable ViewModels
  • Are coming from Android/iOS
  • Want incremental adoption

Choose Riverpod if you:

  • Need fine-grained provider scoping
  • Want compile-time dependency verification
  • Prefer functional style

Choose Bloc if you:

  • Want strict event/state separation
  • Need event logging/replay
  • Prefer event-driven architecture

(If you like the Bloc ecosystem but not the event boilerplate, Cubit is the library's own recommended starting point — its docs suggest starting with Cubit and scaling up to Bloc when you need transformers or an event log.)

Choose Provider if you:

  • Need the simplest solution
  • Have a small app
  • Want minimal dependencies

Released under the MIT License.