Third Party

With BLoC

Use Qora for server-state and BLoC for UI state machines, with explicit separation of concerns.

BLoC is effective for complex UI state machines such as multi-step checkout, authentication flows, and form orchestration. It is less efficient for repetitive server-state operations such as fetch, cache, retry, and background revalidation.

In a combined architecture, BLoC remains responsible for UI transitions and Qora manages server-state lifecycles.


Division of Labour

ResponsibilityBLoCQora
Fetch user profileQoraBuilder
Fetch product list / infinite feedQoraBuilder / InfiniteQueryBuilder
Create, update, delete (mutations)QoraMutationBuilder
Multi-step checkout flowCheckoutBloc
Form validation stateBLoC or ValueNotifier
Authentication flowAuthBloc
Optimistic UI + cache invalidation after writesetQueryData + invalidate

The two coexist cleanly: QoraScope nests inside the BlocProvider tree (or vice versa).


The Anti-Pattern: Data-Fetching BLoC

A BLoC that only wraps an API call provides no structural benefit over a function call, yet carries the full overhead of events, states, and wiring:

lib/blocs/user_bloc.dart
abstract class UserEvent {}
class LoadUser extends UserEvent { final int id; LoadUser(this.id); }

abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState { final User user; UserLoaded(this.user); }
class UserError extends UserState { final String message; UserError(this.message); }

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserApi _api;

  UserBloc(this._api) : super(UserInitial()) {
    on<LoadUser>((event, emit) async {
      emit(UserLoading());
      try {
        emit(UserLoaded(await _api.getUser(event.id)));
      } catch (e) {
        emit(UserError('$e')); // no retry, no deduplication, no SWR
      }
    });
  }
}
lib/user_screen.dart
BlocBuilder<UserBloc, UserState>(
  builder: (context, state) => switch (state) {
    UserLoading() => const CircularProgressIndicator(),
    UserLoaded(:final user) => UserCard(user),
    UserError(:final message) => ErrorWidget(message),
    _ => const SizedBox.shrink(),
  },
)

After: QoraBuilder for Server Data

lib/user_screen.dart
QoraBuilder<User>(
  queryKey: ['users', userId],
  fetcher: () => UserApi.getUser(userId),
  options: const QoraOptions(staleTime: Duration(minutes: 5)),
  builder: (context, state, fetchStatus) => switch (state) {
    Success(:final data) => UserCard(data),
    Loading() => const CircularProgressIndicator(),
    Failure(:final error) => ErrorWidget('$error'),
    Initial() => const SizedBox.shrink(),
  },
)

For data-only reads, Qora removes event/state boilerplate and manual transition code. Retry with backoff, request deduplication, and background revalidation on focus or reconnect are built in.

The third builder argument fetchStatus distinguishes active requests (FetchStatus.fetching) from paused offline requests (FetchStatus.paused).


BLoC + Qora on the Same Screen

Complex screens often need both: a UI state machine (BLoC) driving a view that also displays server data (Qora). They compose without friction:

lib/checkout_screen.dart
BlocBuilder<CheckoutBloc, CheckoutState>(
  builder: (context, checkoutState) {
    // CheckoutBloc drives the step machine
    if (checkoutState is SelectingAddress) {
      return QoraBuilder<List<Address>>(
        queryKey: ['users', userId, 'addresses'],
        fetcher: () => api.getAddresses(userId),
        builder: (context, state, fetchStatus) => switch (state) {
          Success(:final data) => AddressSelector(
              addresses: data,
              onSelect: (address) =>
                context.read<CheckoutBloc>().add(AddressSelected(address)),
            ),
          Loading() => const CircularProgressIndicator(),
          Failure(:final error)=> ErrorWidget('$error'),
          Initial() => const SizedBox.shrink(),
        },
      );
    }
    if (checkoutState is ReviewingOrder) {
      return OrderReviewWidget(order: checkoutState.order);
    }
    return const SizedBox.shrink();
  },
)

BLoC controls step selection. Qora provides server data inside each step.


Invalidating Qora from a BLoC

When a BLoC performs a write (a form submission, a completed checkout step), it can invalidate Qora's cache so the next read gets fresh data:

lib/blocs/update_user_bloc.dart
class UpdateUserBloc extends Bloc<UpdateUserEvent, UpdateUserState> {
  final QoraClient _client;
  final UserApi _api;

  UpdateUserBloc(this._client, this._api) : super(UpdateUserInitial()) {
    on<SubmitUpdate>((event, emit) async {
      emit(UpdateUserLoading());
      try {
        await _api.updateUser(event.id, event.name);
        _client.invalidate(['users', event.id]); // Qora refetches on next read
        emit(UpdateUserSuccess());
      } catch (e) {
        emit(UpdateUserError('$e'));
      }
    });
  }
}

Pass QoraClient into BLoCs that need to coordinate with the server-state cache. Do not pass BuildContext, the client is a shared cache object, not a widget concern.


Setup

lib/main.dart
final qoraClient = QoraClient(
  config: const QoraClientConfig(debugMode: kDebugMode),
);

runApp(
  MultiBlocProvider(
    providers: [
      BlocProvider(create: (_) => AuthBloc()),
      BlocProvider(create: (ctx) => CheckoutBloc(ctx.read())),
    ],
    child: QoraScope(
      client: qoraClient,
      lifecycleManager: FlutterLifecycleManager(qoraClient: qoraClient),
      connectivityManager: FlutterConnectivityManager(),
      child: const MyApp(),
    ),
  ),
);

QoraScope can be nested inside or outside MultiBlocProvider. Select the layout that yields the clearest composition in the app root.


Checklist

  • Add qora_flutter to pubspec.yaml
  • Add QoraScope to the widget tree alongside MultiBlocProvider
  • Identify BLoCs that only do emit(Loading) -> fetch -> emit(Loaded/Error) and replace them with QoraBuilder
  • Keep BLoCs that model multi-event workflows, complex transitions, or non-fetch domain logic
  • Remove BlocProvider for data-only blocs
  • Replace BlocBuilder for data-only blocs with QoraBuilder
  • Inject QoraClient into BLoCs that perform writes and need to invalidate the cache

Copyright © 2026