Skip to content
Umar ManzoorBook a call
← Case studies
FlutterArchitectureState Management

Untangling state in a scaling Flutter app — why boundaries beat the library

The team blamed their state management library. The real problem was missing boundaries. Here's how restructuring state — not switching tools — made a growing Flutter app reasonable to change again.

Smaller rebuild surfaces · state the team could actually reason about

Every few months, a team tells me their Flutter app has become hard to change, and almost every time the first suspect is the same: the state management library.

Bloc is "too much boilerplate." Provider "doesn't scale." Maybe they should move everything to Riverpod. Or GetX. Or back again.

I've been in that conversation more than once. And the uncomfortable truth is that the library is rarely the real problem.

On a scaling Flutter app, what usually hurts is missing boundaries — where state lives, who owns it, how long it should live, and what is allowed to depend on it.

This is a write-up of one of those situations, and how restructuring those boundaries — not switching the tool — made a growing app reasonable to change again.

Problem: the app got harder to change every sprint

A growing consumer Flutter app had reached the stage many successful apps eventually reach: features were slowing down, and small changes were causing bugs in unrelated screens.

On paper, the app "used a state management solution." In practice, state was scattered everywhere.

Business decisions lived inside widgets. The same authoritative data was duplicated across multiple parts of the application. Screens reached directly into each other's state. Some state was global simply because making it global was convenient at the time.

Tapping one toggle could trigger work across a much larger part of the widget tree than anyone expected.

The team's instinct was to blame the library and plan a migration. That would have cost weeks and changed very little — because the mess wasn't in the library. It was in the architecture around it.

Constraints

  • A live app with active users, so nothing could regress during the work.
  • Multiple engineers shipping in parallel — no freeze for a rewrite.
  • The fix had to be incremental and teachable, so the team could keep the discipline after I left.

The real diagnosis: missing boundaries, not the wrong library

The questions that actually matter for Flutter state management have surprisingly little to do with Bloc vs Riverpod vs Provider. They are questions about ownership, lifecycle, dependencies, and scope.

  • Where does each piece of state live, and who owns it? If multiple parts of the app maintain independent mutable copies of the same authoritative data, you have a synchronization problem waiting to happen.
  • What is the lifecycle of that state? Does it belong to a widget, a screen, a feature, the current session, or the entire application?
  • Is domain and application logic separated from the UI? When business rules and side effects live inside widgets, they become harder to test, reuse, and reason about independently.
  • What is allowed to depend on the state? A feature should not need to know about the internal state of an unrelated feature just because the state happens to be globally accessible.
  • What is the rebuild surface? A state change should not unnecessarily cause expensive or unrelated parts of the widget tree to do work.

Once you frame the problem this way, the library becomes an implementation detail. The architecture is the thing.

Approach: restructure state around clear boundaries

I didn't switch the state management library. Instead, I put clearer boundaries around it, feature by feature, in production.

One source of truth for authoritative state

Each piece of authoritative data got a clear owner. Screens could derive local UI state from that data, but they no longer maintained competing copies of the same information that could drift out of sync.

This distinction matters. Not every local copy is a problem. A search field can have local text state while the search results belong to a feature-level state holder. An edit form can track whether it is dirty while the underlying user profile has a different owner.

The goal is not to eliminate all local state. The goal is to eliminate multiple competing sources of truth.

Business logic out of the widgets

The UI became a thin layer that renders state and emits intent. Domain decisions, application workflows, and side effects moved into testable units below the UI.

That doesn't mean every if statement needs to leave a widget. Simple presentation logic can remain close to the UI. The important distinction is that widgets should not own domain rules or application side effects.

A widget should not be the only place where the application knows how a payment is processed, how an order transitions state, or what happens when a user completes a workflow.

State scoped to where it is actually needed

Global state was demoted to feature-level, screen-level, or local state wherever its actual lifecycle allowed it.

The rule was simple: state should live at the narrowest lifecycle and ownership scope that satisfies its consumers.

