Keeping your code clean and tested are the two most important development practices. In Flutter, this is even more true than with other frameworks. On one hand, it's nice to hack a quick app together, on the other hand, larger projects start falling apart when you mix the business logic everywhere. Even state management patterns like BLoC are not sufficient in themselves to allow for easily extendable codebase.

TDD Clean Architecture Course This post is just one part of a tutorial series. See all of the other parts here and learn to architect your Flutter apps!

The Secret to Maintainable Apps

This is where we can employ clean architecture and test driven development. As proposed by our friendly Uncle Bob, we should all strive to separate code into independent layers and depend on abstractions instead of concrete implementations. How can such an independence be achieved? Although we're getting ahead of ourselves a bit, on the layered "onion" image below, the horizontal arrows ---> represent dependency flow. For example, Entities do not depend on anything, Use Cases depend only on Entities etc.

All of this is, well, a bit abstract (pun intended). Also, while the essence of clean architecture remains the same for every framework, the devil lies in the details. Principles like SOLID and YAGNI sound nice, you may even understand what they mean, but it won't do you any good if you don't know how to start writing clean code.

The Project We Will Build

As with anything, theory can only take you so far - the real learning comes from doing. We will build an app for getting interesting facts about numbers - a Number Trivia App!

It has all the core components such as getting data from an API, local cache, error handling, input validation and more. As for the state management, I decided to go with BLoC. You don't have to! I cannot stress this enough. Clean architecture is not about a particular state management technique. As you will see in a short while, architecting your app properly will make the choice of state management almost inconsequential. I still wouldn't choose the StatefulWidget approach, but anything else is cool.

Clean Architecture & Flutter

To make things clear and Flutter-specific, let me introduce you to Reso Coder's Flutter Clean Architecture Proposal™ to demonstrate something, dare I say, more important than the dependency flow - data & call flow. Of course, this is only a high-level overview which may or may not tell you much, depending on your previous experience. We will dissect and apply this diagram to our Number Trivia App in a short while.

Explanation & Project Organization

Every "feature" of the app, like getting some interesting trivia about a number, will be divided into 3 layers - presentation, domain and data. The app we're building will have only one feature.

this VS Code extension. To make creating individual feature folders easier, you can use

Presentation

This is the stuff you're used to from "unclean" Flutter architecture. You obviously need widgets to display something on the screen. These widgets then dispatch events to the Bloc and listen for states (or an equivalent if you don't use Bloc for state management).

Note that the "Presentation Logic Holder" (e.g. Bloc) doesn't do much by itself. It delegates all its work to use cases. At most, the presentation layer handles basic input conversion and validation.

Applied to the Number Trivia App

We will have only a single page with widgets called NumberTriviaPage with a single NumberTriviaBloc.

Domain

Domain is the inner layer which shouldn't be susceptible to the whims of changing data sources or porting our app to Angular Dart. It will contain only the core business logic (use cases) and business objects (entities). It should be totally independent of every other layer.

Use Cases are classes which encapsulate all the business logic of a particular use case of the app (e.g. GetConcreteNumberTrivia or GetRandomNumberTrivia).

But... How is the domain layer completely independent when it gets data from a Repository, which is from the data layer? Do you see that fancy colorful gradient for the Repository? That signifies that it belongs to both layers at the same time. We can accomplish this with dependency inversion.

That's just a fancy way of saying that we create an abstract Repository class defining a contract of what the Repository must do - this goes into the domain layer. We then depend on the Repository "contract" defined in domain, knowing that the actual implementation of the Repository in the data layer will fullfill this contract.

Dependency inversion principle is the last of the SOLID principles. It basically states that the boundaries between layers should be handled with interfaces (abstract classes in Dart).

Applied to the Number Trivia App

There won't be much business logic to execute in the app, since we're just displaying interesting number facts. As for the business objects, there will be a single, fairly lean Entity called NumberTrivia - just a number and the text of the trivia.

Data

The data layer consists of a Repository implementation (the contract comes from the domain layer) and data sources - one is usually for getting remote (API) data and the other for caching that data. Repository is where you decide if you return fresh or cached data, when to cache it and so on.

You may notice that data sources don't return Entities but rather Models. The reason behind this is that transforming raw data (e.g JSON) into Dart objects requires some JSON conversion code. We don't want this JSON-specific code inside the domain Entities - what if we decide to switch to XML?

Therefore, we create Model classes which extend Entities and add some specific functionality (toJson, fromJson) or additional fields, like database ID, for example.

Applied to the Number Trivia App

The RemoteDataSource will perform HTTP GET requests on the Numbers API. LocalDataSource will simply cache data using the shared_preferences package. These two data sources will be "combined" in NumberTriviaRepository which will be the single source of truth for the interesting number trivia data. The caching policy will be very simple. If there is a network connection, always get data from the API and cache it. Then, if there's no network, return the latest cached data.

Next up...