Flutter Mobile — Clean architecture (part 1)

AbedElaziz Shehadeh
6 min readFeb 1, 2022

Building Flutter apps with a clean architecture approach

Introduction

Building applications without following a proper architecture can be problematic in the long term when the application grows and if multiple developers are working on it simultaneously — which is the case for most of the time. Therefore, planning and following a clear coding architecture is a crucial step in building an application regardless of the technology used, whether it is mobile, web, backend, native or multiplatform, to ensure it’s reliable, maintainable, scalable, and most importantly; testable.

This article focuses on the attempt to build a Flutter mobile application following the clean architecture and the dependency rule approach.

If you to see the step-by-step implementation, you can skip this article and jump to Flutter Mobile — Clean architecture (part 2).

Why Flutter?

I have been focusing lately on building mobile applications (and exploring web) using Flutter Multiplatform technology for the following reasons:

  1. Same UI and Business Logic in all platforms: sharing code between platforms meaning writing less code which leads to generally less bugs, faster development time that in turn increases time-to-market speed.
  2. The closest to native development when it comes to performance and look and feel.
  3. The possibility to be used beyond mobile as Flutter supports Web, Mac, Windows, and Linux.
  4. Flutter and Dart are slightly easier to learn compared to other technologies.
  5. Simple and straightforward implementation to access and write platform-specific logic so you can easily integrate internal and external native SDKs and create Dart wrappers if needed.
  6. Out-of-the-box support for tons of UI and animation components to allow building the UI easier and faster so developers can focus on what really matters — The Business Logic-.

Flutter Mobile App Architecture

I have tried to experiment building a Flutter application using the Clean Architecture concept outlined by Robert Martin. The main idea is to separate the code into loosely coupled layers, therefore, maintainability, scalability and testability could be achieved. These architecture layers are:

  • Data layer: this layer contains the logic needed to interact with data sources like cache (databases, secure storage, shared preferences), remote (API services), or any other data source that the application could require. In other words, this layer provides a single source of truth for data.
  • Domain layer: it contains UseCases that encapsulates a single and a very specific task to be performed. This layer extracts business logic from the presentation layer, such as from BLoC to make the presentation’s BLoC class simpler with a sole job to coordinate between views and use cases.
  • Presentation layer: this layer contains UI (views, widgets), and BLoC state management which exposes states that the UI would consume.

Advantages

Before going deeper into each layer, let’s look into the advantages of following such a pattern:

  1. This structure provides a better separation of concerns where each layer has a clear API of what it is supposed to do and the features classes live in different layers where referencing them is not possible without an explicit module dependency.
  2. Various features can be developed in parallel and in isolation, e.g., one team can work only on preparing the API services, defining data models and what each API consumes and produces, knowing how the UseCase looks like at this stage is not needed. Same goes to UseCases where one can focus on performing a specific task without the need to know how the data looks or where it is coming from.
  3. This structure makes it easier to test the business logic without the UI, databases, API services or any other external elements.
  4. Independent of dependencies such as Frameworks, databases, any 3rd party dependencies because the business logic is not bound to any of them.
  5. This pattern forces some kind of a standard coding style. Usually, when the app grows with lots of features added and many developers working on it, it’s easier for the source code to become messy. By following this kind of code separation, making changes, and adding new features will require a similar list of steps to be followed by everyone.

Now let’s go back to explain each layer.

Image 1: The proposed architecture

The Data layer can only access Domain layer and contains the following:

  1. Repositories implementation for the repository interfaces that the Domain layer defines. For example, Domain defines a contract for featureX related functionality and Data creates an implementation and obtains the data from a data source. This is useful because we have the ability to change or add multiple implementations without interacting with Domain.
  2. Data sources which implement the logic of data access from different sources, such as Cache and Remote. With such an approach, a new data source can be added with ease.
  3. Data sources factory which provides a mechanism to choose the right data source when needed.

The Domain layer cannot access and does not have knowledge about the outside world. It includes the following:

  1. Use cases which execute a specific task. These cases can combine data from one or multiple repositories, for example, after performing login from remote, save the access token to a secure storage at cache.
  2. Models which declare the data format needed.

The Presentation layer in our approach represents lib directory which contains Flutter framework related dependencies. This layer will have access to all other layers for dependency injection purposes, where ideally, only the Domain layer is required. In addition, it contains the following:

  1. Flutter views and Widgets such as Stateless and Stateful widgets.
  2. BLoCs which stand for Business logic component — a state management approach in Flutter — classes that would execute one or more Use Cases and emit states to the view/widget that is bound to it.

In general, this is the architecure I propose. Let me take a step back to explain what’s going on at the Data layer.

Data Layer Structure

As mentioned before, data layer has data sources:

  1. Remote: this is a pure Dart code and will handle remote API communication with the backend.
  2. Cache: this is a pure Dart code and will handle offline data persistence. SQLite DB, Shared Preferences, and Secure Storage, etc can be used here.
  3. Any other sources: it could be any other data sources the project may require.

In this architecture, I propose extracting the data sources (cache and remote) into separate packages or plugins instead of having the implementation into one package. Data will then define a repository interface/abstraction which each of these sources will implement.

What do we mean by packages and plugins?

Packages vs Plugins

In Flutter, packages and plugins are a way to enable the creation of modular code that can be easily shared. In this architecture, Domain, Data, Remote, and Cache are built as packages. The main difference between package and plugins is that Packages are written in Pure Dart, however, plugins contain an API written in Dart combined with one or more platform-specific implementations, therefore, if you have a native library of some kind which could be used as a data source, you can build it as a plugin and create a dart wrapper.

The 2 sources will have Data as dependency to implement their interfaces. That way, each of these data sources could be developed in isolation and could be used in this project or other projects if needed. It is worth mentioning that these packages and plugins can be added as local path, git, or submodule dependency.

So let’s have a look at the general folder structure for this architecture;

Image 2: Folder’s structure

Recommended Core Packages

Challenges and Cons

  • Boilerplate code especially when mapping entities and models and defining abstractions.
  • This architecture could be a bit difficult to start with.

Conclusion

The proposed architecture will be tested and validated further to detect any potential problems and try to improve it where applicable.

If you find an issue or have different ideas on how this could be built better, you can let me know in the comments.

You can access the full source code here:

What’s next?

--

--