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()(frombloc_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.