Flutter Clean Architecture [3]: An Overview & Project Structure
Tags: frontend, flutter, mobile, architecture
Published: 2023-06-13 15:14:30

> **You can find the completed project repository [here ](https://github.com/marwaMejri/flutter_architecture)**

In the previous [**section**](https://dev.to/marwamejri/flutter-clean-architecture-2-an-overview-project-structure-7o2), we exposed the secure center layer of our app, which operates independently of other layers. Now, let's explore how our app interacts with the outside world, including APIs and third-party libraries.

### Data Layer 

The data layer is composed of **repositories**, **data sources**, and **models**. The repositories are classes that can interact with zero or some data sources in order to expose the data to the rest of the app, while the data sources, as the name implies, are in charge of giving the app the data it needs to perform a function. They can represent data from a variety of sources, including the network, a local database, files, and even memory.

Each data source function should produce a model that captures the information that was retrieved from a particular source. The model is actually an entity with some extra functionality on top of it. In our case, we'll add to/from JSON functions to each model in order to convert it to Dart objects.

#### Implementing the Models

The model file will be located in the data/models folder:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9cj2vczri719scesf46.png)

Each model represents the API data, as was already mentioned. However, we also know that each interface of the app will interact with entities to create its UI, so what should we do? 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dqsedfcyjmo2wyj7xqxy.gif)

I came up with an abstract class called DataMapper whose main role is to map a model into the desired entity. This class will be shared among all app models and will be located within the 'core/utils/mapper' folder:

```Dart
abstract class DataMapper<Type> {
  Type mapToEntity();
}
```
The DataMapper class, as we can see, only has one function that returns the entity Type. To map the appropriate entity, each model will now extend this class and override the **mapToEntity** function.

#### Creating the model class with the use of the data mapper class

So, under **data/models/weather_info_remote_response_model**, I created a new class called **WeatherInfoResponseModel** to represent the weather data:

```Dart
class WeatherInfoResponseModel extends DataMapper<WeatherInfoEntity> {
  WeatherInfoResponseModel(
      {this.coordinateData,
      this.weatherDescription,
      this.mainWeatherData,
      this.weatherVisibility,
      this.windData,
      this.cloudsData,
      this.date,
      this.sunsetAndSunriseData,
      this.timezone,
      this.id,
      this.cityName,
     });
  CoordinateResponseModel? coordinateData;
  List<WeatherDescriptionResponseModel>? weatherDescription;
  MainWeatherInfoResponseModel? mainWeatherData;
  int? weatherVisibility;
  WindInfoResponseModel? windData;
  CloudsResponseModel? cloudsData;
  int? date;
  SunsetSunriseResponseModel? sunsetAndSunriseData;
  int? timezone;
  int? id;
  String? cityName;
  @override
  WeatherInfoEntity mapToEntity() {
    final List<WeatherDescriptionEntity>? _weatherDescription =
        weatherDescription
            ?.map((WeatherDescriptionResponseModel weatherDescriptionEntity) =>
                weatherDescriptionEntity.mapToEntity())
            .toList();
    final WeatherTypeEnum? weatherTypeEnum =
        _weatherDescription?[0].main?.toWeatherType();
    return WeatherInfoEntity(
      main: mainWeatherData?.mapToEntity() ?? const MainWeatherInfoEntity(),
      id: id ?? 0,
      visibility: weatherVisibility?.toKM() ?? '',
      clouds: cloudsData?.mapToEntity() ?? const CloudsEntity(),
      weather: _weatherDescription,
      dt: date?.fromTimestampToDate(),
      name: cityName ?? '',
      sys: sunsetAndSunriseData?.mapToEntity() ?? const SunsetSunriseEntity(),
      timezone: timezone ?? 0,
      wind: windData?.mapToEntity() ?? const WindInfoEntity(),
      weatherTheme: weatherTypeEnum.toWeatherTheme(),
    );
  }
}
```
To map this model into the WeatherInfoEntity class:

**1.** Each sub-model in the WeatherInfoResponseModel should override the mapToEntity function(ex: coordinateData,mainWeatherData,windData..).
**2.** To ensure that WeatherInfoEntity is suitable for creating our interfaces, it is essential to define the appropriate value for each field. For example, I provided an alternative value for null output and made some conversions to meet the desired result, such as converting the visibility field from meters to kilometers.

