Skip to main content

Add a bloc & page

A page owns layout + its bloc; the bloc owns state + side effects. Pages provide their bloc via AutoRouteWrapper.

The bloc state contract (load-bearing)

Every bloc state must be @JsonSerializable() + Equatable, with a Status enum, explicit toJson/fromJson, and part 'state.g.dart'. The DevTools bloc plugin serializes this — so the shape is non-negotiable. Mirror an existing bloc (e.g. app/lib/inside/blocs/setup/ or today_chores/).

// state.dart
@JsonSerializable()
class FeatureState extends Equatable {
const FeatureState({this.status = FeatureStatus.idle, this.errorMessage});
final FeatureStatus status;
final String? errorMessage;

FeatureState copyWith({FeatureStatus? status, String? Function()? setErrorMessage}) =>
FeatureState(
status: status ?? this.status,
errorMessage: setErrorMessage != null ? setErrorMessage() : errorMessage,
);

factory FeatureState.fromJson(Map<String, dynamic> json) => _$FeatureStateFromJson(json);
Map<String, dynamic> toJson() => _$FeatureStateToJson(this);

@override
List<Object?> get props => [status, errorMessage];
}
  • Events: an abstract base + concrete event classes. Register handlers with sequential() (from bloc_concurrency) to keep transitions race-free.
  • A bloc receives repositories (and effect providers where allowed) via its constructor — never a Client Provider directly.

The page

Pages implement AutoRouteWrapper and create the bloc in wrappedRoute() — never in build():

@RoutePage()
class FeaturePage extends StatelessWidget implements AutoRouteWrapper {
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) => FeatureBloc(
featureRepository: context.read<FeatureRepository>(),
)..add(FeatureStarted()),
child: this,
);

@override
Widget build(BuildContext context) { /* layout; extract sub-widgets to widgets/ */ }
}

Follow the per-page widgets/ pattern: page.dart is layout + bloc; concrete sub-components live in the page's widgets/ directory (see authenticated/home/).

Wire the route

Add the page to AppRouter (app/lib/inside/routes/router.dart), then regenerate the router — scoped, so you don't clobber siblings:

cd app
fvm dart run build_runner build --delete-conflicting-outputs \
--build-filter "lib/inside/routes/router.gr.dart"

Then regenerate the state too: --build-filter "lib/inside/blocs/<feature>/state.g.dart". See Codegen & graphify.