skip to content

How does the official Flutter architecture guide's Compass app organize lib/, and why does it mix feature-first and layer-first folders?

level: juniorimportance: must knowfreq 58%

answer

  1. one folder split by feature, one by type
  2. ui/<feature>/view_models and widgets
  3. data/repositories and data/services shared
  4. ui/core for shared widgets and themes
  5. testing/ holds fakes beside test/

basics

~20 s

The Compass app groups lib/ui by feature, because each feature has one view and one view model, but groups lib/data by type, because repositories and services are shared across features. Domain models sit in lib/domain, used by both layers.

solid answer

~40 s

The Flutter team's case study uses a hybrid. `lib/ui/<feature_name>/` holds that feature's `view_models/` and `widgets/` (including `<feature_name>_screen.dart`), because a view and its view model belong to exactly one feature. `lib/data/` is split by type into `repositories/`, `services/` and `model/` (API models), because several view models reuse the same repository or service. `lib/domain/models/` holds the app's data types that both layers use, and `lib/ui/core/` collects widgets and themes shared by many views. Alongside `lib/` sit `test/`, which mirrors `lib/`, and `testing/`, a subpackage of fakes and test utilities. The rule underneath: group code by who owns it, so feature-owned code lives with the feature and shared code lives by role.

go deeper

for a junior

Recall the two grouping styles and the Compass split: ui by feature, data by type, domain models shared, ui/core for shared widgets.

for a middle

Explain the reason for the split: views and view models are owned by one feature, while repositories and services are reused by several view models.

for a senior

Show where folder conventions stop working, such as features importing each other's widgets, and how those ownership lines become package boundaries later.

for a principal

Frame folder structure as ownership: argue when a single package with conventions is enough and what evidence would justify splitting it.

## Two ways to fold a Flutter codebase A Flutter app's `lib/` directory can be organized in two broad ways: - **Feature-first** (by feature): everything one feature needs lives together, e.g. an `auth/` folder with its screen, view model and use cases. - **Layer-first** (by type): every class of one architectural role lives together, e.g. `repositories/`, `services/`, `view_models/` folders spanning all features. The official Flutter app-architecture guide does not pick one. Its worked example, the **Compass app** in the case study, uses a deliberate **combination**, and the reason for the split is the part interviewers want to hear. ## The Compass layout ```text lib/ ui/ core/ ui/ # shared widgets, e.g. brand-styled buttons themes/ <feature_name>/ view_models/ <view_model_class>.dart widgets/ <feature_name>_screen.dart <other_widgets> domain/ models/ # app data types used by data and ui data/ repositories/ services/ model/ # API models config/ utils/ routing/ main.dart main_development.dart main_staging.dart test/ # unit and widget tests, mirrors lib/ testing/ # fakes and models other packages' tests reuse ``` ## Why the UI layer is feature-first In the guide's MVVM layering, a **view** (the widgets of one screen) and its **view model** (the object holding that screen's UI state and commands) are paired: each feature has exactly one view and one view model. Nothing else reuses them. Keeping them under `ui/<feature_name>/` means: - a change to one screen touches one folder; - deleting a feature means deleting one folder; - a new team member finds a screen by its product name, not by guessing which role folder it hides in. ## Why the data layer is layer-first **Repositories** (the source of truth for one kind of data, e.g. bookings) and **services** (thin wrappers around one external source, e.g. an HTTP API or local storage) are not owned by one feature. A booking repository can serve the booking screen, the home screen and the activities screen. If you put it inside one feature's folder, every other feature has to reach into that folder, which hides a shared dependency behind one feature's name. So the data layer is grouped by role instead. The **domain** folder holds the app's own data types, because both the data layer (which produces them) and the UI layer (which renders them) depend on them. ## The supporting folders 1. `ui/core/` collects widgets and theme logic shared by several views, such as buttons with brand styling, so the feature folders don't import each other. 2. `config/`, `utils/` and `routing/` hold app-wide wiring rather than feature code. 3. Three entry points (`main.dart`, `main_development.dart`, `main_staging.dart`) start the same app against different environments. 4. `test/` mirrors `lib/`, so the test for a class is easy to find. 5. `testing/` is a subpackage of **fakes** and test models. The guide describes it as a version of the app you don't ship; other packages' tests can import it. ## How to use this in an interview | Code | Owned by | Grouped | |---|---|---| | Screen widgets, view model | one feature | by feature, under `ui/` | | Repositories, services, API models | many features | by type, under `data/` | | Domain models | data and ui layers | `domain/models/` | | Shared widgets and themes | many views | `ui/core/` | The principle to state is **"group by ownership"**: code owned by one feature lives with that feature; code shared across features lives by role in a neutral place. The Compass layout is one application of it, not a mandate; the guide itself notes the same rules could be written with streams, Riverpod, flutter_bloc or signals instead of `ChangeNotifier` view models, and the folder shape would barely change. Two follow-on points strong candidates add. First, this is still one Dart package: folders are a convention, and nothing stops a view model in one feature from importing another feature's widgets. Second, when an app outgrows that, the same ownership lines become the boundaries of **local packages** (one per feature plus shared core packages), where the Dart package system and lints enforce what folders only suggest. ## Common mistakes - **Pure layer-first at scale**: a global `view_models/` folder with forty files, where every screen change means hunting through several role folders. - **Pure feature-first for shared data**: a `BookingRepository` stuck inside the `booking/` folder, which the home and activities screens then import from a sibling feature. - **A `core` or `common` folder that absorbs everything**: shared UI belongs there, feature logic does not, or the folder becomes the coupling point the layout was meant to avoid. - **Treating the Compass tree as mandatory**: `flutter create` with its default app template puts only `main.dart` in `lib/`; the layout is a recommendation, and a team that follows the ownership rule with different folder names is doing the same thing.

  • Where would you put a widget that two features need in the Compass layout?
    In `lib/ui/core/ui/`, the shared UI folder. Leaving it in one feature's `widgets/` folder forces the other feature to import from a sibling feature, which couples the two and hides the shared dependency behind one feature's name.
  • What does the top-level testing/ folder give you that test/ does not?
    `test/` holds the test files themselves and mirrors `lib/`. `testing/` holds reusable fakes (for example fake repositories) and test models, packaged so tests in other packages can import them. It keeps test doubles out of the shipped `lib/` code while still sharing them.

A shared kitchen with personal lockers: each cook's own knives stay in their locker (feature folders), while the ovens and fridges that everyone uses stand in the common area by type (data folders).

saying these in an interview costs you the question

  • Flutter requires a fixed folder layout that flutter create enforces
  • Puts repositories inside the first feature that needed them
  • Claims the official guide mandates pure feature-first folders
  • Thinks feature folders stop features importing each other
  • Puts view models in a global view_models folder for all screens