All that is left is to add to/from JSON functions:

```Dart
 WeatherModel.fromJson(Map<String, dynamic> json) {
    coord = json['coord'] != null ? Coord.fromJson(json['coord']) : null;
    if (json['weather'] != null) {
      weather = <Weather>[];
      json['weather'].forEach((Weather v) {
        weather?.add(Weather.fromJson(v));
      });
    }
    base = json['base'];
    main = json['main'] != null ? Main.fromJson(json['main']) : null;
    visibility = json['visibility'];
    wind = json['wind'] != null ? Wind.fromJson(json['wind']) : null;
    clouds = json['clouds'] != null ? Clouds.fromJson(json['clouds']) : null;
    dt = json['dt'];
    sys = json['sys'] != null ? Sys.fromJson(json['sys']) : null;
    timezone = json['timezone'];
    id = json['id'];
    name = json['name'];
    cod = json['cod'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> map = <String, dynamic>{};
    if (coord != null) {
      map['coord'] = coord?.toJson();
    }
    if (weather != null) {
      map['weather'] = weather?.map((Weather v) => v.toJson()).toList();
    }
    map['base'] = base;
    if (main != null) {
      map['main'] = main?.toJson();
    }
    map['visibility'] = visibility;
    if (wind != null) {
      map['wind'] = wind?.toJson();
    }
    if (clouds != null) {
      map['clouds'] = clouds?.toJson();
    }
    map['dt'] = dt;
    if (sys != null) {
      map['sys'] = sys?.toJson();
    }
    map['timezone'] = timezone;
    map['id'] = id;
    map['name'] = name;
    map['cod'] = cod;
    return map;
  }
```
In the following code snippet, I have to write every line of those functions myself, and if I want to add or delete an attribute of my model, I have to edit it all over again, which makes maintainability difficult.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tmf3pqidf6y3alclwlfy.gif)

After researching, I found that the most popular JSON serialization solution for Flutter is the **['json_serializable'](https://pub.dev/packages/json_serializable)** package created by Google developers. By using this package in combination with **['build_runner'](https://pub.dev/packages/build_runner)** and **['json_annotation'](https://pub.dev/packages/json_annotation)** packages, the fromJSON and toJSON functions can be automatically generated for models that are properly configured and annotated. This eliminates the need for manually writing out all the JSON keys.
As a result of adding configurations, the WeatherInfoResponseModel will look like this:

```Dart
part 'weather_info_response_model.g.dart';
@JsonSerializable()
class WeatherInfoResponseModel extends DataMapper<WeatherInfoEntity> {
  WeatherInfoResponseModel(
      {this.coordinateData,
      this.weatherDescription,
      this.mainWeatherData,
      this.weatherVisibility,
      this.windData,
      this.cloudsData,
      this.date,
      this.sunsetAndSunriseData,
      this.timezone,
      this.id,
      this.cityName,
     });
  factory WeatherInfoResponseModel.fromJson(Map<String, dynamic> json) =>
      _$WeatherInfoResponseModelFromJson(json);
  @JsonKey(name: 'coord')
  CoordinateResponseModel? coordinateData;
  @JsonKey(name: 'weather')
  List<WeatherDescriptionResponseModel>? weatherDescription;
  @JsonKey(name: 'main')
  MainWeatherInfoResponseModel? mainWeatherData;
  @JsonKey(name: 'visibility')
  int? weatherVisibility;
  @JsonKey(name: 'wind')
  WindInfoResponseModel? windData;
  @JsonKey(name: 'clouds')
  CloudsResponseModel? cloudsData;
  @JsonKey(name: 'dt')
  int? date;
  @JsonKey(name: 'sys')
  SunsetSunriseResponseModel? sunsetAndSunriseData;
  int? timezone;
  int? id;
  @JsonKey(name: 'name')
  String? cityName;
  @override
  WeatherInfoEntity mapToEntity() {
    final List<WeatherDescriptionEntity>? _weatherDescription =
        weatherDescription
            ?.map((WeatherDescriptionResponseModel weatherDescriptionEntity) =>
                weatherDescriptionEntity.mapToEntity())
            .toList();
    final WeatherTypeEnum? weatherTypeEnum =
        _weatherDescription?[0].main?.toWeatherType();
    return WeatherInfoEntity(
      main: mainWeatherData?.mapToEntity() ?? const MainWeatherInfoEntity(),
      id: id ?? 0,
      visibility: weatherVisibility?.toKM() ?? '',
      clouds: cloudsData?.mapToEntity() ?? const CloudsEntity(),
      weather: _weatherDescription,
      dt: date?.fromTimestampToDate(),
      name: cityName ?? '',
      sys: sunsetAndSunriseData?.mapToEntity() ?? const SunsetSunriseEntity(),
      timezone: timezone ?? 0,
      wind: windData?.mapToEntity() ?? const WindInfoEntity(),
      weatherTheme: weatherTypeEnum.toWeatherTheme(),
    );
  }
}
```
So, at the top of the file, we included the 'weather info response model.g.dart' part to provide the WeatherInfoResponseModel class access to the methods inside the file that will be generated later using the **build_runner** plugin. We also annotated our class with **@JsonSerializable()** to tell the build runner that this class is involved in the build. Finally, we added the fromJson factory constructor as well as the **JsonKey()** annotation in front of each field to indicate the API key.
Following our exploration of the model structure, we will begin work on the repository implementation and data source contracts.

#### Implementing repository the contract

The Repositories are the app's data layer's brain; they are in charge of:

**1.** Exposing data to the rest of the app.
**2.** Centralizing changes to the data.
**3.** Resolving conflicts between multiple data sources.
**4.** Abstracting sources of data from the rest of the app.

Besides this, the repository implements the logic for deciding whether to collect data from a remote data source or to use results cached in a local database. Each data source class, on the other hand, acts as a bridge between the application and the system for data operations; it is responsible for interacting with just one source of data.

In our case, we'll have two data sources: one for **network data** and another for **local data** to ensure that the user sees anything even when he's not connected to the internet. This indicates that we'll also require a method to determine the user's current network connection state. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2c8ctd7vwcae8x0t3bp.gif)

