The Page widget is the entry point for a feature. It is the first widget created when navigating to a screen. Its sole responsibilities are:
- Providing Blocs to the widget tree via
BlocProvider/MultiBlocProvider. - Declaring the feature's route.
- Wrapping in
PopScopewhen back-navigation must be prevented.
Page classes must be named [FeatureName]Page in PascalCase:
class LoginPage extends StatelessWidget { ... }
class ProfilePage extends StatelessWidget { ... }
class OrderDetailPage extends StatelessWidget { ... }The Page class must extend StatelessWidget:
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
...
}The constructor must be const.
When a page requires parameters (e.g., an ID or a flag), declare a separate Params class in the same file:
class OrderDetailParams {
const OrderDetailParams({
required this.orderId,
this.showReviewPrompt = false,
});
final String orderId;
final bool showReviewPrompt;
}
class OrderDetailPage extends StatelessWidget {
const OrderDetailPage({required this.params, super.key});
final OrderDetailParams params;
...
}For deep-link or URL-driven parameters, add a fromQueryParams factory to the Params class:
factory OrderDetailParams.fromQueryParams(Map<String, dynamic> query) =>
OrderDetailParams(
orderId: query['id'] as String? ?? '',
showReviewPrompt: query['review'] == 'true',
);Every Page must declare routeName and path as static constants:
class LoginPage extends StatelessWidget {
static const routeName = 'login';
static const path = '/$routeName';
...
}For routes with path parameters:
class OrderDetailPage extends StatelessWidget {
static const routeName = 'order/:id';
static const path = '/$routeName';
/// Builds the concrete path by substituting the [orderId].
static String buildPath(String orderId) =>
path.replaceFirst(':id', orderId);
...
}Exception: Pages that serve as tabs within a
DefaultTabControlleror as steps in a multi-page form do not need a route.
The build method must return either:
- A
BlocProvider/MultiBlocProvider→Scaffold - A
PopScopewrapping aBlocProvider/MultiBlocProvider→Scaffold
No other root widget is allowed.
The Scaffold's body must be the feature's View widget.
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginBloc(
signInUseCase: context.read<SignInUseCase>(),
),
child: Scaffold(
body: const LoginView(),
),
);
}@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => ProfileBloc(
getUserUseCase: context.read<GetUserUseCase>(),
),
),
BlocProvider(
create: (context) => AvatarBloc(
uploadAvatarUseCase: context.read<UploadAvatarUseCase>(),
),
),
],
child: Scaffold(
body: const ProfileView(),
),
);
}@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: BlocProvider(
create: (context) => OnboardingBloc(...),
child: Scaffold(
body: const OnboardingView(),
),
),
);
}// presentation/login/view/login_page.dart
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
static const routeName = 'login';
static const path = '/$routeName';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginBloc(
signInUseCase: context.read<SignInUseCase>(),
),
child: const Scaffold(
body: LoginView(),
),
);
}
}