All articles
September 21, 20262 min read#flutter#clean-architecture#dart#modular

Clean Architecture in Flutter: The Modular Structure I Use on Every Project

The exact package layout, dependency rules, and conventions behind the Flutter apps I ship, from 100K concurrent users at Mengantar to small in-house products. Opinionated, and battle-tested.

Every Flutter project I start today uses the same structure. It is the structure behind Mengantar, Formulir, KosPro, and the Ninaite apps, and it is what my open-source CLI and the UFIL plugin generate. I am sharing it because the question I get most from mentees is not "how do I use BLoC" but "where do I put things".

The shape

A workspace with multiple Dart packages, not one giant lib/ folder:

app/                 thin shell: routing, DI wiring, theme
core/
  core_network/      Dio client, interceptors, error mapping
  core_storage/      secure storage, preferences, local DB
  core_ui/           design system widgets, theme tokens
  core_di/           service locator setup
features/
  auth/
  orders/
  profile/

Each feature package has the same three layers inside:

features/orders/lib/
  data/        remote + local data sources, DTOs, repository impl
  domain/      entities, repository interfaces, use cases
  presentation/ blocs, pages, widgets

The only rule that matters

Dependencies point inward. presentation may import domain. data may import domain. domain imports nothing from the other two. Features never import each other directly; if orders needs something from auth, it goes through an interface in core or an event on a shared bus.

That single rule is what makes the structure worth the ceremony. When a screen misbehaves you know the bug is in presentation or the use case it calls. When an API changes, only data changes. When a junior joins, they can read one feature end to end in an afternoon.

What goes where, concretely

  • Entity: a plain Dart class with the fields the UI actually needs. No fromJson.
  • DTO: the shape the backend sends. Has fromJson. Lives in data and never leaks to the UI.
  • Repository interface in domain, implementation in data. The implementation decides whether to hit the network or the cache.
  • Use case: one public method, one job. GetOrderDetail, SubmitOrder. Blocs call use cases, never repositories.
  • Bloc: translates events into states by calling use cases. Zero if (response.statusCode == 200) logic. That belongs in data.

Why packages instead of folders

Folders are suggestions. Packages are enforced. If orders is its own package and auth is not in its pubspec.yaml, the import simply fails. On a team of five with weekly releases that enforcement is the difference between an architecture and a diagram.

Packages also give you faster incremental builds, isolated tests you can run per feature in CI, and the ability to hand one feature to one developer without merge conflicts in a shared lib/.

The cost, honestly

Setup is slower. A new feature means a new package, three folders, a pubspec.yaml, and DI registration. On the first project I did this by hand and it took twenty minutes per feature. That is exactly why I wrote a CLI in Rust to generate it, and later an AI plugin that follows the same conventions. Once the scaffolding is automated the cost is close to zero and the benefits compound for the life of the app.

If you take one thing

Start with the dependency rule and the three layers inside a feature. You can move to packages later. But decide the rule on day one, write it down in the README, and make the first pull request that breaks it a teaching moment rather than a precedent.

Written by
Ghozi Mahdi
Senior Mobile Engineer · Founder, Rancangin Aja
Get in touch