The BLoC Structure That Finally Let Our Flutter App Scale

Dhruva ShaivaDhruva Shaiva
4 min read

Let's be honest. Every growing Flutter project eventually hits a wall.

It starts out fun. You're building features, everything works, and the code is clean. Then, a few months pass. More developers join the team. Deadlines get tighter. Suddenly, you're looking at a feature and have no idea where the state logic lives. Changing one small thing breaks three others. Onboarding a new developer takes weeks instead of days.

If you've ever felt that pain, you're not alone. We've all been there.

It taught us a valuable lesson: the secret to scaling isn't just picking a state management library like BLoC. It's about being ruthlessly disciplined in how you structure it.

This isn't some academic "clean architecture" theory. This is the real-world, battle-tested playbook we now live by.

My Guiding Philosophy: A "Boring" Codebase is a Good Codebase

It took me years to appreciate this, but my goal as a senior dev is to make our codebase as "boring" and predictable as possible.

"Boring" means I can jump into any feature, even one I didn't write, and I instantly know where to find the UI, the state logic, and the data models. It means a junior dev can be productive on their first day because the patterns are consistent everywhere.

Here's how we achieve that boring, beautiful consistency.

Rule #1: We ONLY Use a Feature-First Folder Structure

Everything is feature-driven.

When we start a new feature, say, "product reviews," we create a single folder for it. Everything related to product reviews lives inside.

lib/src/features/authentication/
├── data/
│   ├── models/
│   └── repositories/
├── presentation/
│   ├── bloc/
│   ├── view/
│   └── widgets/

The beauty of this is that if we need to change the authentication feature, we live inside this one folder. If a developer is working on it, they aren't causing merge conflicts with someone working on the "user profile" feature. It's self-contained, clean, and just makes sense.

Rule #2: We Slayed Our Boilerplate with a Generic State

You know the drill. For every API call, you need states for Loading, Success, and Failure. In the beginning, we were writing these same three classes inside every single BLoC's state file. It was soul-crushing, repetitive work.

This was probably the biggest productivity game-changer we implemented. We created a single, generic DataState class that we now use across the entire application.

It looks something like this. Feel free to steal it.

// lib/src/core/utils/data_state.dart
// (Your reusable DataState code from the previous example goes here)
abstract class DataState<T> { ... }
class DataSuccess<T> extends DataState<T> { ... }
class DataFailure<T> extends DataState<T> { ... }
class DataLoading<T> extends DataState<T> { ... }

Now, our BLoC definitions are clean and simple: class AuthenticationBloc extends Bloc<AuthenticationEvent, DataState<List<AuthenticationModel>>>

We instantly know it will have a loading state, a failure state with an error, and a success state with a list of authentication. No more reinventing the wheel for every single feature.

Rule #3: A BLoC Has Only One Job. Period.

"Should I just add this logic to the UserProfileBloc?"

I get asked this all the time. My answer is almost always a question back: "Is it part of the user's core profile, or is it a related, but separate, piece of data?"

Our rule is simple: a BLoC is responsible for the state of one logical thing.

A UserProfileBloc handles the user's name, email, and profile picture. It should absolutely NOT also handle the list of their past orders. That's a separate logical entity. That deserves its own BLoC.

When you let a BLoC do too many things, you create a "God Object"—a monstrous class that's impossible to test and terrifying to modify. By keeping them small and focused, they stay nimble and easy to understand. If a screen needs data from both, that's fine! Provide both Blocs to that part of the widget tree.

It's Your Turn

Look, this isn't the only way to build a Flutter app, but it's the way that has brought scalability to our team.

It’s about creating a system that frees you up to be creative.

I'm genuinely curious—how does your team solve these problems? What architectural rules do you live by? Drop a comment below. Let's talk about it.

0
Subscribe to my newsletter

Read articles from Dhruva Shaiva directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dhruva Shaiva
Dhruva Shaiva