To meet this requirement by creating the **'ConnectivityCheckerHelper'** class, which allows for checking the connectivity whenever desired. This class can be found in the directory **core/utils/helpers/connectivity_helper**:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6bmq8g5n04hmzsq168k.png)
I used the **['Connectivity_plus'](https://pub.dev/packages/connectivity_plus)** plugin to determine the user's connection status. It offers two functions: one to check the device's connection status, and another that triggers whenever the connectivity state changes:

```Dart
@injectable
class ConnectivityCheckerHelper {
  Future<bool> checkConnectivity() async {
    final ConnectivityResult connectivityResult =
        await Connectivity().checkConnectivity();
    return _handleResult(connectivityResult);
  }

  static Stream<ConnectivityResult> listenToConnectivityChanged() {
    return Connectivity().onConnectivityChanged;
  }

  bool _handleResult(ConnectivityResult connectivityResult) {
    final bool connected;
    if (connectivityResult == ConnectivityResult.mobile ||
        connectivityResult == ConnectivityResult.wifi) {
      connected = true;
    } else {
      connected = false;
    }

    return connected;
  }
}

```
In the previous class, two functions were created: 'checkConnectivity()', which returns true if the user is connected and false otherwise, and 'listenToConnectivityChanged()', which listens to any changes in the connection status.

#### Implementing Data Sources

Now that the 'ConnectivityCheckerHelper' class has been created, it's time to implement our two data sources.

#### Remote Data Source

The data retrieved from the network source is represented by the 'Remote Data Source'. Its implementation can be found in the **'data/datasources/remote_datasource'** directory, consisting of two files: the data source contract and its implementation:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7unowhoxow9wpspuei61.png)

The interface of the **WeatherRemoteDataSource** will be nearly identical to that of the Repository, with two methods: **getWeatherDataByCoordinates** and **getWeatherDataByCity**. As previously noted, the return type of these methods will be based on the response models rather than the entities, therefore each data source method will return the created **WeatherInfoResponseModel** (converted from JSON) using the shared **ApiResultModel**.

```Dart

abstract class WeatherRemoteDataSource {
  Future<ApiResultModel<WeatherInfoResponseModel?>> getWeatherDataByCoordinates(
      {WeatherByCoordinatesRequestModel? weatherByCoordinatesRequestModel});

  Future<ApiResultModel<WeatherInfoResponseModel?>> getWeatherDataByCity(
      {String? cityName});
}
```
I implemented the new interface by creating a class called **WeatherRemoteDataSourceImpl**. This class will use **HTTP** requests to communicate with the server and obtain the needed data.

