The second article is from a series of articles about BLoC.
You can find the previous article here.

In this article, we will look at the division into layers and the general structure of the project.


Layers and project structure

The Business Logic Component architecture

If you want to explore and modify the diagram by downloading the source file

and open with app.diagrams.net

The layers can be described using the following table

Widget

BLoC

Data

Description

Configuration or application layer.

Contain app immutable blueprints (Widget) and current mutable configuration (Element).

Manages dependencies between components

Domain logic layer, business logic layer.

Manages business workflow, and maps events to states using the current state and repository interface as a source of new data.

Business Logic Component. Split configuration and data layers by Publish/subscribe pattern.

Data access layer, persistence layer, networking, and other services which are required to support a particular business layer).

Repositories manipulate a few data providers.

Data providers call methods from clients and map results by Data Transfer Objects to data entity/models.

Separation

The widgets layer interacts with BLoC through blocs.

Separates from BLoC layers by the add method, state, and stream getter.

Separates from Data layers by repositories methods.

BLoC layer can call methods and subscribe to streams from repositories.

The data layer can be split into three parts:

Repository, Data Provider and Data Source

Contains

Elements tree, Widgets structure

  • Proxies

  • Renderers

  • Components

  • Events

  • States

  • BLoC

  • Repositories

  • Data Providers, DAO

  • Clients, Databases

For example

  • StatelessWidget

  • StatefulWidget

  • InheritedModel

  • StatelessElement

  • StatefulElement

  • ProxyElement

  • BuildContext

  • WeatherBLoC

  • AuthenticationState

  • FetchSettingEvent

  • IdleState

  • BasketBLoC

  • LogInEvent

  • ErrorState

  • FirebaseAuthentication

  • HttpClient

  • Database

  • BasketDataProvider

  • GraphQL client

  • SettingsDao

  • AuthenticationRepository


User Interface layer

Yes, but what about such as "Presentation" or "UI" layers, you ask?
User Interface/Presentation/View/Rendering - encapsulated directly in Flutter Framework & Flutter Engine and rarely managed by hand.

Layer includes:

Flutter system overview

Widget layer

You can think about the widget layer as a presentation layer, but should remember and agree with a few points:

  • Stateless, Stateful, Inherited, and the same widgets are only about dependencies, lifecycle, and composition, not a User Interface. Widgets draw absolutely nothing.

Widgets layer also about:

  • localization
  • app lifecycle
  • metrics
  • memory pressure
  • navigation (including saving the current state in the cache)
  • app dependencies
  • interaction with the platform
  • initialization

Widgets are not about what you draw on the screen, but about how you describe and configure your application - this is declarative, and the meaning of the phrase "everything is a widget".

So it's much more transparent to initially think of widgets as declarative application configurations rather than something you draw on a canvas.

💡
Configuration or application layer. Contain app immutable blueprints (Widget) and current mutable configuration (Element). Declare UI configuration and manages dependencies between components.

The widget layer interacts with UI through the BuildOwner manager.

The primary build owner is typically owned by the WidgetsBinding and is driven from the operating system along with the rest of the build/layout/paint pipeline.
Additional build owners can be built to manage off-screen widget trees.

Widgets are about immutable configuration and composition.

We can split widgets and elements into three types:

Components (e.g. Stateless, Stateful, Inherited widgets, Scaffold, Text, Container, GestureDetector, Navigator, Theme, MediaQuery)

  • Composition
  • Dependency management
  • Lifecycle

Renderers (e.g. Row, Column, Stack, Padding, Align, Opacity, RawImage) - classes very rarely created by hand, a configuration of what will be drawn on the screen

  • Dimensions
  • Position
  • Layout and rendering
⚠️
Flutter framework has not had a “Widget tree

The wording “Widget tree” only exists for sake of making it easier to understand since developers are using Widgets, but, in Flutter, there is NO Widget tree! To be correct, we should rather say: “tree of Elements”

The widget layer's responsibility is to figure out how to render itself based on one or more bloc states. In addition, it should handle user input and application lifecycle events.

In addition, the widget layer will have to figure out what to render on the screen based on the state from the bloc layer.

Widgets, Elements tree, Render tree

Elements are mutable configurations created by the widget’s blueprint.

An instantiation of a Widget at a particular location in the tree.

Also, the context familiar to everyone is a reference to the widget element.

And of course, a widget’s State is about Elements too.

💡
Each widget corresponds to one element. Elements are linked to each other and form a tree. Therefore an element is a reference to something in the tree.
Widgets and render objects belong to elements

Here are the different element types:

💡
The BuildContext is nothing else but the Element itself which corresponds to
• the Widget is being rebuilt (inside the build or builder methods)
• the StatefulWidget linked to the State where you are referencing the context variable.

BLoC layer

💡
The business logic layer's responsibility is to respond to input from the widget layer with new states. This layer can depend on one or more repositories to retrieve data needed to build up the application state.

The main idea of using BLoC is separate Widget and Data layers with Pub/Sub pattern, providing a predictable sequence of transformation of events into states and isolating logic errors.

The widget adds event and BLoC transform and emits a few states on it.

