23

Flutter TDD Clean Architecture Course [1] – Explanation & Project Structure

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.

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

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 APILocalDataSource 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...

Having the foundational structure of the Number Trivia App's architecture in place, we will begin implementing the inner and most stable layer - domain. We will also add all the packages needed for developing the app, including testing-specific ones like mockito. We're doing test driven development, after all!

Subscribe below to make sure you get notified about the new tutorials and Flutter news.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Tudor says:

    This a very a very good solution for architecting an app. I am thinking to use it for my next project. But what I dont understand is the difference between model / entity. What are u using the model? only form toJson, fromJson. Entity is created based on model. For example Repository get a list of User models (id, job_id), Jobs(id, name) and after it build an UserEntity?
    Thank you

    • Models are data layer dependent – they contain conversion methods (JSON, XML, …) and DB specific fields (ID, …). Entities hold only the fields which make sense from the “business standpoint.
      So yes, Repository will get Models from the data sources and return Entities to the domain layer. However, Entities are NOT based on Models – it’s the other way around. Basically, everything spins around the domain layer when it comes to the clean architecture.

  • So if you are sharing models between multiple features do they go into core?

  • JK says:

    Hi Reso Coder.
    Do you mind to give me some idea or making a short video about implementation this clean architecture in Firestore CRUD ?

    Thank you so much. Appreciated.

    • I can most definitely make a tutorial on that. Basically though, the principles remain the same as they are in this series. The only real difference would be that you’d have a “FirestoreDataSource” where you would put all of the CRUD operations for a particular feature.

  • Antonio says:

    Your entities extends equatable for comparing objects, for example in the model unit test. But if the object has attributes that can have setter, IDE provides a warning because equatable is marked as inmutable (and because of the setter they can not be final)
    Any suggestion?
    Thanks, this serie and all post in general are great

  • Ken says:

    Loving the tutorials so far! It seems really helpful. I do have a question however about the dio package. If I’m understanding it correctly, it does json serialization for you automatically. Does this mean that I should replace any model classes with dio? Sorry if the question is a bit unclear

  • Tomás R. Pita says:

    Greetings, excellent tutorials, I’m learning a lot, I wrote you because I do not see where is the code of the part of Presentation, that in the tutorial 2 you say you already wrote, where can I look at that code? thank you!

  • BlockChainiste says:

    Hello Reso Coder.

    Thank you so much for all. I follow since a moment and I decide you use your achitecture for my local e-commerce app.

    These my requests:

    1-If for example i have these features (login, registration, manageProfile, ManageShop, etc), all of them must have (domain, data, presentation subfolders ?. If yes, how to share data between them

    2-For datasources, I think to use my api (e.g: http://www.my-site.com/api) with chopper librairy. How can i do it.

    3-If we assume that i use chopper for api call,
    must I have an endpoint for each feature or do I have to put chopper in the core folder?

    Please, i need your help. Thank you so much.

    • Hello!
      The separation into features is something that should help you and it should definitely not cause duplication.

      I usually pull anything which has to be shared between multiple features into the core folder. I found that I usually put most of the data-related classes into core, leaving features with just the presentation layer, basically.

      I’d put Chopper classes for individual endpoints into “feature/data” folder and then pull up only the Chopper set up into the “core” folder.

  • Aakash Aakash says:

    I have one question.
    What if we need some widget like suppose bottom sheet in other features of the app. So when we separate packages for each feature which itself contains the pages and widgets packages then what will happen when some other feature of the app needs to use the same page or widget which is already used by another feature. Should we copy the file or import that package into our new feature?

  • Mohammad Meshkani says:

    May you say why you put bloc in the presentation more clearly?

  • BlockChainiste says:

    Please Matej Rešetár.

    It’s good to make a custom class to manage the routing between the different screen the app ?

    I think to put inside the core folder.

  • Lein says:

    Thank you for your tutorial. currently I refactor my project to this Architecture. After a while I realised, you have many icon image for each folder. what Icon did you use? thank you

  • Ryan Hoo says:

    Hi Reso Coder,

    Could Provider package provide a cleaner architecture as compared to BLoC? I’m new to Flutter, wondering which one is better in terms of learning curve.

  • michael lee says:

    Hello Reso Coder.
    Thank you for the fantastic series of flutter.
    one thing that I would like to see is a next button at the bottom of the page, for me to easily jump to the next blog post. thanks a lot again~Really appreciate your work

  • >