The View widget sits between the Page and the Body. Its responsibilities are:
- Listening to Bloc state changes and triggering side effects (navigation, snackbars, dialogs).
- Delegating rendering to the
Body.
It has no parameters beyond super.key, and does no direct rendering of business content.
View classes must be named [FeatureName]View:
class LoginView extends StatelessWidget { ... }
class ProfileView extends StatelessWidget { ... }The View class must extend StatelessWidget:
class LoginView extends StatelessWidget {
const LoginView({super.key});
...
}The constructor must be const and accept no parameters beyond super.key.
The build method must return either:
- The feature's
Bodywidget directly, if no listeners are needed. - A
BlocListenerwrapping theBody. - A
MultiBlocListenerwrapping theBody.
No other root widget is allowed.
@override
Widget build(BuildContext context) => const LoginBody();Each BlocListener must listen to one single property of the state at a time (singularity principle). Use listenWhen to filter to the exact property:
@override
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == .success) {
Navigator.of(context).pushReplacementNamed(HomePage.path);
}
if (state.status == .failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
);
}
},
child: const LoginBody(),
);
}When multiple state properties need to trigger side effects, use MultiBlocListener:
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
BlocListener<LoginBloc, LoginState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == .success) {
Navigator.of(context).pushReplacementNamed(HomePage.path);
}
},
),
BlocListener<LoginBloc, LoginState>(
listenWhen: (prev, curr) => prev.errorMessage != curr.errorMessage,
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage!)),
);
}
},
),
],
child: const LoginBody(),
);
}When a state change in one Bloc should trigger an event in another, do it inside a listener:
BlocListener<AuthBloc, AuthState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == .authenticated) {
context.read<ProfileBloc>().add(const ProfileLoadRequested());
}
},
child: ...,
)When a feature must support both phone and tablet layouts, use LayoutBuilder in the View to render the appropriate Body:
@override
Widget build(BuildContext context) {
return BlocListener<ProfileBloc, ProfileState>(
listenWhen: ...,
listener: ...,
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 600) {
return const ProfileBodyTablet();
}
return const ProfileBody();
},
),
);
}// presentation/login/view/login_page.dart (View section)
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<LoginBloc, LoginState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
switch (state.status) {
case .success:
Navigator.of(context).pushReplacementNamed(HomePage.path);
case .failure:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sign in failed. Please try again.')),
);
default:
break;
}
},
child: const LoginBody(),
);
}
}