So, to maintain clean code and follow the DRY principle, I created a helper class that manages the creation of the HTTP client and provides generic HTTP request methods.  The helper class can be found in the **core/utils/helpers/api_call_helper** directory: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c961f3kfz6xjr4l0vse.png)

And for the ApiCallHelper class will look like this:

```Dart
@injectable
class ApiCallHelper {
  ApiCallHelper(this.connectivityCheckerHelper);

  final ConnectivityCheckerHelper connectivityCheckerHelper;
  final String? baseUrl = devBaseUrl;

  Map<String, String> _sharedDefaultHeader = <String, String>{};

  Future<void> initSharedDefaultHeader(
      [String contentValue = contentTypeValue]) async {
    _sharedDefaultHeader = <String, String>{};
    _sharedDefaultHeader.addAll(<String, String>{
      contentTypeKey: contentValue,
    });
  }

  Future<bool> _getConnectionState() async {
    final bool _result = await connectivityCheckerHelper.checkConnectivity();
    return _result;
  }

  ///@[params] should be added to the url as the api params
  Future<ApiResultModel<http.Response>> getWS({
    required String uri,
    Map<String, String> headers = const <String, String>{},
    Map<String, dynamic> params = const <String, dynamic>{},
  }) async {
    await initSharedDefaultHeader();
    _sharedDefaultHeader.addAll(headers);
    if (await _getConnectionState()) {
      try {
        final String _url = '$baseUrl$uri';
        final http.Response response = await http
            .get(_url.parseUri(params: params), headers: _sharedDefaultHeader)
            .timeout(timeOutDuration);
        if (response.statusCode >= 200 && response.statusCode < 300) {
          return ApiResultModel<http.Response>.success(data: response);
        } else {
          return ApiResultModel<http.Response>.failure(
            errorResultEntity: ErrorResultModel(
              message: response.reasonPhrase,
              statusCode: response.statusCode,
            ),
          );
        }
      } on TimeoutException catch (_) {
        return const ApiResultModel<http.Response>.failure(
          errorResultEntity: ErrorResultModel(
            message: commonErrorUnexpectedMessage,
            statusCode: timeoutRequestStatusCode,
          ),
        );
      } on IOException catch (_) {
        throw CustomConnectionException(
          exceptionMessage: commonConnectionFailedMessage,
          exceptionCode: ioExceptionStatusCode,
        );
      }
    } else {
      throw CustomConnectionException(
        exceptionMessage: commonConnectionFailedMessage,
        exceptionCode: ioExceptionStatusCode,
      );
    }
  }
}
```

**1.** The ApiCallHelper class will utilize the previously created ConnectivityCheckerHelper, which will be injected through the constructor. 
**2.** The first method I wrote is **initSharedDefaultHeader**, which will set the default configuration for each request header. For example, I specified application/Json as the default header content type.
**3.** Second, using the connection helper, I wrote a private method called getConnectionState that returns whether or not the user is connected.
**4.** The **getWS** function, which represents the HTTP GET method, is the file's last function. Its usage requires the specification of the request URI, and it also allows for the inclusion of optional extra headers or request parameters. This function returns an HTTP response utilizing the ApiResultModel sealed class. When the response has a 2xx status code, the function returns a success state along with the received data. If the response returns a different status code, the function returns a failure state accompanied by error information. To handle exceptions, particularly network exceptions, the function throws a **CustomConnectionException** that holds the custom exception message.

We may use the same logic to create the other HTTP requests, but our application only needs the GET method. Now that all of the dependencies have been established, it is time to write the remote datasource implementation class:

```Dart
@Injectable(as: WeatherRemoteDataSource)
class WeatherRemoteDataSourceImpl implements WeatherRemoteDataSource {
  WeatherRemoteDataSourceImpl(this._apiCallHelper);

  final ApiCallHelper _apiCallHelper;

  @override
  Future<ApiResultModel<WeatherInfoResponseModel?>> getWeatherDataByCity(
      {String? cityName}) async {
    try {
      final ApiResultModel<Response> _result = await _apiCallHelper.getWS(
          uri: getWeatherDetails,
          params: <String, dynamic>{
            cityNameKey: cityName,
            appIdKey: appIdValue
          });
      return _result.when(
        success: (Response response) {
          return ApiResultModel<WeatherInfoResponseModel?>.success(
            data: WeatherInfoResponseModel.fromJson(
              response.decodeJson(),
            ),
          );
        },
        failure: (ErrorResultModel errorModel) {
          return ApiResultModel<WeatherInfoResponseModel?>.failure(
              errorResultEntity: errorModel);
        },
      );
    } on CustomConnectionException catch (exception) {
      throw CustomConnectionException(
        exceptionMessage: exception.exceptionMessage,
        exceptionCode: exception.exceptionCode,
      );
    }
  }

  @override
  Future<ApiResultModel<WeatherInfoResponseModel?>> getWeatherDataByCoordinates(
      {WeatherByCoordinatesRequestModel?
          weatherByCoordinatesRequestModel}) async {
    try {
      final ApiResultModel<Response> _result = await _apiCallHelper
          .getWS(uri: getWeatherDetails, params: <String, dynamic>{
        latitudeKey: weatherByCoordinatesRequestModel?.lat,
        longitudeKey: weatherByCoordinatesRequestModel?.lon,
        appIdKey: appIdValue,
      });
      return _result.when(
        success: (Response response) {
          return ApiResultModel<WeatherInfoResponseModel?>.success(
            data: WeatherInfoResponseModel.fromJson(
              response.decodeJson(),
            ),
          );
        },
        failure: (ErrorResultModel errorModel) {
          return ApiResultModel<WeatherInfoResponseModel?>.failure(
              errorResultEntity: errorModel);
        },
      );
    } on CustomConnectionException catch (exception) {
      throw CustomConnectionException(
        exceptionMessage: exception.exceptionMessage,
        exceptionCode: exception.exceptionCode,
      );
    }
  }
}
```
The **WeatherRemoteDataSourceImpl** class implements the remote data source contract by overriding its methods. To obtain its response model, each method calls the **getWS** function given by the injected ApiCallHelper object. In addition, if the user is disconnected from the Internet, a **CustomConnectionException** is thrown.

#### Local Data Source 

Until now, the methods we created were always concerned with retrieving data from the server. And now it's time to write the **WeatherLocalDataSourceImpl** class, which is in charge of caching weather data in a local database, retrieving all local weathers, and getting the most recently saved weather data. Additionally, this class will include an abstract class and other classe for implementation. But before that, we will start by creating our local database.

#### Local Database

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eaqnktrwner4id5cikyr.png)

In Flutter, we have numerous options for a local database, but in our situation, I chose the **ObjectBox** database, which is an excellent fit for our needs. This database consumes the least amount of CPU, memory, and battery power, ensuring that your program is not only effective but also long-lasting:

