Business Logic Component [2 of 4]
The second article is from a series of articles about BLoC.
You can find the full article here.
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
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:
Widgets layer also about:
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.
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.
Recommended by LinkedIn
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:
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.
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.
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
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