PART 3 - Building a Simple Social Feed using DDD Clean Architecture
In the previous chapter, we looked at how to apply the DDD Clean Architecture to our simple social feed flutter application. We implemented the Presentation layer and the Domain layer. In this chapter, we will be implementing the Infrastructure layer and the Application layer.
Source code on Github
The source code for Circle is available on Github. You can find it here
The Infrastructure Layer
After implementing the Domain layer, the next step is to implement the Infrastructure layer. The Infrastructure layer is responsible for providing the implementation of the interfaces defined in the Domain layer.
In our case, we have six methods in the PostsInterface
located in the Domain layer. We will be implementing these interfaces here, in the Infrastructure layer.
In the image above we have the infrastructure
folder which contains the core
folder which contains init_posts.dart
used to generate initial posts for the app. It also contains the posts
folder. The posts
folder contains the posts_repository.dart
file which contains the implementations of the interfaces defined in the Domain layer.
Here is one example of one of the methods defined in the PostsInterface
in the Domain layer.
(as: PostsInterface)
class PostsRepository implements PostsInterface {
Future<Either<Failure, Unit>> likePostPressed({required int postId}) async {
try {
var dbBox = await Hive.openBox<PostsResponse>('postResponse');
if (dbBox.isEmpty) {
return left(const Failure.serverError());
}
// Get the posts from dbBox and like/unlike the post where post.id is equal to the postId passed in
var posts = dbBox.get('posts')!.posts;
var post = posts.firstWhere((post) => post.id == postId);
// if the post is already disliked, then remove the dislike
if (post.disliked) {
post.disliked = false;
post.dislikes -= 1;
}
// then like/unlike the post
post.liked = !post.liked;
post.likes = post.liked ? post.likes + 1 : post.likes - 1;
// Update the posts in the dbBox
dbBox.put('posts', PostsResponse(posts: posts));
return right(unit);
} on SocketException catch (_) {
return left(const Failure.noInternet());
}
}
}
In the above code block, we are implementing the likePostPressed
method defined in the PostsInterface
in the Domain layer. We are using the Hive
package to store the posts in the local database. We are also using the Dartz
package to handle errors and exceptions, and the Injectable
package to inject the PostsRepository
class as a singleton (more on dependency injection coming up later).
The PostsRepository
class would be required to implement all other methods defined in the PostsInterface
in the Domain layer.
The PostsRepository
class would be injected into the PostsBloc
class in the Application layer.
In summary, the Infrastructure layer is responsible for providing the implementation of the interfaces defined in the Domain layer.
The Application Layer
The Application layer is responsible for handling the business logic of the application. It is also responsible for handling the state of the application. In our case, we will be using the flutter_bloc
package to handle the state of the application.
In the image above, we have the application
folder which contains the posts
folder which represents the posts feature of the application. The posts
folder contains the posts_bloc.dart
, posts_event.dart
and posts_state.dart
files. The posts_bloc.dart
file contains the PostsBloc
class which is responsible for handling the state of the application. The posts_event.dart
file contains the PostsEvent
class which represents the events that can occur in the application. The posts_state.dart
file contains the PostsState
class which represents the state posts feature within the application. It also contains a posts_bloc.freezed.dart
file which is generated by the freezed
package.
Let's start with the PostsEvent
class.
part of 'posts_bloc.dart';
abstract class PostsEvent {}
class GetPosts extends PostsEvent {}
class GenerateInitPosts extends PostsEvent {}
class LikePostPressed extends PostsEvent {
final int postId;
LikePostPressed({required this.postId});
}
class DislikePostPressed extends PostsEvent {
final int postId;
DislikePostPressed({required this.postId});
}
class BookmarkPostPressed extends PostsEvent {
final int postId;
BookmarkPostPressed({required this.postId});
}
class CreatePost extends PostsEvent {
final String content;
final File? photo;
CreatePost({required this.content, this.photo});
}
In the above code block, we have the PostsEvent
class which represents the events that can occur in the application. We have the GetPosts
event which is used to get the posts from the local database. We have the GenerateInitPosts
event which is used to generate initial posts for the application and the class is immutable, which means that it cannot be changed after it has been created. The subsequent events mimick the methods defined in the PostsInterface
in the Domain layer as these are actions that can be performed on the posts.
Next, we have the PostsState
class.
part of 'posts_bloc.dart';
class PostsState with _$PostsState {
const factory PostsState({
required bool isLoading,
required Option<Either<Failure, PostsResponse>> failureOrResponseOption,
required Option<Either<Failure, Unit>> failureOrUnitOption,
}) = _PostsState;
factory PostsState.initial() => PostsState(
isLoading: false,
failureOrResponseOption: none(),
failureOrUnitOption: none(),
);
}
In the above code block, we have the PostsState
class which represents the state of the application. We are using the freezed
package to generate the PostsState
class. The PostsState
class has three properties. The isLoading
property is used to indicate whether the application is loading or not. The failureOrResponseOption
property is used to indicate whether there is an error or a response option. The failureOrUnitOption
property is used to indicate whether there is an error or a void (when the result is a success, but does not return a response) option.
We also have a PostsState.initial()
method which is used to initialize the state of the application and all the properties are set to their default values.
Next, we have the PostsBloc
class with just one example of the methods defined in the class.
part 'posts_bloc.freezed.dart';
part 'posts_event.dart';
part 'posts_state.dart';
class PostsBloc extends Bloc<PostsEvent, PostsState> {
final PostsInterface postsInterface;
PostsBloc(this.postsInterface) : super(PostsState.initial()) {
on<GetPosts>((event, emit) async {
emit(
state.copyWith(
isLoading: true,
failureOrResponseOption: none(),
),
);
final result = await postsInterface.getPosts();
result.fold(
(failure) => emit(
state.copyWith(
isLoading: false,
failureOrResponseOption: some(left(failure)),
),
),
(postsResponse) => emit(
state.copyWith(
isLoading: false,
failureOrResponseOption: some(right(postsResponse)),
),
),
);
});
}
}
In the above code block, we have the PostsBloc
class which is responsible for handling the state of the posts feature. We are using the flutter_bloc
package to handle the state of the application. We are also using the freezed
package to generate the PostsBloc
class. We are using the injectable
package to inject the PostsInterface
class into the PostsBloc
class as a singleton. We are also using the Dartz
package to handle errors and exceptions.
The PostsBloc
class would be injected into the PostsScreen
class in the Presentation layer using flutter_bloc
and get_it
.
The on
method is used to listen for events and perform actions based on the events. The emit
method is used to emit a new state to the application. The state.copyWith
method is used to copy the current state of the application and update the properties of the state. The some
method is used to indicate that the value is present. The none
method is used to indicate that the value is absent. The left
method is used to indicate that the value is an error. The right
method is used to indicate that the value is a success.
The PostsBloc
class would be required to implement all other methods defined in the PostsEvent
.
In summary, the PostsBloc
class is responsible for handling the state of the application. It listens for events and performs actions based on the events. It also emits a new state to the application.
Source code on Github
The source code for Circle is available on Github. You can find it here