Clean architecture of Flutter application. Part 2 - Practice
Tags: flutter, architecture, dart
Published: 2021-10-03 07:36:09

Hello everyone. In the [previous article](https://dev.to/george_andronchik/clean-architecture-of-flutter-application-part-1-theory-3b6p), we spoke about clean architecture concepts. Now it's time to implement them.

For a good understanding, we are going to develop a simple app. The app receives  info about solar flares and storms from the NASA API and presents it on the view.


### Create Project

First of all, you have to install flutter. You may check [here](https://flutter.dev/docs/get-started/install) the installation process.

After that let’s create a project. There are few ways to do it. The basic way is to create a project by terminal command:
```
flutter create sunFlare
```

Now we have an example of a project. Let's modify it a bit by removing unwanted code and setting up directories.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvbomcoc54w49cys7yod.png)

So, we’ve created directories for each layer (data, domain, and presentation) and another one for the application layer which will contain application initialization and dependency injections. Also, we’ve created files app.dart (app initialization) and home.dart (main view of application). Code of these files you can see below:

**main:**
```dart
import 'package:flutter/material.dart';
import 'package:sunFlare/application/app.dart';
 
void main() {
 runApp(Application());
}
```

**app:**
```dart
import 'package:flutter/material.dart';
import 'package:sunFlare/presentation/home.dart';
 
class Application extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
       visualDensity: VisualDensity.adaptivePlatformDensity,
     ),
     home: Home(),
   );
 }
}
```

**home:**
```dart
import 'package:flutter/material.dart';
 
class Home extends StatefulWidget {
 @override
 _HomeState createState() => _HomeState();
}
 
class _HomeState extends State<Home> {
 @override
 Widget build(BuildContext context) {
   return Scaffold();
 }
}
```

The first step behind and now it’s time to develop a domain.

### Domain

As you can understand from the [previous article](https://dev.to/george_andronchik/clean-architecture-of-flutter-application-part-1-theory-3b6p), the domain layer is the most important part of the application and it’s the first layer you should design. By the way, if you design a backend system, you just think about what entities (aggregators) should exist in the system and design it. So far as we are designing the client application, we already have some initial data (which we fetch from the backend), so we should take it into consideration when designing our domain entities. However, it doesn’t mean we have to use the same data format as we receive from the backend. Our application has its own business logic, so we have to define entities that participate in this process.

Now then, here is our domain-level models:

```dart
import 'package:meta/meta.dart';
 
class GeoStorm {
 final String gstId;
 final DateTime startTime;
 
 GeoStorm({
   @required this.gstId,
   @required this.startTime,
 });
}
```

```dart
import 'package:meta/meta.dart';
 
class SolarFlare {
 final String flrID;
 final DateTime startTime;
 final DateTime endTime;
 final String classType;
 final String sourceLocation;
 
 SolarFlare({
   @required this.flrID,
   @required this.startTime,
   this.endTime,
   @required this.classType,
   @required this.sourceLocation,
 });
}
```

We are going to implement a use case for collecting last solar activities (geo storm and solar flare), so let’s define the model first.

```dart
import 'package:meta/meta.dart';
import 'solar_flare.dart';
import 'geo_storm.dart';
 
class SolarActivities {
 final SolarFlare lastFlare;
 final GeoStorm lastStorm;
 
 SolarActivities({
   @required this.lastFlare,
   @required this.lastStorm,
 });
}
```

Fine. Now we have business-level models. Let’s define protocols for repositories returning these models.

```dart
import 'package:meta/meta.dart';
import 'package:sunFlare/domain/entities/geo_storm.dart';
 
abstract class GeoStormRepo {
 Future<List<GeoStorm>> getStorms({
   @required DateTime from,
   @required DateTime to,
 });
```

```dart
import 'package:meta/meta.dart';
import 'package:sunFlare/domain/entities/solar_flare.dart';
 
abstract class SolarFlareRepo {
 Future<List<SolarFlare>> getFlares({
   @required DateTime from,
   @required DateTime to,
 });
}
```

And as I’ve promised here is a use case.

```dart
import 'package:sunFlare/domain/entities/solar_activities.dart';
import 'package:sunFlare/domain/repos/geo_storm_repo.dart';
import 'package:sunFlare/domain/repos/solar_flare_repo.dart';
 
class SolarActivitiesUseCase {
 final GeoStormRepo _geoStormRepo;
 final SolarFlareRepo _solarFlareRepo;
 
 SolarActivitiesUseCase(this._geoStormRepo, this._solarFlareRepo);
 
 Future<SolarActivities> getLastSolarActivities() async {
   final fromDate = DateTime.now().subtract(Duration(days: 365));
   final toDate = DateTime.now();
 
   final storms = await _geoStormRepo.getStorms(from: fromDate, to: toDate);
   final flares = await _solarFlareRepo.getFlares(from: fromDate, to: toDate);
 
   return SolarActivities(lastFlare: flares.last, lastStorm: storms.last);
 }
}
```

Good. Now let me clarify what we’ve done just now. First of all, we designed the data models we needed for our use case. After that, we found out where to get those models from and defined repository protocols. Finally, we implemented a direct use case, which function is to return the last solar activities. It calls functions of repositories, extracts and collects last solar activities and returns it.

The tree of domain layer directory should looks like this:
![Domain layer directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsqokglh7q7bvo57967k.png)

We’ve just implemented the core of our application — the business logic. Now it's time to take care of the data layer.

### Data

The first step is quite similar to the first step of the previous section - we are going to design the data models which will be fetched from the network.

```dart
import 'package:sunFlare/domain/entities/geo_storm.dart';
 
class GeoStormDTO {
 final String gstId;
 final DateTime startTime;
 final String link;
 
 GeoStormDTO.fromApi(Map<String, dynamic> map)
     : gstId = map['gstID'],
       startTime = DateTime.parse(map['startTime']),
       link = map['link'];
}
```

```dart
import 'package:sunFlare/domain/entities/solar_flare.dart';
 
class SolarFlareDTO {
 final String flrID;
 final DateTime startTime;
 final DateTime endTime;
 final String classType;
 final String sourceLocation;
 final String link;
 
 SolarFlareDTO.fromApi(Map<String, dynamic> map)
     : flrID = map['flrID'],
       startTime = DateTime.parse(map['beginTime']),
       endTime = map['endTime'] != null ? DateTime.parse(map['endTime']) : null,
       classType = map['classType'],
       sourceLocation = map['sourceLocation'],
       link = map['link'];
}
```

DTO means Data Transfer Object. It’s an usual name for transport layer models. The models implement constructors parsing JSON.

The next code snippet contains the implementation of NasaService, which is responsible for NASA API requests.

```dart
import 'package:dio/dio.dart';
import 'package:sunFlare/data/entities/geo_storm_dto.dart';
import 'package:sunFlare/data/entities/solar_flare_dto.dart';
import 'package:intl/intl.dart';
 
class NasaService {
 static const _BASE_URL = 'https://kauai.ccmc.gsfc.nasa.gov';
 
 final Dio _dio = Dio(
   BaseOptions(baseUrl: _BASE_URL),
 );
 
 Future<List<GeoStormDTO>> getGeoStorms(DateTime from, DateTime to) async {
   final response = await _dio.get(
     '/DONKI/WS/get/GST',
     queryParameters: {
       'startDate': DateFormat('yyyy-MM-dd').format(from),
       'endDate': DateFormat('yyyy-MM-dd').format(to)
     },
   );
 
   return (response.data as List).map((i) => GeoStormDTO.fromApi(i)).toList();
 }
 
 Future<List<SolarFlareDTO>> getFlares(DateTime from, DateTime to) async {
   final response = await _dio.get(
     '/DONKI/WS/get/FLR',
     queryParameters: {
       'startDate': DateFormat('yyyy-MM-dd').format(from),
       'endDate': DateFormat('yyyy-MM-dd').format(to)
     },
   );
 
   return (response.data as List)
       .map((i) => SolarFlareDTO.fromApi(i))
       .toList();
 }
}
```

The service contains methods calling API and returning DTO objects.

Now we have to extend our DTO models. We are going to implement mappers from data layer models to domain layer models.

```dart
import 'package:sunFlare/domain/entities/geo_storm.dart';
 
class GeoStormDTO {
 final String gstId;
 final DateTime startTime;
 final String link;
 
 GeoStormDTO.fromApi(Map<String, dynamic> map)
     : gstId = map['gstID'],
       startTime = DateTime.parse(map['startTime']),
       link = map['link'];
}
 
extension GeoStormMapper on GeoStormDTO {
 GeoStorm toModel() {
   return GeoStorm(gstId: gstId, startTime: startTime);
 }
}
```

```dart
import 'package:sunFlare/domain/entities/solar_flare.dart';
 
class SolarFlareDTO {
 final String flrID;
 final DateTime startTime;
 final DateTime endTime;
 final String classType;
 final String sourceLocation;
 final String link;
 
 SolarFlareDTO.fromApi(Map<String, dynamic> map)
     : flrID = map['flrID'],
       startTime = DateTime.parse(map['beginTime']),
       endTime =
           map['endTime'] != null ? DateTime.parse(map['endTime']) : null,
       classType = map['classType'],
       sourceLocation = map['sourceLocation'],
       link = map['link'];
}
 
extension SolarFlareMapper on SolarFlareDTO {
 SolarFlare toModel() {
   return SolarFlare(
       flrID: flrID,
       startTime: startTime,
       classType: classType,
       sourceLocation: sourceLocation);
 }
}
```

And finally it’s time to implement repositories which protocols are in the domain layer.

```dart
import 'package:sunFlare/data/services/nasa_service.dart';
import 'package:sunFlare/domain/entities/geo_storm.dart';
import 'package:sunFlare/domain/repos/geo_storm_repo.dart';
import 'package:sunFlare/data/entities/geo_storm_dto.dart';
 
class GeoStormRepoImpl extends GeoStormRepo {
 final NasaService _nasaService;
 
 GeoStormRepoImpl(this._nasaService);
 
 @override
 Future<List<GeoStorm>> getStorms({DateTime from, DateTime to}) async {
   final res = await _nasaService.getGeoStorms(from, to);
   return res.map((e) => e.toModel()).toList();
 }
}
```

```dart
import 'package:sunFlare/data/services/nasa_service.dart';
import 'package:sunFlare/domain/entities/solar_flare.dart';
import 'package:sunFlare/domain/repos/solar_flare_repo.dart';
import 'package:sunFlare/data/entities/solar_flare_dto.dart';
 
class SolarFlareRepoImpl extends SolarFlareRepo {
 final NasaService _nasaService;
 
 SolarFlareRepoImpl(this._nasaService);
 
 @override
 Future<List<SolarFlare>> getFlares({DateTime from, DateTime to}) async {
   final res = await _nasaService.getFlares(from, to);
   return res.map((e) => e.toModel()).toList();
 }
}
```

The constructors of repositories accept NasaService. Methods call requests, map DTO models to domain models by mappers we have just realized, and return domain models to the domain layer.

This is how the Data directory should look like now.
![Data layer directory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjt02akeg2kcpogwo16o.png)

### User Interface

Almost there. Domain and Data are behind, now it’s time for Presentation Layer.

> I am not going to write a lot about presentation layer architectures in this article. There is a range of variants and if you are interested in this topic, please let me know in the comments.

As long as we decided to use the MVVM pattern for presentation layer architecture, let’s add dependencies for RX.

```
flutter:
 ...
 mobx: any
 flutter_mobx: any
```

Also, we need to add packets to dev dependencies to generate files that allow us to use annotations `@observable`, `@computed`, `@action`. Just a bit of syntax sugar.

```
dev_dependencies:
...
 mobx_codegen: any
 build_runner: any
```

We already have the view — Home. Just add the file called home_state.dart nearby. This file will contain viewModel (which in flutter is usually called state for some reason). And add the code to the file:

```dart
import 'package:mobx/mobx.dart';
import 'package:sunFlare/domain/use_cases/solar_activities_use_case.dart';
import 'package:sunFlare/domain/entities/solar_activities.dart';
 
part 'home_state.g.dart';
 
class HomeState = HomeStateBase with _$HomeState;
 
abstract class HomeStateBase with Store {
 HomeStateBase(this._useCase) {
   getSolarActivities();
 }
 
 final SolarActivitiesUseCase _useCase;
 
 @observable
 SolarActivities solarActivities;
 
 @observable
 bool isLoading = false;
 
 @action
 Future<void> getSolarActivities() async {
   isLoading = true;
   solarActivities = await _useCase.getLastSolarActivities();
   isLoading = false;
 }
}
```

Nothing special here. We call our use case in the constructor. Also, we have two observable properties — solarActivities and isLoading. solarActivities is just the model returned by the use case. isLoading shows us if the request is in progress. The view will subscribe to these variables soon.

To generate class `home_state.g.dart` (to use `@obsevable` annotations), just call the command in terminal:

```
flutter packages pub run build_runner build
```

Let’s come back to our view — home.dart and update it.

```dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:sunFlare/presentation/home_state.dart';
 
class Home extends StatefulWidget {
 HomeState homeState;
 
 Home({Key key, @required this.homeState}) : super(key: key);
 
 @override
 _HomeState createState() => _HomeState();
}
 
class _HomeState extends State<Home> {
 Widget _body() {
   return Observer(
     builder: (_) {
       if (widget.homeState.isLoading)
         return Center(
           child: CircularProgressIndicator(),
         );
       return Column(
         crossAxisAlignment: CrossAxisAlignment.stretch,
         children: [
           Text(
               'Last Solar Flare Date: ${widget.homeState.solarActivities.lastFlare.startTime}'),
           Text(
               'Last Geo Storm Date: ${widget.homeState.solarActivities.lastStorm.startTime}'),
         ],
       );
     },
   );
 }
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(body: SafeArea(child: _body()));
 }
}
```

HomeState object in initializator as an argument and extremely simple UI. If isLoading == true, we show an activity indicator. If not, we present data of the last solar activities.

### Application

Finish him! We have everything we need, now it's time to keep it together. The application layer includes dependency injections and initializations. Create directory dependencies in the application directory and add two files there.

```dart
import 'package:sunFlare/domain/repos/geo_storm_repo.dart';
import 'package:sunFlare/domain/repos/solar_flare_repo.dart';
import 'package:sunFlare/data/repos/geo_storm_repo.dart';
import 'package:sunFlare/data/repos/solar_flare_repo.dart';
import 'package:sunFlare/data/services/nasa_service.dart';
 
class RepoModule {
 static GeoStormRepo _geoStormRepo;
 static SolarFlareRepo _solarFlareRepo;
 
 static NasaService _nasaService = NasaService();
 
 static GeoStormRepo geoStormRepo() {
   if (_geoStormRepo == null) {
     _geoStormRepo = GeoStormRepoImpl(_nasaService);
   }
   return _geoStormRepo;
 }
 
 static SolarFlareRepo solarFlareRepo() {
   if (_solarFlareRepo == null) {
     _solarFlareRepo = SolarFlareRepoImpl(_nasaService);
   }
   return _solarFlareRepo;
 }
}
```

```dart
import 'package:sunFlare/domain/use_cases/solar_activities_use_case.dart';
import 'package:sunFlare/presentation/home_state.dart';
import 'repo_module.dart';
 
class HomeModule {
 static HomeState homeState() {
   return HomeState(SolarActivitiesUseCase(
       RepoModule.geoStormRepo(), RepoModule.solarFlareRepo()));
 }
}
```

Come back to app.dart and throw `HomeModule.homeState()` to Home constructor:

```dart
import 'package:flutter/material.dart';
import 'package:sunFlare/application/dependencies/home_module.dart';
import 'package:sunFlare/presentation/home.dart';
 
class Application extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
       visualDensity: VisualDensity.adaptivePlatformDensity,
     ),
     home: Home(homeState: HomeModule.homeState()),
   );
 }
}
```

Run the application and enjoy the result :)

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r64o24ritasrzek36fr7.png)

Congratulations! We’ve got it. Now you understand how to build the clean architecture of the flutter application. In case you don’t understand something, feel free to ask me in the comments.

**Full code example you can find at** [GitHub](https://github.com/gandronchik/flutter_clean_arch)