Clean Architecture: Applying with Flutter
Tags: flutter, cleanarchitecture, dart, tutorial
Published: 2022-07-09 18:45:20

Before starting to read this text, it is recommended to have notions of some basic concepts such as [Clean Architecture](https://dev.to/rubemfsv/clean-architecture-the-concept-behind-the-code-52do) and [SOLID](https://dev.to/rubemfsv/solid-the-concept-behind-the-code-3h4o), as they make it easier to understand what will be presented.

This text has two purposes: I. Show an architectural division of a Flutter application using Clean Architecture; II. Guide the implementation of new features in this proposed architecture.

The analyzed code is based on the Clean Architecture approach proposed by [Rodrigo Manguinho](https://www.linkedin.com/in/rmanguinho/) in his [Flutter course](https://rmanguinho.github.io). His approach is aligned with the original proposal by [Robert Martin](http://cleancoder.com/products).

## Architectural Division

The first step is to analyze how the division is done.

```
android/
ios/
lib/
  data/
    cache/
    http/
    models/
    usecases/
  domain/
    entities/
    helpers/
    usecases/
  infra/
    cache/
    http/
  main/
    builders/
    composites/
    decorators/
    factories/
      cache/
      http/
      pages/
      usecases/
    main.dart
  presentation/
    mixins/
    presenters/
    protocols/
  ui/
    assets/
    components/
    helpers/
    mixins/
    pages/
  validation/
    protocols/
    validators/
requirements/
    bdd_specs/
    checklists/
    usecases/
test/
    data/
    domain/
    infra/
    main/
    mocks/
    presentation/
    ui/
    validation/
```

And from there, we can make associations with the [Clean Architecture theory](https://dev.to/rubemfsv/clean-architecture-the-concept-behind-the-code-52do) for an easier understanding of [division of responsibilities](https://dev.to/rubemfsv/clean-architecture-layers-and-boundaries-13ed).

Next, let's take a closer look at the purpose of each file structure.

- **Android**: Contains the files needed to build the application on android systems.
- **iOS**: Contains the files needed to build the application on iOS systems.
- **Lib**: Contains all files needed for the application.
 - **Data**: The data folder represents the data layer of the Clean Architecture, being dependent on the domain layer. Contains the implementations of business rules that are declared in _domain_.
 - **Domain**: Represents the domain layer of the Clean Architecture, the innermost layer of the application, not having any dependency on any other layer, where it contains the business rules.
 - **Infra**: This folder contains the implementations referring to the HTTP protocol and the cache, it is also the only place where you will have access to external dependencies related to these two items mentioned.
 - **Main**: It corresponds to the main layer of the application, where the interfaces developed in the UI layer are integrated with the business rules created in the folders that represent the innermost layers of the Clean Architecture. All this is due to the use of design patterns such as Factory Method, Composite and Builder.
 - **Presentation**: This layer is where the data is prepared to be consumed in the UI, handling the logic behind the screens.
 - **UI**: Contains the components and visual interfaces that are seen in the system, this is where the screens are created.
 - **Validation**: Where it contains the implementations of the validations used in the fields (ex: minimum amount of characters, required field, valid email, among others).
- **Requirements**: Contains documented system requirements, this folder may or may not have all of the following subfolders, it depends a lot on how the team works.
 - **Bdd_specs**: Contains files written in Gherkin language to describe the expected behavior of the system.
 - **Checklist**: It contains the description of the behavior of the pages, in order to facilitate during the unit tests, to know what to validate and what to expect.
 - **Usecases**: It contains the expected behavior of the use cases of the system, where it describes the variations of the business rules to facilitate the unit tests and implementation.
- **Test**: It contains all the unit tests of the application, each internal folder represents the layer that the tests belong to, and the _mocks_ folder contains the mocks used in the tests.

## Implementation Guide

After understanding the reason for the division and what responsibilities are contained in each folder of the structure, a recommended logical sequence for a better implementation performance using this architecture will be described.

In order to simplify the explanation, unit tests will not be described in detail. However, it is strongly recommended to start with unit tests before development (TDD) of each step using the _requirements_ to support the scenarios.

The following demonstration is the creation of the Login flow to log into an application.

### First step: Create business rules in the domain layer

Inside lib/domain/usecases, create `authentication.dart`. This file will be an abstract class that will describe the authentication business rule.

```
import '../entities/entities.dart';

abstract class Authentication {
  Future<AccountEntity> auth(AuthenticationParams params);
}

class AuthenticationParams {
  final String email;
  final String password;

  AuthenticationParams({required this.email, required this.password});
}
```

As we can see, it is an abstract class that has an `auth()` method that receives the _AuthenticationParams_ parameters that are declared below (email and password), and expects to return an `AccountEntity` asynchronously through the _Future_.

_AccountEntity_ is a class created in lib/domain/entities which represents the token that is returned after authentication to persist the session.

```
class AccountEntity {
  final String token;

  AccountEntity({required this.token});
}
```

### Second step: Implement the rules in the data layer

In this layer, we create the use case to implement the rule created previously in the domain layer, but inside lib/data/usecases.

The file usually looks like the example below.

```
import '../../../domain/entities/entities.dart';
import '../../../domain/helpers/helpers.dart';
import '../../../domain/usecases/usecases.dart';

import '../../http/http.dart';
import '../../models/models.dart';

class RemoteAuthentication implements Authentication {
  final HttpClient httpClient;
  final String url;

  RemoteAuthentication({required this.httpClient, required this.url});

  Future<AccountEntity> auth(AuthenticationParams params) async {
    final body = RemoteAuthenticationParams.fromDomain(params).toJson();
    try {
      final hpptResponse =
          await httpClient.request(url: url, method: 'post', body: body);

      return RemoteAccountModel.fromJson(hpptResponse).toEntity();
    } on HttpError catch (error) {
      throw error == HttpError.unauthorized
          ? DomainError.invalidCredentials
          : DomainError.unexpected;
    }
  }
}

class RemoteAuthenticationParams {
  final String email;
  final String password;

  RemoteAuthenticationParams({required this.email, required this.password});

  factory RemoteAuthenticationParams.fromDomain(AuthenticationParams params) =>
      RemoteAuthenticationParams(
          email: params.email, password: params.password);

  Map toJson() => {'email': email, 'password': password};
}
```

As we can see, the _RemoteAuthentication_ class implements the _Authentication_ abstract class, receiving the HTTP client and the url for the request. In the `auth()` method it receives the parameters, and calls the `RemoteAuthenticationParams.fromDomain(params)` factory created below with the purpose of converting what comes in the standard format to the json format to be sent in the HTTP request inside the body. After that, the request is made and the value returned in _httpResponse_ is stored, and this _httpResponse_ is returned in the method inside a model in order to convert the result to the standard format to work (entity) through `RemoteAccountModel.fromJson( hpptResponse).toEntity()`.

This factory and model are created in this layer with the purpose of not polluting the domain layer, because what happens in the data layer should not influence what happens in the domain layer.

For the sake of curiosity, the implementation of _RemoteAccountModel_ is below. It takes an accessToken in json format and converts it to an Entity.

```
import '../../domain/entities/entities.dart';
import '../http/http.dart';

class RemoteAccountModel {
  final String accessToken;

  RemoteAccountModel(this.accessToken);

  factory RemoteAccountModel.fromJson(Map json) {
    if (!json.containsKey('accessToken')) {
      throw HttpError.invalidData;
    }
    return RemoteAccountModel(json['accessToken']);
  }

  AccountEntity toEntity() => AccountEntity(token: accessToken);
}
```

### Third step: Implement the screens in the UI layer

To simplify the understanding, only code snippets referring to the authentication method call will be presented. The Login screen contains more actions and details that go beyond authentication. Consider the screen prototype below for easier visualization.

![Login screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtgap03reh4ix8ntu091.png)

In lib/ui/pages/ you will need at least two files: I. `login_page.dart`, which will be the Login page; II. `login_presenter.dart` which will contain the abstract class with the methods and streams that are used on the page, and the implementation of this abstract class takes place in the presentation layer.

The `login_presenter.dart` file is similar to the example below.

```
import 'package:flutter/material.dart';

abstract class LoginPresenter implements Listenable {
  void validateEmail(String email);
  void validatePassword(String password);

  Future<void> auth();
}
```

And the code below is for the Login button.

```
class LoginButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final presenter = Provider.of<LoginPresenter>(context);

    return StreamBuilder<bool>(
      stream: presenter.isFormValidStream,
      builder: (context, snapshot) {
        return ElevatedButton(
          onPressed: snapshot.data == true ? presenter.auth : null,
          child: Text(R.translations.enterButtonText.toUpperCase()),
        );
      },
    );
  }
}
```

The `auth()` method when clicked on `onPressed` calls the presenter to do the authentication. In this case, you are not passing parameters in `auth()` because the parameters are taken from the presenter when interacting with the screen (the `validateEmail()` and `validatePassword()` declared above in the page presenter are used). We will see more details in the next step.

### Fourth step: Implement the UI's abstract presenter class in the presentation layer

To make it easier to work with Streams, it is recommended to use the [GetX](https://pub.dev/packages/get) library (or any other one of your choice) to make it less verbose. The choice for _GetX_ is due to its great support and being constantly updated.

In lib/presentation/presenters, `getx_login_presenter.dart` is created. It is a _GetxLoginPresenter_ class that extends _GetxController_ and implements _LoginPresenter_. Although the example below has _Validation_ and _SaveCurrentAccount_, we will focus only on _Authentication_.

```
import 'dart:async';
import 'package:get/get.dart';

import '../../ui/pages/login/login_presenter.dart';
import '../../domain/helpers/domain_error.dart';
import '../../domain/usecases/usecases.dart';
import '../protocols/protocols.dart';

class GetxLoginPresenter extends GetxController
  implements LoginPresenter {
  final Validation validation;
  final Authentication authentication;
  final SaveCurrentAccount saveCurrentAccount;

  String? _email;
  String? _password;

  GetxLoginPresenter({
    required this.validation,
    required this.authentication,
    required this.saveCurrentAccount,
  });

  void validateEmail(String email) {
    _email = email;
    // Validation code here
  }

  void validatePassword(String password) {
    _password = password;
    // Validation code here
  }

  Future<void> auth() async {
    try {
      final account = await authentication.auth(AuthenticationParams(
        email: _email!,
        password: _password!,
      ));
      await saveCurrentAccount.save(account);
    } on DomainError catch (error) {
      // Handle errors here
    }
  }
}
```

In the `validateEmail(String email)` and `validatePassword(String password)` methods, the user's email and password are captured when typing in the _Inputs_ of the Login screen. In `auth()`, this is where there is a call to the previously implemented authentication method, which receives the email and password captured by the previous validates.

The call `authentication.auth(AuthenticationParams(email: _email!, password: _password!))` returns a token (as explained earlier), is assigned to a variable called _account_ and then cached via `saveCurrentAccount.save( account)` (this point was not explained in this text, but it is through it that the user session persists on the device).

### Fifth step: Connect all layers for requests to work

After everything is implemented, now just connect all the parts. For this, the design pattern [Factory Method](https://refactoring.guru/design-patterns/factory-method) is used.

Inside lib/main/factories/usecases we create the _factory_ of the use case being implemented. In the case of this example, it is related to authentication.

The _authentication_factory.dart_ is created, which returns the _RemoteAuthentication_ that receives as a parameter the _factory_ of the _Http Client_ and the _factory_ that creates the URL. The URL of the API you want to request is passed as a parameter along with the _factory_ that creates the URL. In the example it is the URL that ends with _/login_.

```
import '../../../domain/usecases/usecases.dart';
import '../../../data/usecases/usecases.dart';
import '../factories.dart';

Authentication makeRemoteAuthentication() {
  return RemoteAuthentication(
    httpClient: makeHttpAdapter(),
    url: makeApiUrl('login'),
  );
}
```

After that, in lib/main/factories/pages, the folder for the Login factories is created. For this explanation, we will focus on _login_page_factory.dart_ and _login_presenter_factory.dart_.

First, the _login_presenter_factory.dart_ is made, which is a Widget that returns the _GetxLoginPresenter_. This _presenter_ was created previously, and inside it is injecting the authentication _factories_ (which was created just above) and the validation and saving token in the cache (which were not covered in this text, but follow the same premises of _ makeRemoteAuthentication_) .

```
import '../../factories.dart';
import '../../../../presentation/presenters/presenters.dart';
import '../../../../ui/pages/pages.dart';

LoginPresenter makeGetxLoginPresenter() {
  return GetxLoginPresenter(
    authentication: makeRemoteAuthentication(),
    validation: makeLoginValidation(),
    saveCurrentAccount: makeLocalSaveCurrentAccount(),
  );
}
```

Then, following the same line of thought, the _factory_ of the Login page is made. Like the _factory_ of the _presenter_, it's a Widget, but in this case it returns the _LoginPage_ with the _factory_ of the _presenter_ created earlier being injected as a parameter.

```
import 'package:flutter/material.dart';
import '../../../../ui/pages/pages.dart';
import '../../factories.dart';

Widget makeLoginPage() {
  return LoginPage(makeGetxLoginPresenter());
}
```

### Sixth step: Apply the screen created in the application

Finally, it is necessary to call the Login _factory_ in the application, so that it can be accessed by the user.

In the _main.dart_ file located in lib/main, add the _factory_ page created into the page array (_getPages_). In the route, is passed the _name_ of the rout - in this case it is _/login_ - and the page, which in this case is the pointer to the _factory_ `makeLoginPage`. This logic is used with all other pages. The code looks like it is below.

```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

import '../ui/components/components.dart';
import 'factories/factories.dart';

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
    final routeObserver = Get.put<RouteObserver>(RouteObserver<PageRoute>());

    return GetMaterialApp(
      title: 'Flutter Clean App',
      debugShowCheckedModeBanner: false,
      theme: makeAppTheme(),
      navigatorObservers: [routeObserver],
      initialRoute: '/',
      getPages: [
        GetPage(
            name: '/login', page: makeLoginPage, transition: Transition.fadeIn),
      ],
    );
  }
}
```

## Conclusion

Although it is a little complex to understand at first and seems a bit redundant, abstractions are necessary. Various design patterns are applied to ensure code quality and independence, facilitating evolution and maintenance.

Following the development process and understanding why you are doing it in such a way makes code production easier. After a while it ends up being done naturally, as it has a linear development process: I. Use case in the domain layer; II. Use case in the data layer; III. UI creation; IV. Creation of logics to call the request in the presentation layer; V. Creation of _factories_ to integrate all layers into the main layer; VI. And then the call of the main _factory_ in the application routes so that it is available to the user.

Despite having many abstracted parts, it is recommended to read the code of the hidden parts for a better understanding. [In this repository](https://github.com/rmanguinho/clean-flutter-app) from Rodrigo Manguinho's course you can access these abstracted codes.

## References

* Rodrigo Manguinho https://github.com/rmanguinho/clean-flutter-app
* MARTIN, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. 1st. ed. USA: Prentice Hall Press, 2017. ISBN 0134494164.