**1.** The **[objectbox](https://pub.dev/packages/objectbox)** plugin is a Flutter database that allows for super-fast NoSQL ACID (atomicity, consistency, isolation, and durability) object persistence.
**2.** The **[objectbox_flutter_libs](https://pub.dev/packages/objectbox_flutter_libs)** plugin, which offers the platform's native ObjectBox library.
**3.** The **[objectbox_generator](https://pub.dev/packages/objectbox_generator)** plugin is used to generate code for our database.

The following step involves adding the local database class, located in the **'datasources/local_datasource/local_database'** directory:

```Dart
class AppLocalDatabase {
  static Store? _store;

  /// Create an instance of ObjectBox to use throughout the app.
  static Future<AppLocalDatabase> create() async {
    final Directory docsDir = await getApplicationDocumentsDirectory();
    _store = await openStore(
      directory: p.join(docsDir.path, 'objectbox'),
    );
    return AppLocalDatabase();
  }

  int? insert<T>(T object) {
    final Box<T>? box = _store?.box<T>();
    return box?.put(object);
  }

  Future<List<T>?> getAll<T>() async {
    final Box<T>? box = _store?.box<T>();
    return box?.getAll();
  }
}
```
**1.** First, I created a static private object of type **Store** to serve as the starting point for implementing **ObjectBox**. It is the database's direct interface and also controls boxes. In reality, a **Box** instance provides access to items of a specific type, similar to a table in a standard database.
**2.** The second step is to open my database and keep it open while my app runs. To do this, I wrote a static create method that returns my **AppLocalDatabase** object, which will be injected, allowing my app to use just one instance.
**3.** Finally, I added two more functions: **insert**, which inserts a new object into the database, and **getAll**, which retrieves all of the stored weather data.

#### Local Data Source Implementation
Now we'll go on to the **WeatherLocalDataSourceImpl** class, which has the injected **AppLocalDatabase** object in its constructor. This class implements three functions: **CacheWeatherInfo**, that caches current weather data, **getLastWeatherInfo**, which returns the most recently saved WeatherInfoEntity object, and **getAllLocalWeatherInfo**, which retrieves all stored weather data:

```Dart
@Injectable(as: WeatherLocalDataSource)
class WeatherRemoteDataSourceImpl implements WeatherLocalDataSource {
  WeatherRemoteDataSourceImpl(this.appLocalDatabase);

  final AppLocalDatabase appLocalDatabase;

  @override
  void cacheWeatherInfo(WeatherInfoResponseModel? weatherInfoResponseModel) {
    final WeatherInfoEntity? weatherData =
        weatherInfoResponseModel?.mapToEntity();
    final WeatherInfoLocalEntity _localEntity = WeatherInfoLocalEntity(
      timezone: weatherData?.timezone,
      name: weatherData?.name,
      dt: weatherData?.dt,
      visibility: weatherData?.visibility,
    );
    _localEntity.sys.target = SunsetSunriseLocalEntity(
      sunset: weatherData?.sys?.sunset,
      country: weatherData?.sys?.country,
      type: weatherData?.sys?.type,
      sunrise: weatherData?.sys?.sunrise,
    );
    _localEntity.weather.addAll(
      <WeatherDescriptionLocalEntity>[
        WeatherDescriptionLocalEntity(
          main: weatherData?.weather?[0]?.main,
          icon: weatherData?.weather?[0]?.icon,
          description: weatherData?.weather?[0]?.description,
        ),
      ],
    );
    _localEntity.main.target = MainWeatherInfoLocalEntity(
      tempMin: weatherData?.main?.tempMin,
      tempMax: weatherData?.main?.tempMax,
      pressure: weatherData?.main?.pressure,
      humidity: weatherData?.main?.humidity,
      feelsLike: weatherData?.main?.feelsLike,
      temp: weatherData?.main?.temp,
    );
    _localEntity.wind.target = WindInfoLocalEntity(
      speed: weatherData?.wind?.speed,
      deg: weatherData?.wind?.deg,
    );
    _localEntity.clouds.target = CloudsLocalEntity(
      all: weatherData?.clouds?.all,
    );
    _localEntity.weatherTheme.target = WeatherThemeLocalEntity(
      firstColorHex: weatherData?.weatherTheme?.firstColor?.value,
      secondColorHex: weatherData?.weatherTheme?.secondColor?.value,
    );
    appLocalDatabase.insert<WeatherInfoLocalEntity>(_localEntity);
  }

  @override
  Future<WeatherInfoEntity?> getLastWeatherInfo() async {
    final List<WeatherInfoLocalEntity>? weatherInfoLocalData =
        await appLocalDatabase.getAll<WeatherInfoLocalEntity>();
    if ((weatherInfoLocalData?.length ?? 0) > 0) {
      final WeatherInfoEntity? _lastInfoData =
          weatherInfoLocalData?.last.mapToEntity();
      return _lastInfoData;
    }
    return null;
  }

  @override
  Future<List<WeatherInfoEntity?>?> getAllLocalWeathers() async {
    final List<WeatherInfoLocalEntity>? weatherInfoLocalData =
        await appLocalDatabase.getAll<WeatherInfoLocalEntity>();
    if ((weatherInfoLocalData?.length ?? 0) > 0) {
      final List<WeatherInfoEntity?>? _localData = weatherInfoLocalData
          ?.map((WeatherInfoLocalEntity element) => element.mapToEntity())
          .toList();
      return _localData;
    }
    return null;
  }
}
```
#### Implementing the Repository

All of the contracts required for the repository's dependencies have been created.These dependencies include local and remote data sources.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0g8qd6k73gl2fkrp4tt.gif)

The **WeatherRepositoryImpl** class will implement the functions specified in its contract, such as obtaining weather data for a specific city via the **getWeatherDataByCity** method, retrieving weather information based on coordinates via the **getWeatherDataByCoordinates** method, and accessing all local weather information via the **getAllLocalWeathers** method. 

Moreover, it's important to remember that the primary function of the Repository is:

> Retrieve up-to-date data from the API when an internet connection is available and to access cached data when the user is offline. 

Additionally, this class is tasked with storing the data retrieved from the server in the ObjectBox database. To achieve this, I have created two private functions: **getLastLocalWeatherInfo**  which is invoked when a **CustomConnectionException** is thrown, and **cacheLocalData** which is utilized when the **getWeatherDataByCity** and **getWeatherDataByCoordinates** functions successfully retrieve data:

```Dart
@Injectable(as: WeatherRepository)
class WeatherRepositoryImpl implements WeatherRepository {
  WeatherRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
  });

  final WeatherRemoteDataSource remoteDataSource;

  final WeatherLocalDataSource localDataSource;

  @override
  Future<ApiResultModel<WeatherInfoEntity?>> getWeatherDataByCity(
      {String? cityName}) async {
    try {
      final ApiResultModel<WeatherInfoResponseModel?> _result =
          await remoteDataSource.getWeatherDataByCity(cityName: cityName);
      return _result.when(
        success: (WeatherInfoResponseModel? weatherInfoResponseModel) async {
          if (weatherInfoResponseModel != null) {
            _cacheLocalData(weatherInfoResponseModel);
          }
          return ApiResultModel<WeatherInfoEntity?>.success(
            data: weatherInfoResponseModel?.mapToEntity(),
          );
        },
        failure: (ErrorResultModel errorResultModel) {
          return ApiResultModel<WeatherInfoEntity>.failure(
            errorResultEntity: errorResultModel,
          );
        },
      );
    } on CustomConnectionException catch (_) {
      final ApiResultModel<WeatherInfoEntity?> _result =
          await _getLastLocalWeatherInfo();
      return _result;
    }
  }

  Future<ApiResultModel<WeatherInfoEntity?>> _getLastLocalWeatherInfo() async {
    final WeatherInfoEntity? _localResult =
        await localDataSource.getLastWeatherInfo();
    return ApiResultModel<WeatherInfoEntity?>.success(
      data: _localResult,
    );
  }

  @override
  Future<ApiResultModel<WeatherInfoEntity?>> getWeatherDataByCoordinates(
      {WeatherByCoordinatesRequestModel?
          weatherByCoordinatesRequestModel}) async {
    try {
      final ApiResultModel<WeatherInfoResponseModel?> _result =
          await remoteDataSource.getWeatherDataByCoordinates(
              weatherByCoordinatesRequestModel:
                  weatherByCoordinatesRequestModel);
      return _result.when(
        success: (WeatherInfoResponseModel? weatherInfoResponseModel) async {
          if (weatherInfoResponseModel != null) {
            _cacheLocalData(weatherInfoResponseModel);
          }
          return ApiResultModel<WeatherInfoEntity?>.success(
            data: weatherInfoResponseModel?.mapToEntity(),
          );
        },
        failure: (ErrorResultModel errorResultModel) {
          return ApiResultModel<WeatherInfoEntity>.failure(
            errorResultEntity: errorResultModel,
          );
        },
      );
    } on CustomConnectionException catch (_) {
      final ApiResultModel<WeatherInfoEntity?> _result =
          await _getLastLocalWeatherInfo();
      return _result;
    }
  }

  void _cacheLocalData(WeatherInfoResponseModel? weatherData) {
    localDataSource.cacheWeatherInfo(weatherData);
  }

  @override
  Future<ApiResultModel<List<WeatherInfoEntity?>?>>
      getAllLocalWeathers() async {
    final List<WeatherInfoEntity?>? _result =
        await localDataSource.getAllLocalWeathers();
    return ApiResultModel<List<WeatherInfoEntity?>?>.success(
      data: _result,
    );
  }
}

```
### Next up
Our progress is remarkable, as we have successfully completed the entire data layer, and the domain layer is already implemented. Now, the final step is to focus on the presentation layer: [**Next**](https://dev.to/marwamejri/flutter-clean-architecture-4-an-overview-project-structure-19jk)