You can find the completed project repository here
In the initial section, we learn the fundamentals of clean architecture and set up the necessary folders to organize each layer. Now let's dive into these empty folders, starting with the Domain layer.
So let's start the party, enough talking.
Domain Layer
Entity
First, we add our entity that represents the weather data.
To determine which fields this class must have, we must examine the Weather API response. The following JSON response is returned by this API:
{
"coord": {
"lon": 10.637,
"lat": 35.8254
},
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02d"
}
],
"base": "stations",
"main": {
"temp": 304.03,
"feels_like": 307.27,
"temp_min": 303.02,
"temp_max": 304.03,
"pressure": 1014,
"humidity": 58
},
"visibility": 10000,
"wind": {
"speed": 5.66,
"deg": 100
},
"clouds": {
"all": 20
},
"dt": 1663066385,
"sys": {
"type": 1,
"id": 1193,
"country": "TN",
"sunrise": 1663045105,
"sunset": 1663090127
},
"timezone": 3600,
"id": 2464915,
"name": "Sousse",
"cod": 200
}
Except for the coord, base, and cod fields, we are interested in the majority of the fields in this answer. As a result, our entity will resemble this:
class WeatherInfoEntity extends Equatable {
const WeatherInfoEntity({
this.weather,
this.main,
this.visibility,
this.wind,
this.clouds,
this.dt,
this.sys,
this.timezone,
this.id,
this.name,
this.weatherTheme,
});
final List<WeatherDescriptionEntity?>? weather;
final MainWeatherInfoEntity? main;
final String? visibility;
final WindInfoEntity? wind;
final CloudsEntity? clouds;
final String? dt;
final SunsetSunriseEntity? sys;
final int? timezone;
final int? id;
final String? name;
final WeatherThemeEntity? weatherTheme;
@override
List<Object?> get props => <Object?>[
weather,
main,
visibility,
wind,
clouds,
dt,
sys,
timezone,
id,
name,
weatherTheme,
];
}
The WeatherInfoEntity class extends the Equatable class from the equatable plugin to allow for simple value comparisons without all the boilerplate, as Dart only supports referential equality by default. Also, if you notice that I added a new field of type WeatherThemeEntity, this field will help me specify the proper theme for a certain weather. It is not required to provide such a field, but it allows for a greater connection with the application.
class WeatherThemeEntity {
WeatherThemeEntity({
this.firstColor,
this.secondColor,
});
Color? firstColor;
Color? secondColor;
}
So, the WeatherThemeEntity class will have two color type fields, in which I will add a gradient background for each type of weather.
Repository & Error handling
As indicated earlier, the repository belongs to both the domain and data layers. In fact, it will be defined in the domain first and then implemented in data. This enables 100% independence of the domain layer and also helps with developing tests for the use cases without having an actual repository implementation. So, how will the repository contract look?
In our contract, we define three methods for our weather functionality: one to retrieve weather data by coordinates, another to obtain weather for a specific city, and a third to retrieve all saved weather information locally. Logically, the return types for the first two methods should be Future, and for the final method, Future>. However, error handling is also an important consideration. We have two options: allowing exceptions to propagate and handling them elsewhere, or catching exceptions early and returning an error object from the methods.
In our architecture, I have chosen the second approach. This means that any method in the repository or use case will return both the entity and failure objects, providing a clear way to handle both successful outcomes and potential errors.
So , to implement it, I used the freezed plugin to create a sealed class named *ApiResultModel * that will return either failure with an error object or success with the specific data.
sealed classes are used to represent restricted class hierarchies, where a value can have one of a restricted set's types but not another. They are also an extension of enum classes, except that an enum constant can only have one instance, but a sealed class can have several instances.
Furthermore, the freezed plugin will generate extra functionality like copyWith and toString methods, but more importantly, it will give us access to some pattern-matching syntax.
To use this plugin, you must first install the following dependencies:
cupertino_icons: ^1.0.5
equatable: ^2.0.5
freezed: ^2.3.3
get_it: ^7.6.0
injectable: ^2.1.2
json_annotation: ^4.7.0
Following that, I created a 'commondomain' subfolder under the 'core' folder. This 'commondomain' subfolder will further include an 'entities' subfolder. Additionally, I have introduced a 'based_api_result' subfolder intended for storing entities that will be shared across different sections of our project:
Creating the sealed class
The ApiResultModel sealed class will have two states: success and failure. The success state will return the data type specified when calling our sealed class. For example, if we want the method to return a WeatherInfoEntity on success, the return type should be ApiResultModel. When a problem occurs, the return type is an ErrorResultModel object, as defined in the failure state.
To create the two states, we employ factory constructors and include the necessary annotations and keywords. Make sure to add the '@freezed' annotation and the 'part' keyword at the top of the file so the plugin can build the methods specified previously:
part 'api_result_model.freezed.dart';
@freezed
class ApiResultModel<T> with _$ApiResultModel<T> {
const factory ApiResultModel.success({required T data}) = Success<T>;
const factory ApiResultModel.failure(
{required ErrorResultModel errorResultEntity}) = Failure<T>;
}
The error object includes two attributes: one for the API error status code and one for the message:
class ErrorResultModel extends Equatable {
const ErrorResultModel({
this.statusCode,
this.message,
});
final int? statusCode;
final String? message;
@override
List<Object?> get props => <Object?>[
statusCode,
message,
];
}
Creating the repository contract with the use of the sealed class
So, in our abstract repository class, we'll have three methods: getWeatherDataByCoordinates, getWeatherDataByCity, and getAllLocalWeathers. These methods return Future>, where T is the data type:
abstract class WeatherRepository {
Future<ApiResultModel<WeatherInfoEntity?>> getWeatherDataByCoordinates(
{WeatherByCoordinatesRequestModel? weatherByCoordinatesRequestModel});
Future<ApiResultModel<WeatherInfoEntity?>> getWeatherDataByCity(
{String? cityName});
Future<ApiResultModel<List<WeatherInfoEntity?>?>> getAllLocalWeathers();
}
Use cases & callable classes
Any use case must have the *WeatherRepository * object, which will be provided in the constructor, as we know the use case corresponds to a repository method. In light of this, we need to add three use cases to our application: one for retrieving weather data based on coordinates, one for retrieving weather data based on city, and one for obtaining all previously stored weathers from a local database.
To begin, I will add the required repository for the GetWeatherDataByCoordinates use case:
@injectable
class GetWeatherDataByCoordinates {
GetWeatherDataByCoordinates(this.weatherRepository);
final WeatherRepository weatherRepository;
}
Next, all that remains is to add the function that will retrieve the data from the repository:
@injectable
class GetWeatherDataByCoordinates {
GetWeatherDataByCoordinates(this.weatherRepository);
final WeatherRepository weatherRepository;
Future<ApiResultModel<WeatherInfoEntity?>> getWeatherData(
WeatherByCoordinatesRequestModel? weatherByCoordinatesRequestModel) {
return weatherRepository.getWeatherDataByCoordinates(
weatherByCoordinatesRequestModel: weatherByCoordinatesRequestModel);
}
}
So far, we have made good progress on our weather app. We have created an Entity, a Repository contract, and the first use case. To complete the work at the domain layer, we only need to create the remaining two use cases.
In regards to use cases, it is important to remember that each should have a return method that interacts with the repository. It makes no difference whether the logic inside the useCase retrieves a WeatherInfoEntity or something else, the goal is the same. And to improve the solution further, we could create a base class with a method that returns a generic type that can be extended by each specific use case.
creating the base use case class
To create the base useCase class, I implemented an explicit interface (in Dart, this is an abstract class) located in the core/commondomain/usecases directory since this class can be shared across multiple features of our app:
abstract class BaseParamsUseCase<Type, Request> {
Future<ApiResultModel<Type>> call(Request? params);
}
class NoParams extends Equatable {
@override
List<Object> get props => <Object>[];
}
As you can see, we added two type parameters to the BaseParamsUseCase: one represents the data type in case of success, and the Request type is for the parameters that will be passed through the use case, such as the latitude and longitude for the previous 'GetWeatherDataByCoordinates'. So, in order to implement the base class, each use case must specify the parameter Request that will be passed to the "call" method. However, if a use case does not require any parameters, we can use the NoParams class as the Request type, as in the case of 'GetAllLocalWeathers'.
The name of the 'call' method was not picked randomly. In fact, the 'call' method in the Dart programming language allows you to define a class as callable, meaning that you can use an instance of the class as if it were a function. This method is well-suited for use cases where the class names are verbs, such as 'GetWeatherDataByCoordinates'.
Extending the Base Class
Adding a use case is now very simple: each use case should implement the previous base class and override the calling method. For example, for 'GetWeatherDataByCoordinates', we implement the 'BaseParamsUseCase' with the addition of two parameters: one representing the return type of this use case and the other representing the parameters to be passed into it, which in our case is the 'WeatherByCoordinatesRequestModel', which holds the required latitude and longitude values:
class GetWeatherDataByCoordinates
implements
BaseParamsUseCase<WeatherInfoEntity?,
WeatherByCoordinatesRequestModel> {
GetWeatherDataByCoordinates(this.weatherRepository);
final WeatherRepository weatherRepository;
@override
Future<ApiResultModel<WeatherInfoEntity?>> call(
WeatherByCoordinatesRequestModel? weatherByCoordinatesRequestModel) {
return weatherRepository.getWeatherDataByCoordinates(
weatherByCoordinatesRequestModel: weatherByCoordinatesRequestModel);
}
}
class WeatherByCoordinatesRequestModel {
WeatherByCoordinatesRequestModel({
this.lat,
this.lon,
});
final double? lat;
final double? lon;
}
The other two use cases will follow the same logic as the one previously mentioned:
@injectable
class GetWeatherDataByCity
implements BaseParamsUseCase<WeatherInfoEntity?, String> {
GetWeatherDataByCity(this.weatherRepository);
final WeatherRepository weatherRepository;
@override
Future<ApiResultModel<WeatherInfoEntity?>> call(String? cityName) {
return weatherRepository.getWeatherDataByCity(cityName: cityName);
}
}
@injectable
class GetAllLocalWeathers
implements BaseParamsUseCase<List<WeatherInfoEntity?>?, NoParams> {
GetAllLocalWeathers(this.weatherRepository);
final WeatherRepository weatherRepository;
@override
Future<ApiResultModel<List<WeatherInfoEntity?>?>> call(NoParams? params) {
return weatherRepository.getAllLocalWeathers();
}
}
Next up
In the next part, we will start working on the data layer containing the Repository implementation and Data Sources: Next
Top comments (4)
Hello Marwa! I usually don't make comments on posts, I just follow and as soon as I get the information I want I return to do my stuffs but the way you lead this post is quite parfect. It would be ungrateful of me to turn around without telling you. I imagine all the time it took you to go at that detail level, not just with technical terms and to unfold but to use simple and understandable expressions. Taking the hand and going step by step, it's just perfect. I also love your funny cats that you put everywhere to liven up and not make the post too serious. I'm so verbous because I'm a newbie at Flutter clean architecture and I was looking so far for a so comprehensive way to reproduce the structure in any project but the tutos I found on YouTube don't satisfy me until now. So, with all that been said, I want to ask you if the model classes in the data layer should extend their entity correspondant classes in domain. And if the response is yes, tell me why. I will bother you for a moment because I may also have other questions.
Thank you so much for your kind words. It means a lot to me that you took the time to share your appreciation. I'm glad you found the information helpful and the approach engaging. Your feedback really encourages me to continue sharing detailed and understandable content. And I'm thrilled to hear that you enjoyed the funny cat images—they do add a bit of fun, don't they?
As for your question about Flutter clean architecture: In the data layer, whether model classes should extend their corresponding entity classes in the domain layer is a common point of discussion. Typically, the model classes in the data layer do not extend the entity classes in the domain layer. Instead, they are converted to and from entities.
Will you teach me flutter
with pleasure