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
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
|
|
|
For example |
|
|
|
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:
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.
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
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.
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.
Here are the different element types:
BLoC layer
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 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
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> getById(int id);
Future<Entity> update(Entity entity);
Future<void> deleteById(int id);
}
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> getById(int id) => _client.execute(...).then<Entity>(DTO.decode);
@override
Future<Entity> update(Entity entity) => _client.execute(...).then<Entity>(DTO.decode);
@override
Future<void> deleteById(int id) => _client.execute(...);
}
Repository
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> getById(int id);
Future<Entity> update(Entity entity);
Future<void> deleteById(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> getById(int id) {
final cache = await _databaseDataProvider.getById(id);
if (cache is Entity) return data;
final data = await _networkDataProvider.getById(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> deleteById(int id) {
await _networkDataProvider.deleteById(id);
await _databaseDataProvider.deleteById(id);
}
}
Typical flow
- Initially, the BLoC has an
Idle
state - User press button call onTap callback
- BLoC.add(Event)
- BLoC emits a
Progress
state (copy data fromstate
getter) - Widgets react with shimmers, loaders, and locked buttons
- BLoC calls the
IRepository
method - RepositoryImpl calls
INetwork
andIDatabase
providers - Return consolidated data or throw Exception to BLoC
- BLoC emits a
Successful
(set data) orError
(copy data fromstate
getter) - BLoC emits an
Idle
state (copy data fromstate
getter)
Anatomy of a project
.
βββ <platform>/
βββ assets/
β βββ ...
βββ integration_test/
β βββ ...
βββ lib/
β βββ src/
β β βββ common/
β β β βββ .../
β β β βββ util/
β β β β βββ ...
β β β βββ constant/
β β β β βββ ...
β β β βββ model/
β β β β βββ ...
β β β βββ bloc/
β β β β βββ ...
β β β βββ widget/
β β β βββ ...
β β βββ feature/
β β β βββ <feature_name>/
β β β βββ model/
β β β β βββ ...
β β β βββ widget/
β β β β βββ ...
β β β βββ bloc/
β β β β βββ ...
β β β βββ data/
β β β βββ ...
β β βββ app.dart
β βββ main.dart
βββ packages/
β βββ database/
β β βββ ...
β βββ localization/
β β βββ ...
β βββ router/
β β βββ ...
β βββ <package_name>/
β βββ example/
β β βββ ...
β βββ lib/
β β βββ src/
β β β βββ ...
β β βββ <package_name>.dart
β βββ test/
β β βββ ...
β βββ pubspec.yaml
βββ test/
β βββ ...
βββ tool/
β βββ ...
βββ README.md
βββ pubspec.yaml
βββ analysis_options.yaml
βββ Makefile