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 indataand never leaks to the UI. - Repository interface in
domain, implementation indata. 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 indata.
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.