Setting Up BLoC Architecture for a Flutter App

As the app grows, chaos in screen states starts to slow down releases. We design and set up BLoC architecture for your product: layers, events, and states are arranged so that new screens can be added without rewriting old code. We work turnkey — from the schema to handing over artifacts to your team.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a mobile application for FEEDME
    Development of a mobile application for FEEDME
    971
  • Development of a mobile application for XOOMER
    Development of a mobile application for XOOMER
    869
  • Development of a mobile application for RHL
    Development of a mobile application for RHL
    1289
  • Development of a mobile application for ZIPPY
    Development of a mobile application for ZIPPY
    1143
  • Development of a mobile application for Affhome
    Development of a mobile application for Affhome
    1076
  • Development of a mobile application for the FLAVORS company
    Development of a mobile application for the FLAVORS company
    677

Why BLoC Flutter Breaks Down as Screens Scale

The BLoC architecture in a Flutter application almost always starts out clean: one stream of states per screen, predictable transitions, tests for every scenario. Problems begin at scale, when there are dozens of screens and several people working on a feature.

State accumulates fields for every new case, events multiply, and no one holds the full picture in their head anymore — changes in one block unexpectedly break a neighboring screen.

A typical failure is an event race. The user quickly switches filters, responses arrive out of order, and the result of a stale request settles into the application state.

Externally this looks like a "stuck" list or data from the wrong screen, and it reproduces inconsistently, so the bug lives in production for months.

This is fixed not with a patch in one place, but with discipline in the event flow: canceling stale operations and having an explicit owner of the current result.

The second source is uncontrolled widget re-rendering. When a block's state stores everything indiscriminately, any change to it drags along a rebuild of an entire subtree, even though a single row changed.

On weak devices this shows up as degraded responsiveness and battery drain, and at scale — as growing build times: the more connections between blocks, the longer the project takes to compile and run through tests.

A separate pain is state bloat. One block per screen turns into a dumping ground of dozens of fields, some of which are relevant only for a specific scenario. Tests become fragile, and a new engineer on the team spends days figuring out which transitions are even possible.

Let's break down how this is addressed at the Flutter architecture level — from splitting states to rules for working with events.

What a well-designed application architecture delivers

Predictable builds and release cycle

Separating into layers — presentation, business logic, data handling — removes the main cause of long builds: rebuilding the entire module because of a change in a single screen.

When screen state lives in a separate class rather than being smeared across widgets, a change affects only its own branch. In practice, this cuts build time severalfold and eliminates the situation where a minor edit in one screen breaks the build of the entire application.

Recovery from failure and the class of failures

The flow of events and states makes screen behavior reproducible: the log shows which event arrived and which state the interface transitioned to. Recovery time after a failure drops from hours to minutes — the engineer doesn't guess but sees the point of failure.

An entire class of interface failures disappears: data desynchronization on the screen, "stuck" loading states, lost responses when switching between screens. These errors can't be reproduced manually and previously reached users.

Release stability and the cost of downtime

The fewer hidden connections between screens, the lower the cost of downtime: a failure is localized in one layer and doesn't drag the others along.

Tests for business logic are written without launching the interface, so the share of paths covered by checks grows, and regressions are caught before release rather than after.

Releases become predictable — the team ships more often and with fewer rollbacks, while new functionality is added without rewriting neighboring screens.

Layers, states, and events: how BLoC Flutter is structured

BLoC is not "yet another state management library," but a way to break an application into layers with a unidirectional data flow. An event comes from the interface, the bloc processes it and emits a new state, and the widget only renders what it receives.

Because of this, logic stops living inside screens, it can be covered by tests without launching the interface, and a failure can be reproduced by a sequence of events rather than by "catching the moment."

Then the engineering part begins: how to slice the blocs. Below are three working schemes that are actually chosen between at the start of a project.

Option When it fits Limitations
A single flow for the entire application A small product, 3-5 screens, one developer Any change drags the entire state graph along with it, tests become fragile
Splitting blocs by feature Medium and large applications, several teams Clear module boundaries are needed, otherwise blocs start knowing about each other
Hybrid: blocs by feature + local screen state Screens with a form, filters, animations Requires an agreement on what counts as shared state and what is a screen detail

Splitting blocs by feature gives a predictable cost of change: an edit in checkout does not touch the catalog, and tests of one feature do not fail because of another. The hybrid removes the main pain point — there is no need to drag every filter tap into the shared flow.

The local screen state lives in its own widget and dies with it, and only what survives the transition between screens goes into the bloc.

The configuration of layers matters more than the choice of library. We pin down where data access ends and logic begins, how blocs receive dependencies, and what happens on a repeated event while the previous one is still in progress.

These are the places where applications usually "double" requests and lose state when minimized.

