Flutter BLoC Architecture Explained: Data Layer, Repository, Business Logic, and Presentation

Flutter BLoC Architecture

We are able to divide our application into three layers by using the bloc library:

  • Presentation
  • Business Logic
  • Data
    • Repository
    • Data Provider

Data layer

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

There are two components to the data layer:

  • Data Provider

The data provider typically exposes simple APIs to perform CRUD operations. Our data layer may have the createData, readData, updateData, and deleteData methods.

class DataProvider {
    Future<RawData> readData() async {
        // Read from DB or make network request etc...
    }
}

Repository

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

class Repository {
    final DataProviderA dataProviderA;
    final DataProviderB dataProviderB;

    Future<Data> getAllDataThatMeetsRequirements() async {
        final RawDataA dataSetA = await dataProviderA.readData();
        final RawDataB dataSetB = await dataProviderB.readData();

        final Data filteredData = _filterData(dataSetA, dataSetB);
        return filteredData;
    }
}

Business Logic Layer

The business logic layer’s responsibility is to respond to input from the presentation layer with new states.

class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
    BusinessLogicComponent(this.repository) {
        on<AppStarted>((event, emit) {
            try {
                final data = await repository.getAllDataThatMeetsRequirements();
                emit(Success(data));
            } catch (error) {
                emit(Failure(error));
            }
        });
    }

    final Repository repository;
}

Presentation Layer

It is responsible for ui change based on the state changes.

Bloc Concepts

Bloc Widgets

BlocBuilder

It is a Flutter widget that requires bloc and a builder function. It builds a widget in response to a state change.

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // return widget here based on BlocA's state
  },
);

To provide a bloc that will be only accessible in a particular widget and is not accessible via parent BlocProvider and BuildContext.

BlocBuilder<BlocA, BlocAState>(
  bloc: blocA, // provide the local bloc instance
  builder: (context, state) {
    // return widget here based on BlocA's state
  },
);

For controlling when to build, it provides buildWhen.It takes in pthe revious and current states and returns boolean .If it returns true ,then the builder will be called.

BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  },
);

BlocSelector

It is similar to BlocBuilder, but it allows you to filter updates by selecting a new value based on the current bloc state. It is used to reduce the number of rebuilds.

BlocSelector<BlocA, BlocAState, SelectedState>(
  selector: (state) {
    // return selected state based on the provided state.
  },
  builder: (context, state) {
    // return widget here based on the selected state.
  },
);

BlocProvider

It is a widget which provides bloc to its children via BlocProvider.of<T>(context).It is used as dependency injection so that a single instance of bloc can be provided to multiple widget within same subtree.

In most cases, BlocProvider should be used to create new blocs which will be made available to the rest of the subtree. In this instance, BlocProvider will take care of closing the bloc automatically because it is in charge of creating it.

BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

BlocProvider will by default generate the bloc lazily, which means that when the bloc is sought up using BlocProvider.of(context), create will be invoked. To get around this:

BlocProvider(
  lazy: false,
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

In certain situations, an existing bloc can be sent to a new section of the widget tree using BlocProvider. In those cases, it will not be auto-closed.

BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);

Then, from screenA or childA, we can retrieve BlocA with

// with extensions
context.read<BlocA>();

// without extensions
BlocProvider.of<BlocA>(context);

MultiBlocProvider

Several BlocProvider widgets can be combined into a single Flutter widget.

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
);

BlocListener

BlocListener is a Flutter widget that takes a BlocWidgetListener and an optional Bloc and invokes the listener in response to state changes in the bloc. 

Listener is only called once for each state change (NOT including the initial state) unlike builder in BlocBuilder, and is a void function.

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: const SizedBox(),
);

To provide a bloc that will be only accessible in a particular widget and is not accessible via parent BlocProvider and BuildContext.

BlocListener<BlocA, BlocAState>(
  bloc: blocA,
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: const SizedBox(),
);

For controlling when to build, it provides listenWhen.It takes in the previous and current states and returns a boolean. If it returns true, then the listener will be called.

BlocListener<BlocA, BlocAState>(
  listenWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to call listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: const SizedBox(),
);

MultiBlocListener

Flutter widget that merges multiple BlocListener widgets into one.

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
);

BlocConsumer

It is a combination of both BlocBuilder and BlocListener.It should only be used when both UI rebuild and executing a function are required on bloc state change. It takes all the parameters taken by both BlocBuilder and BlocListener, like buildWhen,listenWhen, bloc, etc.

RepositoryProvider

A Flutter widget that provides a repository to its children via RepositoryProvider.of<T>(context).It is used as a dependency injection widget to provide a single instance of the repository to the entire subtree.

While RepositoryProvider should only be used for repositories, BlocProvider should be used to offer blocs.

RepositoryProvider(
  create: (context) => RepositoryA(),
  child: ChildA(),
);

The repository can then be obtained from Child A:

// with extensions
context.read<RepositoryA>();

// without extensions
RepositoryProvider.of<RepositoryA>(context)

MultiRepositoryProvider

Flutter widget that merges multiple RepositoryProvider widgets

MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
);

BlocProvider Usage

First, we need to create a bloc. Here, I am creating the counter bloc :

sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }
}

Now to supply this bloc to counterPage, we can write the provider as follows: 

void main() => runApp(CounterApp());

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

Now, to make the CounterPage to react to state changes : 

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.0),
            ),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()),
            ),
          ),
        ],
      ),
    );
  }
}

RepositoryProvider Usage

First, we need to create a repository : 

class WeatherRepository {
  WeatherRepository({
    WeatherApiClient? weatherApiClient
  }) : _weatherApiClient = weatherApiClient ?? WeatherApiClient();

  final WeatherApiClient _weatherApiClient;

  Future<Weather> getWeather(String city) async {
    final location = await _weatherApiClient.locationSearch(city);
    final woeid = location.woeid;
    final weather = await _weatherApiClient.getWeather(woeid);
    return Weather(
      temperature: weather.theTemp,
      location: location.title,
      condition: weather.weatherStateAbbr.toCondition,
    );
  }
}

Now we will pass our WeatherRepository instance to our app through the constructor :

import 'package:flutter/material.dart';
import 'package:flutter_weather/app.dart';
import 'package:weather_repository/weather_repository.dart';

void main() {
  runApp(WeatherApp(weatherRepository: WeatherRepository()));
}

Now, to inject this repository into our widget tree, we can use RepositoryProvider or MultiRepositoryProvider(in case of multiple repositories) :

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:weather_repository/weather_repository.dart';

class WeatherApp extends StatelessWidget {
  const WeatherApp({Key? key, required WeatherRepository weatherRepository})
      : _weatherRepository = weatherRepository,
        super(key: key);

  final WeatherRepository _weatherRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider.value(
      value: _weatherRepository,
      child: BlocProvider(
        create: (_) => ThemeCubit(),
        child: WeatherAppView(),
      ),
    );
  }
}

Now to expose our repositories to a cubit/bloc : 

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_weather/weather/weather.dart';
import 'package:weather_repository/weather_repository.dart';

class WeatherPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => WeatherCubit(context.read<WeatherRepository>()),
      child: WeatherView(),
    );
  }
}

Read More: Flutter State Management with BLoC Explained

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.