Business Logic Component [2 of 4]

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

No alt text provided for this image
The Business Logic Component architecture

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:

No alt text provided for this image

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.


BLoC layer

No alt text provided for this image

💡 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 dataread dataupdate 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        

To view or add a comment, sign in

More articles by Mike Matiunin

  • Business Logic Component [4 of 4]

    Business Logic Component [4 of 4]

    In this final article of the BLoC series, we will look at code examples and practical implementation of several popular…

  • Business Logic Component [3 of 4]

    Business Logic Component [3 of 4]

    In the third part of a series of articles about the bloc, we will analyze successful and unsuccessful decisions…

  • Harness the Power of Anonymous Functions in Dart

    Harness the Power of Anonymous Functions in Dart

    Anonymous functions, also known as lambda expressions or closures, are an essential part of modern programming…

  • Handling Asynchronous Dependencies in Flutter & Dart

    Handling Asynchronous Dependencies in Flutter & Dart

    In Flutter and Dart applications, it is common to encounter scenarios where a class depends on an asynchronous…

    2 Comments
  • Business Logic Component [1 of 4]

    Business Logic Component [1 of 4]

    Introduction You can find the full original article here. We are starting a series of articles about Business Logic…

  • Anti-patterns of error handling in dart

    Anti-patterns of error handling in dart

    This article will show common pitfalls you can make when handling exceptions and how to do it right. The article will…

  • Layer link

    Layer link

    Let’s take a look at how to create and display widgets that appear on top of other widgets and follow them when moved…

    1 Comment
  • ChangeNotifier selector

    ChangeNotifier selector

    Have you had a situation where you must select and rebuild the interface only to change specific fields of your…

Insights from the community

Others also viewed

Explore topics