Stages of setting up the architecture for your product

We start with a code audit: we look at how the layers are currently separated, where business logic leaks into the interface, how much state lives in one place, and which dependencies are pulled directly into widgets.

Based on the results, we record a map of problem areas — it determines the scope of work and the order of changes, so we don't have to rewrite the entire application.

Next, we design the layer scheme: interface, state, business logic, data access. For each event and state, we define an owner, write contracts between layers, and establish rules by which a widget knows nothing about the network, and the logic knows nothing about the markup.

The scheme is agreed with you before any edits begin — at this stage it's cheaper to change a decision than the code later.

  1. Code audit — analysis of the current structure, growth points, and module coupling, a map of problem areas.
  2. Layer scheme — distribution of responsibility between interface, state, logic, and data, and the contracts between them.
  3. Architecture setup — implementation of the BLoC approach, moving logic out of widgets, unified rules for handling states and errors.
  4. Load testing — running scenarios with frequent updates and large lists, measuring interface responsiveness and memory consumption.
  5. Development guidelines — naming rules, layer boundaries, templates for typical screens, review checks.
  6. Handover to the team — training your developers, walking through contentious points, supporting the first tasks under the new rules.

After setup, we run load testing: we look at how the interface behaves under frequent state updates, long lists, and parallel requests. If bottlenecks remain, we refine the scheme rather than masking them with spot fixes.

The result is predictable screen behavior and a clear point where to look for the cause of a failure.

We finish with development guidelines and handover to the team: naming rules, layer boundaries, templates for typical screens, a review checklist.

Your developers get not a one-off fix but a working standard — new screens are built according to it without our involvement, and maintenance doesn't turn into archaeology on someone else's code.

Artifacts your team keeps

Architecture work ends not with a presentation, but with a set of files in your repository. Below is what exactly remains with the team after setting up the BLoC layer, so that development can continue without us.

  • Dependency and layer diagram. A diagram of data flows between the interface, blocs, storage, and external services — indicating where the boundaries lie and what each part is allowed to know. With it, a new developer understands the project structure in one evening, rather than in a week of reading code.
  • Bloc templates. Ready-made templates for typical scenarios: loading a list, a form, pagination, working with cache. Each template comes with events, states, and an explanation of why the state is designed the way it is.
  • Error handling rules. A unified policy: which failures are shown to the user, which go to the log, where a request is retried, and how network errors and business logic errors are separated. This eliminates the inconsistency where each screen handles a failure in its own way.
  • Architecture documentation. A description of layers, naming conventions, rules for separation of responsibilities, and typical solutions with rationale. The document lives in the repository alongside the code and is updated together with it.
  • Review policy. A checklist for reviewing changes: whether logic leaks into the interface, whether state grows out of control, whether new scenarios are covered by tests. Review becomes predictable rather than depending on who looks at the changes.
  • A set of tests for key blocs. Checks of states and transitions that lock in behavior and protect against regressions during further work.
  • Instructions for connecting new screens. A step-by-step procedure: how to create a bloc, connect it to the interface, add events and states — so that the team extends the application according to a single pattern.

These artifacts are not reporting, but a working tool: a new engineer onboards with them, reviews are conducted with them, and further work is planned with them. Describe your task — we will come back with an estimate and a plan of what will be included in the handover.

Case study: how we cut build time and eliminated freezes

The project is a delivery app built on Flutter with three teams in a single repository.

By the second-year release, the state of things had become problematic: a full build took 22 minutes, tests ran for 14, and on the catalog screen under load, interface failures regularly popped up — main thread blocking during re-rendering of large lists.

Each release shipped once every three weeks, and the team spent half its time triaging regressions rather than working on tasks.

What was changed

The logic was split into layers: presentation, business logic, and data stopped depending on each other directly. Screen state was moved to event streams — the interface subscribes only to the slice it needs and does not re-render the entire list on a pinpoint change.

Heavy operations — parsing server responses, preparing data for lists — were moved off the main thread.

The build was reworked separately: incremental compilation and a dependency cache between runs were enabled, and the project was split into modules so that a change in one would not trigger a rebuild of everything.

Tests were split into fast unit tests and a full suite that runs before release, rather than on every save.

Metrics before and after

Metric Before After
Full build 22 min 7 min
Test run 14 min 4 min
Share of releases with interface failures 40% 6%
Release cycle 3 weeks 5 days

Interface failures stopped being a regular occurrence: out of 30 consecutive releases, only two required an urgent fix, and both were due to external causes, not architecture.

Rolling back to a previous version now takes minutes: the build is reproducible, and screen state is isolated, so a regression does not spread across the entire application.

What this gave the team

The release cycle was cut by a factor of three, and the team returned to product tasks instead of constantly fighting crashes. A new developer gets up to speed on the project within a week: the structure is predictable, and the tests show exactly what broke.