Think of the business logic layer as the bridge between the widgets (application layer) and the data layer. The business logic layer is notified of events/actions from the widget layer and then communicates with a repository to build a new state for the presentation layer to consume.

Repositories must be passed only by the bloc’s constructor!

Widgets can interact only with "add" method, "stream", and "state" getters.

Mutable only internal bloc’s state field.

You can create and emit new states by current "state" getter, current "event", and repository methods.

More about the theoretical part is described in the previous article.


Data

The data layer can be split into two parts:

  • Repository, Facade
  • Data Provider, Data Access Objects, Adapter, Service
  • Client, Database, Data Source, Executor

This layer is the lowest level of the application and interacts with databases, network requests, and other asynchronous data sources.

Client

💡
The client's responsibility is to provide raw data. The data provider should be generic and versatile. Request remote sources or databases.

The clients will usually expose simple APIs to perform CRUD operations or make requests.

As usual, in reality, we do not have the opportunity to use client interfaces that are tied to the implementation.

e.g.

  • GraphQL client
  • HTTP client
  • Web Socket client
  • Centrifuge client
  • Database executor
  • Key-Value Storage
  • Firebase Firestore
  • Firebase Authentication
class MyClient {
    Future<Response> execute(Request request) => ...;
}

Data provider

💡
Data providers manage clients and return business entities (maybe mapped by Data Transfer Object) to repositories.

We might have a create data, read data, update data, and delete data method as part of our data layer that returns business models and entities.

The constructor must pass a client!

e.g.

  • OrdersNetworksDataProvider
  • AuthenticationDatabaseDataProvider
  • UserCartDao
  • PhotoStorage
abstract class IMyDataProvider {
  Future<Entity> create(EntityData data);
  Future<Entity> get(int id);
  Future<Entity> update(Entity entity);
  Future<void> delete(int id);
}

abstract class MyDataProviderImpl implements IMyDataProvider {
  MyDataProviderImpl({required Client client}) : _client = client;
  
  final Client _client;
  
  @override
  Future<Entity> create(EntityData data) => _client.execute(...).then<Entity>(DTO.decode);
  
  @override
  Future<Entity> get(int id)=> _client.execute(...).then<Entity>(DTO.decode);
  
  @override
  Future<Entity> update(Entity entity)=> _client.execute(...).then<Entity>(DTO.decode);
  
  @override
  Future<void> delete(int id)=> _client.execute(...);
}

Repository

💡
The repository layer is a wrapper around one or more data providers with which the BLoC Layer communicates.

A repository can interact with multiple data providers and perform transformations on the data before handing the result to the business logic layer.

The constructor must pass a data provider!

As usual, repositories are immutable.

abstract class IMyRepository {
  Future<Entity> create(EntityData data);
  @useResult
  Future<Entity> get(int id);
  Future<Entity> update(Entity entity);
  Future<void> delete(int id);
}

@immutable
class MyRepositoryImpl implements IMyRepository {
  MyRepositoryImpl({
    required IMyNetworkDataProvider networkDataProvider,
    required IMyStorageDataProvider _databaseDataProvider,
  })
    : _networkDataProvider = networkDataProvider
    , _databaseDataProvider = databaseDataProvider;
    
  final IMyNetworkDataProvider _networkDataProvider;
  final IMyStorageDataProvider _databaseDataProvider;
  
  @override
  Future<Entity> create(EntityData data) {
    final data = await _networkDataProvider.put(data);
    await _databaseDataProvider.set(data);
  }
  
  @override
  Future<Entity> get(int id) {
    final cache = await _databaseDataProvider.get(id);
    if (cache is Entity) return data;
    final data = await _networkDataProvider.get(id);
    await _databaseDataProvider.set(data);
    return data;
  }
  
  @override
  Future<Entity> update(Entity entity) {
    final data = await _networkDataProvider.update(entity);
    await _databaseDataProvider.set(data);
    return data;
  }
  
  @override
  Future<void> delete(int id) {
    await _networkDataProvider.remove(id);
    await _databaseDataProvider.remove(id);
  }
}

Typical flow

  1. Initially, the BLoC has an Idle state
  2. User press button call onTap callback
  3. BLoC.add(Event)
  4. BLoC emits a Progress state (copy data from state getter)
  5. Widgets react with shimmers, loaders, and locked buttons
  6. BLoC calls the IRepository method
  7. RepositoryImpl calls INetwork and IDatabase providers
  8. Return consolidated data or throw Exception to BLoC
  9. BLoC emits a Successful (set data) or Error (copy data from state getter)
  10. BLoC emits an Idle state (copy data from state getter)

Anatomy of a project

<platform>/
assets/
integration_test/
test/
tool/
packages/
 <package_name>/
   example/
   lib/
     src/
     <package_name>.dart
   test/
   pubspec.yaml
lib/
 src/
   common/
     util/
     constant/
     localization/
     model/
     router/
     bloc/
     widget/
   feature/
     <feature_name>/
       model/
       widget/
       bloc/
       data/
   app.dart
 main.dart
README.md
pubspec.yaml
analysis_options.yaml
Makefile