Presentation Layer - Feelynx/flutter_clean_architecture GitHub Wiki
The Presentation layer in the Clean Architecture pattern is responsible for rendering the UI and handling the interaction between the UI and business logic. In a Flutter application, this layer typically involves Widgets and state management solutions such as Cubit or Bloc to manage the application’s state.
- Widgets: Represent the UI components of the application, responsible for displaying data and handling user interaction.
- Cubit/Bloc: Manages the state of the application and interacts with Use Cases to fetch or update data. The UI listens to changes in the state and updates accordingly.
- Cubit State Classes: Define the various states that the UI can be in, such as loading, success, or error states.
In this example, ProfilePage is a stateless widget that uses BlocConsumer to listen to changes in the ProfileCubit’s state and build the appropriate UI based on the current state.
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<ProfileCubit, ProfileState>(
listener: (context, state) {
if (state is ProfileError) {
ErrorDialog.showErrorDialog(context: context, title: 'Error', message: state.message);
}
},
builder: (context, state) => switch (state) {
ProfileInitial() || ProfileLoading() => const LoadingWidget(),
ProfileLoaded() => SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(state.user.image ?? ''),
),
],
),
const Gap(16),
Text(
'${state.user.firstName} ${state.user.lastName}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const Gap(8),
Text(
'${state.user.company?.title}\n@${state.user.company?.name}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
),
const Gap(24),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Contacts',
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${state.user.email}'),
Text('${state.user.phone}'),
Text('${state.user.university}'),
],
),
),
],
),
),
_ => Container(),
},
),
);
}
}
The ProfileCubit manages the state for the ProfilePage. It handles the logic of fetching user data using ProfileUseCases and emits states that represent loading, success, or error.
class ProfileCubit extends Cubit<ProfileState> {
ProfileCubit(this.profileUseCases) : super(ProfileInitial()) {
getUser();
}
final ProfileUseCases profileUseCases;
Future<void> getUser() async {
emit(ProfileLoading());
final response = await profileUseCases.getUser();
response.when(
(data) {
emit(ProfileLoaded(data));
},
(error) {
emit(ProfileError(error.toString()));
},
);
}
}
In this example:
- The ProfileCubit fetches user data when initialized and emits ProfileLoading to indicate data fetching is in progress.
- Once the data is fetched, it emits ProfileLoaded or ProfileError based on the result.
- The UI listens for these state changes and updates accordingly.
The state of the ProfileState is represented by several classes that extend ProfileState. These states represent different stages of the profile data fetching process.
part of 'profile_cubit.dart';
sealed class ProfileState extends Equatable {
const ProfileState();
@override
List<Object> get props => [];
}
class ProfileInitial extends ProfileState {}
class ProfileLoading extends ProfileState {}
class ProfileLoaded extends ProfileState {
final UserEntity user;
const ProfileLoaded(this.user);
@override
List<Object> get props => [user];
}
class ProfileError extends ProfileState {
final String message;
const ProfileError(this.message);
@override
List<Object> get props => [message];
}
- ProfileInitial: The initial state before any action is performed.
- ProfileLoading: Represents the loading state when data is being fetched.
- ProfileLoaded: Represents the successful loading of profile data.
- ProfileError: Represents an error state with an accompanying error message.
The Presentation layer in Flutter’s Clean Architecture pattern consists of widgets and state management solutions like Cubit. Widgets render the UI based on the state emitted by the Cubit, while Cubit interacts with the domain layer (Use Cases) to fetch and manage data. The state classes encapsulate different stages of data fetching and error handling, ensuring the UI reflects the appropriate state.