The same approach was then scaled to a second product — the migration took less time because the architectural decisions had already been proven in practice.

How BLoC differs from other state management approaches

BLoC is an approach in which a screen's state is described not by a set of scattered variables, but by a stream of events and explicit states. The interface sends an event, the logic processes it and emits a new state, and the widget merely renders what it receives.

This unidirectional scheme eliminates a whole class of bugs where two places in the code independently modify the same data and drift apart.

Approaches to state management should be compared not by how "trendy" they are, but by how much logic lives on the screen and how often it is reused.

Local state inside a widget is the cheapest option, and for a form with a couple of fields or a theme toggle it remains the right choice.

The problem begins when the same data needs to be shared between screens, survive navigation, and be loaded from the network: then state spreads across the widget tree, and debugging turns into a hunt for whoever overwrote the value last.

BLoC is justified where logic repeats across several screens, where a predictable order of updates matters, and where business rules must be covered by tests separately from the interface.

If an application consists of static lists and simple transitions, a full-fledged state layer will add more code than value.

Let us honestly name the limitations of the approach right away: more files and ceremony at the start, a higher entry barrier for new developers, and on small screens a noticeable excess of infrastructure over the task at hand.

Separately, it is worth thinking through how events and states fit into navigation and recovery after the app is backgrounded, otherwise the gain in predictability is eaten up by manual synchronization.

Choosing an architecture is always a trade-off between rigor and speed of change, and we select it for a specific product, not out of habit.

Common technical concerns before starting

Technical leads rarely ask "what is BLoC" — they ask what will happen to their codebase. Below are the questions asked most often, with honest answers and no marketing.

We already have a Flutter app with its own architecture. Do we have to rewrite everything from scratch?

No. BLoC is integrated screen by screen and module by module: first we move one or two critical scenarios into events and states, while the rest of the code keeps running on its existing logic.

After that, migration proceeds in iterations, each of which goes through review and release separately — without a "big rewrite" and without freezing the product for months.

How compatible is this with our current libraries and navigation?

The presentation layer stays the same — only the source of state changes. Navigation, themes, localization, and the network layer connect to BLoC through the same interfaces as before.

Conflicts are possible where logic is already smeared across widgets: we flag those places during the code review stage and show the list before work begins.

What about support after the project handover?

We hand over not just the code, but the rules: the data flow diagram, conventions for naming events and states, templates for typical screens, and a code review checklist. Your team adds a new screen from a ready-made example without having to untangle someone else's logic. For a while afterward, we stay in touch on specific questions.

What implementation risks should we plan for in advance?

The main one is the growth in the number of states on complex screens if module boundaries aren't agreed on upfront. The second is tests: without coverage of events and transitions, refactoring becomes dangerous.

Both risks are eliminated at the start: we lock down module contracts and write checks for key scenarios before we touch working code.

Can we roll back if the approach doesn't work out?

Yes. As long as migration proceeds module by module, each step is a separate commit with a working previous version. If on some screen the solution didn't prove itself, we revert to the previous implementation without affecting the rest of the app.

That's exactly why we don't start with a full rewrite: the cost of a mistake on one screen is incomparable to the cost of a mistake across the entire product.

Describe your codebase and what worries you about the current architecture — we'll come back with an assessment of the migration stages and a verification plan.

Let's discuss your task and get back to you with a plan

We start the conversation not with a commercial proposal, but with an analysis of your app: how the data flow is structured now, where states diverge from reality, which screens suffer from unnecessary rebuilds.

Based on the discussion, you get not an abstract "we'll make it good," but a concrete plan for migrating to BLoC with a timeline estimate and team composition.

What we clarify on the call:

  • The current app structure: how many screens there are, which states live locally and which should be shared across multiple sections.
  • Pain points: where logic is smeared across widgets, where business rules are duplicated, which failures users can reproduce.
  • Requirements for offline mode and synchronization — this determines how events and states should be designed.
  • Timeline and release constraints: whether there is a date by which the current version must remain operational.
  • Who on your side makes product decisions and who will verify the result.

What you get from the conversation:

  • A phased work plan: auditing the current code, designing the layers, migrating screens step by step, testing, and release.
  • A timeline estimate broken down by phase — you can see where work can be parallelized and where sequencing is required.
  • Team composition for the task: who designs the architecture, who leads the migration, who is responsible for tests and release.
  • A migration scheme without stopping the product: old screens keep working while new ones pass validation.
  • Transparent readiness criteria: coverage of key scenarios with tests, no state desynchronization, stable builds.

Describe the task in whatever form is convenient — a link to the repository, a list of problem areas, or a short message. We will get back to you with a timeline estimate, work plan, and proposed team composition so you can decide on starting the project.