Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Dependency Rule and Data Flow in Clean Architecture

Recommended resources

Hello! Welcome to your first lesson. This course is designed to build on your existing Flutter experience and guide you toward a mid-level professional standard.

We'll begin by diving deep into the foundations of Clean Architecture. While you've worked with it before, our goal here is to solidify your understanding of the core principles that enable truly scalable and maintainable applications.

In this lesson, you will learn to explain the dependency rule and the flow of data between layers in Clean Architecture. Mastering these concepts is the first and most critical step in applying this architecture effectively in complex projects.

1. Why Clean Architecture? A High-Level View

As applications grow, codebases can become tangled. UI logic gets mixed with business rules, and data fetching code becomes scattered, making the app difficult to test, debug, and update. Clean Architecture, proposed by Robert C. Martin ("Uncle Bob"), provides a blueprint for structuring software to solve these problems.

Its primary goal is the separation of concerns. It divides an application into distinct layers, with the most important rule being the Dependency Rule.

To get a quick visual intuition for how these layers are organized, let's start with a short, conceptual video.

Flutter Clean Architecture Visual Explanation

This video from Flutter Mapp provides a simple and effective visual analogy for Clean Architecture using a cone. It will help you grasp the overall structure and the general flow of information before we dive into the technical details.

Please watch the entire video. Pay close attention to: The three main layers: Presentation, Business (which we'll call Domain), and Data. The "cone" analogy and what it represents. The visual representation of how a user action triggers a cycle that flows through the layers and back to the UI.

As the video illustrates, the architecture is typically divided into three primary layers:

Layer Responsibility Examples
Presentation Displays the UI and handles user input. It's what the user sees and interacts with. It should not contain any business logic. Widgets, Pages, State Management (BLoC, Cubit), Animations.
Domain Contains the core business logic and rules of the application. This layer is the heart of your app and is completely independent of any UI or data-sourcing frameworks. Use Cases (e.g., GetUserProfile), Entities (e.g., User object), Repository Interfaces.
Data Manages all data operations. It's responsible for fetching data from external sources (APIs, databases) and providing it to the Domain layer. Repository Implementations, API clients (using Dio), Local Database access (e.g., SharedPreferences, Floor).

2. The Dependency Rule: The Core Principle

The single most important rule in Clean Architecture is the Dependency Rule. It states:

Source code dependencies can only point inwards.

This means that outer layers can depend on inner layers, but inner layers must never depend on outer layers.

  • The Presentation layer depends on the Domain layer.
  • The Data layer also depends on the Domain layer.
  • The Domain layer depends on nothing.
This diagram illustrates the separation of layers. The "Call Flow" (blue arrows) shows how dependencies work: Presentation calls Domain, and Domain calls an abstraction that the Data layer implements. The "Data Flow" (red arrows) shows the data returning to the UI.

This raises a question: If the Domain layer can't depend on the Data layer, how does it get data from an API or a database?

The answer is the Dependency Inversion Principle (the "D" in SOLID). Instead of depending on concrete implementations in the Data layer, the Domain layer defines an abstract contract (in Dart, this is an abstract class). The Data layer then provides a concrete implementation of that contract.

This way, the Domain layer only knows about the contract it defined, not the specific details of how or where the data comes from. This keeps the business logic pure and independent.


3. Tracing the Flow: A Deeper Dive

Now, let's explore the specifics of how data and calls move through these layers. The following video from the well-respected Reso Coder channel provides a detailed, practical explanation using a "Number Trivia" app as an example.

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

This video walks through the architecture layer by layer, explaining the role of each component and how they connect. It provides a more in-depth look at the concepts we've discussed.

Watch the following segments in order: The Overall Structure: First, watch the introduction to the three layers to see the author's diagram. This reinforces the concepts we just covered. Layers explanation. The Presentation Layer: Understand how the UI layer initiates actions. Pay attention to how the BLoC's role is simply to delegate work. Presentation layer. The Domain Layer: See why this is the core of the app. Focus on the concept of Use Cases and Entities. Domain layer. The Dependency Rule in Action: This is a crucial part. Focus on how an abstract class for the Repository acts as a contract in the Domain layer. This is the key to dependency inversion. Repository contract. The Data Layer: Finally, see how the Data layer implements the repository contract and interacts with Data Sources and Models. Data layer.

To solidify your understanding, let's also look at a textual breakdown of these principles.

Clean Architecture for Flutter Developers: Structuring Your ...

This article by Christian Tsoungui provides a clear, written explanation of the layers and their responsibilities, which will complement what you've just seen in the videos.

Read the article from the beginning down to the section titled "How They Work Together". Focus on the definitions provided for the Dependency Rule and the responsibilities of each of the three layers.


4. A Step-by-Step Walkthrough

Let's synthesize everything into a single, concrete flow. Imagine a user taps a "Get Profile" button in your app.

  1. Presentation Layer (UI Interaction):

    • The user taps a Button widget.
    • The onPressed callback is triggered, which calls a method on your BLoC/Cubit, for example, context.read<ProfileBloc>().add(FetchProfileEvent()).
  2. Presentation to Domain (Initiating the Use Case):

    • The ProfileBloc receives the event.
    • Inside the BLoC's event handler, it calls the execute method of a GetUserProfile use case. The BLoC depends on the use case from the Domain layer.
    • final user = await getUserProfile.execute();
  3. Domain Layer (Executing Business Logic):

    • The GetUserProfile use case contains the logic for this specific feature. Its job is to orchestrate the fetching of a user profile.
    • To get the data, it calls a method on a repository interface (an abstract class) that it depends on.
    • return await userRepository.getProfile();
    • At this point, the Domain layer's job is done. It has requested data via an abstract contract, without any knowledge of APIs, databases, or JSON.
  4. Data Layer (Fulfilling the Contract):

    • A class named UserRepositoryImpl in the Data layer implements the UserRepository interface from the Domain layer.
    • The getProfile() method in UserRepositoryImpl is executed. This class might check if there's an internet connection.
    • If online, it calls a method on a RemoteDataSource (e.g., api.get('/profile')).
    • If offline, it might call a method on a LocalDataSource to retrieve cached data.
  5. Data Layer (Data Handling):

    • The RemoteDataSource uses a package like Dio to make an HTTP request and receives raw JSON data.
    • This JSON is parsed into a UserModel object. UserModel is a Data layer class that often includes fromJson and toJson methods. It might be a subclass of the domain's User entity.
    • The UserRepositoryImpl receives the UserModel and converts it into a pure User entity to return to the Domain layer.
  6. Data Flowing Back (Data → Domain → Presentation):

    • The User entity is returned from userRepository.getProfile().
    • It flows back to the GetUserProfile use case.
    • The use case returns the User entity to the ProfileBloc.
  7. Presentation Layer (Updating the UI):

    • The ProfileBloc receives the User entity.
    • It emits a new state, ProfileLoadSuccess(user: user).
    • A BlocBuilder in the widget tree listens for this state change and rebuilds the UI to display the user's profile information.

This entire cycle enforces the Dependency Rule while allowing data to flow cleanly across all layers of the application.


Conclusion

In this lesson, we established the foundational theory of Clean Architecture in Flutter. You now have a solid mental model for how the system works.

Key Takeaways:

  • Clean Architecture separates your app into three layers: Presentation, Domain, and Data.
  • The core principle is the Dependency Rule: dependencies must only point inwards, towards the central Domain layer.
  • The Domain layer is independent and contains your core business logic (UseCases, Entities). It defines contracts (Repository interfaces) for data handling.
  • The Data layer implements these contracts, handling the "how" of data retrieval from sources like APIs or local storage.
  • A user action triggers a call flow from Presentation → Domain → Data, and the result creates a data flow from Data → Domain → Presentation, which updates the UI.

In our next lesson, we will begin putting this theory into practice. You will define and implement the core shared components of the architecture, such as base UseCase and Failure classes. This will create the reusable building blocks we'll need for every feature in our application.