Some state belongs to a widget. Some belongs to a screen. Some belongs to a feature. Some genuinely belongs to the application or user session.

The mistake is not having global state. The mistake is making state global by default.

Rebuild surfaces made intentional

We stopped treating "fewer rebuilds" as the goal. The goal was predictable rebuild behavior.

Flutter is designed to rebuild widgets. A rebuild itself is not automatically a performance problem. The problem appears when state changes cause unnecessary work across expensive or unrelated parts of the widget tree.

By narrowing state dependencies and separating unrelated concerns, the rebuild surface became smaller and easier to reason about. A change in one feature was less likely to cause unrelated features to participate in the same update.

Each step shipped behind the existing UI and was verified in production before moving on. No big-bang rewrite. No risky flag day.

Technical decisions

  • Kept the existing state management library — the team already knew it, and migration churn is a cost, not a feature.
  • Introduced clearer boundaries between presentation, application/domain logic, and data access, so business rules could be unit-tested without a widget tree.
  • Established a simple rule the team could apply without me: state lives at the narrowest scope that still works, and widgets do not own domain rules or application side effects.
  • Distinguished between authoritative state and local UI state, so local state could remain local without creating competing sources of truth.
  • Used Flutter's performance tooling — including the performance overlay, DevTools profiling, and rebuild inspection — to identify over-scoped state and expensive rebuild paths before they became recurring problems.

Outcome

  • The busiest screens performed less unnecessary work during common state changes, and the app felt snappier without a UI redesign.
  • Features stopped causing as many regressions in unrelated parts of the app because state dependencies were more explicit and less likely to leak across boundaries.
  • New engineers could more easily follow where data came from, who owned it, and what was allowed to change it.
  • The team could reason about state again.

Most importantly, shipping a feature stopped feeling like defusing a bomb.

No library migration. No rewrite. Just boundaries.

Lessons: how to think about Flutter state management

If your Flutter app is getting harder to change, resist the urge to immediately reopen the Bloc-vs-Riverpod debate. Ask the boundary questions first.

1. Who owns this state? Every piece of authoritative state should have a clear owner. Not necessarily one mutator. Multiple parts of an application may legitimately send intents that change the same state. But they should not each maintain independent copies of the truth.

2. How long should this state live? Does it belong to:

  • A widget?
  • A screen?
  • A feature?
  • The current session?
  • The entire application?
  • Persistent storage?

State should not have a longer lifecycle than its consumers require.

3. What is presentation logic, and what is business logic? Not every conditional belongs in a domain layer. But domain rules, workflows, and side effects should not be hidden inside widgets where they can only be exercised through a widget tree.

4. Who is allowed to depend on this state? The more places that can reach into a state holder, the harder it becomes to understand its impact. Explicit dependencies are easier to reason about than convenient global access.

5. What work does a state change trigger? Don't optimize for an arbitrary rebuild count. Understand the rebuild surface. A small widget rebuilding is usually fine. An expensive subtree repeatedly rebuilding because of unrelated state is a different problem.

The right goal is not "never rebuild." The right goal is to make rebuilds and their costs predictable.

The library is not the architecture

Pick a state management approach your team genuinely understands — Bloc, Riverpod, Provider, or something else. A tool the team can reason about beats a "better" tool the team cannot use consistently.

Then spend your real effort on boundaries:

  • One source of truth for authoritative state.
  • Clear ownership.
  • State scoped to the narrowest appropriate lifecycle.
  • Domain rules and side effects separated from the UI.
  • Explicit dependencies between features.
  • Rebuild surfaces that are understood rather than accidentally broad.

The library determines the mechanics. The boundaries determine whether the application remains understandable.

A state management library can help you store, expose, and react to state. It cannot decide who owns that state, how long it should live, or which parts of your application are allowed to depend on it.

That is architecture. And architecture is what determines whether a growing Flutter app stays maintainable.

Facing something similar?

If your mobile app is slow, unstable, or hard to change, a short call is the fastest way to find out whether I can help.

Book a call