PART 2 - Building a Simple Social Feed using DDD Clean Architecture
Source code on Github
The source code for Circle is available on Github. You can find it here
In previous chapters, we discussed about the concept of DDD Clean Architecture. In this article, we will be building a simple social feed using DDD and Clean Architecture and walk through necessary steps to achieve modularity and scalabilty.
The Layers
As we discussed in the previous article, the layers of the DDD Clean Architecture are the Presentation, Application, Domain, and Infrastructure layers. The layers can be applied Flutter applications as shown below.
The image above shows the layers implemented as four different folder under the lib
folder.
We will begin with the Presentation layer. The presentation layer is the entry point of the application. It is the layer that is responsible for displaying the UI to the user.
The Presentation Layer
The image above shows that the presentation layer is the topmost layer of the application. It is the layer that is responsible for displaying the UI to the user. It is also the layer that is responsible for receiving user input and passing it to the application layer.
In our social feed application, the presentation
folder contains three folders: app
, core
, and home
. The core
folder contains the widgets and utils shared across the layer. The app
folder contains app.dart
which is the entry point of the application. The home
folder contains home_screen.dart
and other folders which represent the different features of the application, for example, posts
folder contains the widgets and utils for the posts feature, activity
folder contains the widgets and utils for the activity feature and so on.
Our core
folder contains the subfolders constants
, colors
, widgets
,and theme
. The constants
folder contains the constants used across the application like app sizes. The colors
folder contains the colors used across the application. The widgets
folder contains the widgets used across the entire application. The theme
folder contains the theme data used across the application.
In the home
folder, we also have a widgets
folder, this folder contains the widgets that are shared across the features of the application. For example, the empty_feed.dart
widget is used in the posts_screen.dart
and activity_screen.dart
widgets.
The app.dart
file contains the MaterialApp
widget which is the entry point of the application. The home
property of the MaterialApp
widget is set to the HomeScreen
widget. The HomeScreen
widget is the main screen of the application. It contains our custom BottomNavigationBar as BottomNav
widget which is used to navigate between the different features of the application.
class App extends StatelessWidget {
const App({super.key});
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
return MaterialApp(
title: 'Circle',
debugShowCheckedModeBanner: false,
theme: themeData,
home: const Scaffold(
appBar: AppAppBar(),
body: HomeScreen(),
),
);
}
}
Source code on Github
The source code for Circle is available on Github. You can find it here
In summary, the presentation layer in our application contains only the folders and files that are responsible for displaying the UI and its logic to the user.
The Domain Layer
The image above show our application's domain layer. The domain layer is the core of the application. It is the layer that contains the business rules of the application. It is the layer that contains the models, use cases, and interfaces that are used by the application.
In our application, the domain
folder contains the core
folder which contains utils used across the layer. The posts
folder contains the models, use cases, and interfaces for the posts feature.
The post_response.dart
file contains the Posts
model which is the model for the posts feature. The Post
model contains the id
, content
, time
, handle
, and comments
properties. The Post
model is used to represent a post in the application.
(typeId: 1)
class Post extends HiveObject {
Post({
required this.id,
required this.name,
required this.handle,
required this.time,
required this.content,
this.image,
});
(0)
final int id;
(1)
final String name;
(2)
final String handle;
(3)
final DateTime time;
(4)
final String content;
(5)
final String? image;
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"],
name: json["name"],
handle: json["handle"],
time: DateTime.parse(json["time"]),
content: json["content"],
image: json["image"],
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"handle": handle,
"time": time.toIso8601String(),
"content": content,
"image": image,
};
}
In the above code block, we have a Post
model which is used to represent a post in the application. The Post
model contains the id
, name
, handle
, time
, content
, and image
properties (more properties can be added to the model). We can also see that the Post
model is annotated with the @HiveType
annotation. This annotation is used to tell Hive that the Post
model is a Hive object. The @HiveField
annotation is used to tell Hive that the property is a Hive field.
Hive
is a lightweight and blazing fast key-value database written in pure Dart. It is used to store data locally on the device. We will use Hive
to store the posts locally on the device.
The post_interface.dart
file contains the PostsInterface
interface which is the interface for the posts feature. The PostsInterface
interface contains the getPosts
method which is used to get the posts from the data source and other methods used by the application.
abstract class PostsInterface {
Future<Either<Failure, PostsResponse>> getPosts();
Future<Either<Failure, PostsResponse>> generateInitPosts();
Future<Either<Failure, Unit>> likePostPressed({required int postId});
Future<Either<Failure, Unit>> dislikePostPressed({required int postId});
Future<Either<Failure, Unit>> bookmarkPostPressed({required int postId});
Future<Either<Failure, Unit>> createPost({
required String content,
File? photo,
});
}
In the above code block, we have an abstract class that acts as an interface for the posts feature. We have 6 methods defined in the interface, each of which is responsible for a different task.
Dart doesn't have the interface
keyword like some other languages, but it does have abstract
classes that can be used to define a contract that other classes can implement. This is done by defining an abstract class that defines the required methods and properties, and then creating a concrete class that implements those methods and properties, as we have done above.
In the Infrastructure layer, we will create a concrete class that implements the PostsInterface
interface and see how it is used in the application.
The getPosts
method is used to get the posts from the data source. The generateInitPosts
method is used to generate the initial posts when the application is first launched. The likePostPressed
method is used to like a post. The dislikePostPressed
method is used to dislike a post. The bookmarkPostPressed
method is used to bookmark a post. The createPost
method is used to create a post.
The methods are async
and return a Future
of type Either<Failure, PostsResponse>
or Either<Failure, Unit>
. The Either
class is a class that is used to represent a value of two possible types. The Failure
class is a class that is used to represent a failure. The PostsResponse
class is a class that is used to represent a response from the data source. The Unit
class is a class that is used to represent a void response.
The Either
and Unit
classes are defined in the dartz
package, which we utilize for functional programming in dart. The Failure
class is defined in the core
folder of the domain
layer because it can be used across multiple features in the layer.
In summary, the domain layer in our application contains only the folders and files that are responsible for the business rules of the application.
Source code on Github
The source code for Circle is available on Github. You can find it here