Presentation Layer
Where your app meets the user's eyes
Open interactive version (quiz + challenge)Real-world analogy
What is it?
The Presentation Layer is the outermost layer in Clean Architecture that the user interacts with. It contains BLoCs that call domain use cases and manage UI state, Screens that provide BLoCs and define page structure, Views that contain the actual UI content, and Widgets that are small reusable components. In team_mvp_kit, the presentation layer uses patterns like TeamzViewStateMixin for common screen behaviors and AsyncContent for the universal loading/error/content pattern. BLoCs handle Either results from use cases using fold() to map success and failure to UI states.
Real-world relevance
In team_mvp_kit, every feature follows the Screen-View-Widget pattern consistently. The Screen creates and provides the BLoC. The View defines the page layout with AppBar and body. Individual widgets handle specific pieces of state using BlocSelector for optimal rebuilds. The TeamzViewStateMixin eliminates repetitive loading overlay and error handling code across dozens of screens. This consistency means any team member can jump into any feature and immediately understand the structure.
Key points
- BLoCs Consuming Use Cases — In the presentation layer, BLoCs receive events from the UI and call use cases from the domain layer. They never call repositories or data sources directly.
- Handling Either Results in BLoC — When use cases return Either, the BLoC uses fold() to handle both cases. Left triggers a failure state, Right triggers a success state.
- Screens vs Views vs Widgets — In team_mvp_kit, Screens are full pages with a Scaffold. Views are the content inside a screen (minus the Scaffold). Widgets are small reusable components. This separation makes code organized and testable.
- TeamzViewStateMixin Pattern — team_mvp_kit uses a mixin that provides common screen behaviors: showing loading overlays, handling errors with SnackBars, and managing lifecycle. This reduces boilerplate across all screens.
- Widgets Consuming BLoCs — Small widgets use BlocBuilder or BlocSelector to display specific parts of state. Each widget only rebuilds when its specific data changes.
- Loading/Error/Content Pattern — Most screens follow a pattern: show loading spinner while fetching, show error with retry if it fails, show content on success. This pattern is so common it deserves a reusable widget.
- Form Screens with BLoC — Forms send events to BLoC on submission. The BLoC validates via use case, then emits success or validation failure. The UI listens for success to navigate away and failure to show errors.
Code example
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Assume domain types: Task, Failure, GetTasksUseCase, etc.
// ---- State ----
class TaskListState extends BaseState {
final List<Task> tasks;
final bool isLoading;
final bool isSubmitting;
final Failure? failure;
const TaskListState({
this.tasks = const [],
this.isLoading = false,
this.isSubmitting = false,
this.failure,
});
TaskListState copyWith({
List<Task>? tasks,
bool? isLoading,
bool? isSubmitting,
Failure? failure,
}) {
return TaskListState(
tasks: tasks ?? this.tasks,
isLoading: isLoading ?? this.isLoading,
isSubmitting: isSubmitting ?? this.isSubmitting,
failure: failure,
);
}
@override
List<Object?> get props => [
tasks, isLoading, isSubmitting, failure,
];
}
// ---- Events ----
class LoadTasks extends BaseEvent {}
class CreateTask extends BaseEvent {
final String title;
final String description;
final TaskPriority priority;
final String assigneeId;
const CreateTask({
required this.title,
required this.description,
this.priority = TaskPriority.medium,
this.assigneeId = '',
});
@override
List<Object?> get props => [
title, description, priority, assigneeId,
];
}
class DeleteTask extends BaseEvent {
final String taskId;
const DeleteTask(this.taskId);
@override
List<Object?> get props => [taskId];
}
// ---- BLoC ----
class TaskListBloc extends Bloc<BaseEvent, TaskListState>
with SafeEmitMixin {
final GetTasksUseCase _getTasks;
final CreateTaskUseCase _createTask;
TaskListBloc({
required GetTasksUseCase getTasks,
required CreateTaskUseCase createTask,
}) : _getTasks = getTasks,
_createTask = createTask,
super(const TaskListState()) {
on<LoadTasks>(_onLoad);
on<CreateTask>(_onCreate);
}
Future<void> _onLoad(
LoadTasks event,
Emitter<TaskListState> emit,
) async {
safeEmit(state.copyWith(
isLoading: true,
failure: null,
));
final result = await _getTasks(const NoParams());
result.fold(
(failure) => safeEmit(state.copyWith(
isLoading: false,
failure: failure,
)),
(tasks) => safeEmit(state.copyWith(
isLoading: false,
tasks: tasks,
)),
);
}
Future<void> _onCreate(
CreateTask event,
Emitter<TaskListState> emit,
) async {
safeEmit(state.copyWith(
isSubmitting: true,
failure: null,
));
final result = await _createTask(CreateTaskParams(
title: event.title,
description: event.description,
priority: event.priority,
assigneeId: event.assigneeId,
));
result.fold(
(failure) => safeEmit(state.copyWith(
isSubmitting: false,
failure: failure,
)),
(_) {
safeEmit(state.copyWith(isSubmitting: false));
add(LoadTasks());
},
);
}
}
// ---- Screen (provides BLoC) ----
class TaskListScreen extends StatelessWidget {
const TaskListScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<TaskListBloc>()
..add(LoadTasks()),
child: const TaskListView(),
);
}
}
// ---- View (content with Scaffold) ----
class TaskListView extends StatelessWidget {
const TaskListView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Tasks'),
actions: const [TaskCountBadge()],
),
body: BlocConsumer<TaskListBloc, TaskListState>(
listenWhen: (prev, curr) =>
prev.failure != curr.failure &&
curr.failure != null,
listener: (context, state) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.failure!.message),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'Retry',
textColor: Colors.white,
onPressed: () => context
.read<TaskListBloc>()
.add(LoadTasks()),
),
),
);
},
builder: (context, state) {
if (state.isLoading && state.tasks.isEmpty) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (state.tasks.isEmpty) {
return const Center(
child: Text('No tasks yet. Create one!'),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<TaskListBloc>()
.add(LoadTasks());
},
child: ListView.builder(
itemCount: state.tasks.length,
itemBuilder: (context, index) {
return TaskCard(
task: state.tasks[index],
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.push('/tasks/create'),
child: const Icon(Icons.add),
),
);
}
}
// ---- Reusable Widget ----
class TaskCard extends StatelessWidget {
final Task task;
const TaskCard({super.key, required this.task});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4,
),
child: ListTile(
leading: Icon(
task.isDone
? Icons.check_circle
: Icons.radio_button_unchecked,
color: task.isDone ? Colors.green : null,
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isDone
? TextDecoration.lineThrough
: null,
),
),
subtitle: Text(task.description),
trailing: task.isHighPriority
? const Icon(Icons.priority_high,
color: Colors.red)
: null,
onTap: () => context.push(
'/tasks/${task.id}',
),
),
);
}
}
// ---- BlocSelector Widget ----
class TaskCountBadge extends StatelessWidget {
const TaskCountBadge({super.key});
@override
Widget build(BuildContext context) {
return BlocSelector<TaskListBloc, TaskListState, int>(
selector: (state) =>
state.tasks.where((t) => !t.isDone).length,
builder: (context, pendingCount) {
if (pendingCount == 0) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Badge(
label: Text('$pendingCount'),
child: const Icon(Icons.task_alt),
),
);
},
);
}
}Line-by-line walkthrough
- 1. TaskListState holds tasks, loading flags, and failure following the tri-state pattern.
- 2. copyWith enables immutable state updates with selective field overrides.
- 3. LoadTasks triggers data fetch. CreateTask carries form data. DeleteTask carries the task ID.
- 4. TaskListBloc depends on use cases injected through the constructor, not repositories.
- 5. _onLoad sets loading true, calls the use case, and uses fold() on the Either result.
- 6. Left branch (failure) emits state with the failure. Right branch (success) emits state with tasks.
- 7. _onCreate sets isSubmitting true, calls create use case, handles both outcomes.
- 8. On successful creation, it dispatches LoadTasks to refresh the list automatically.
- 9. TaskListScreen is the entry point -- it creates and provides the BLoC, then renders TaskListView.
- 10. getIt() uses the service locator to get a fully wired BLoC instance.
- 11. The cascade operator (..) immediately sends LoadTasks to start fetching data.
- 12. TaskListView uses BlocConsumer for both building UI and listening for errors.
- 13. listenWhen filters to only react when a new failure appears in the state.
- 14. The SnackBar includes a Retry action that dispatches LoadTasks again.
- 15. The builder shows a spinner while loading with no existing data, an empty state message, or the list.
- 16. RefreshIndicator enables pull-to-refresh that dispatches LoadTasks.
- 17. TaskCard is a reusable widget showing task details with icons for completion and priority.
- 18. TaskCountBadge uses BlocSelector to only rebuild when the pending count changes.
Spot the bug
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final bloc = ProfileBloc(
getProfile: GetProfileUseCase(getIt()),
);
bloc.add(LoadProfile());
return BlocProvider.value(
value: bloc,
child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: Text(state.user?.name ?? '')),
body: Text(state.user?.email ?? ''),
);
},
),
);
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- flutter_bloc Package (pub.dev)
- BLoC Architecture Guide (bloclibrary.dev)
- Flutter State Management (flutter.dev)