Dart Tutorial

Flutter Lesson 73 of 83 4 min read

Flutter App Architecture: Layers and Folder Structure

Structure a Flutter app with clear layers and a feature-first folder layout: UI, logic and data, and the rules for what depends on what.

On this page

Architecture is deciding where code goes and what may depend on what. A good structure means that when you change how data is stored, no screen has to change, and when a designer moves a button, no business rule is touched.

The problem #

A screen written the quick way does everything:

class _OrdersPageState extends State<OrdersPage> {
  List<Order> _orders = [];

  Future<void> _load() async {
    final response = await http.get(Uri.parse('https://api.example.com/orders'));  // networking
    final list = jsonDecode(response.body) as List;                                // parsing
    final orders = list.map((e) => Order.fromJson(e)).toList();
    orders.removeWhere((o) => o.isCancelled);                                      // business rule
    final prefs = await SharedPreferences.getInstance();                           // caching
    await prefs.setString('orders', response.body);
    setState(() => _orders = orders);                                              // UI state
  }
  // ...and the build method
}

It works. It also cannot be tested without a server, cannot be reused by another screen, and mixes five reasons to change into one file.

Two layers, four kinds of class #

┌──────────────────────────── UI layer ─────────────────────────────┐
│  View        widgets: what the user sees and touches              │
│  ViewModel   the screen's state and the actions it offers         │
└───────────────────────────────┬───────────────────────────────────┘
                                │ calls
┌──────────────────────────── Data layer ───────────────────────────┐
│  Repository  the single source of truth for one kind of data      │
│  Service     talks to one outside system: an API, a database      │
└───────────────────────────────────────────────────────────────────┘
ClassResponsibilityKnows about
ViewDisplays state, forwards user actionsIts view model
ViewModelHolds the screen’s state, turns actions into callsRepositories
RepositoryDecides where data comes from (network or cache), converts raw data into models, enforces rulesServices
ServiceWraps one data source and returns raw resultsNothing in the app

The models (Order, User, Product) are plain immutable classes used by every layer.

The dependency rule #

Dependencies point downwards only.

  • A view never calls a service or a repository directly.
  • A repository never imports a widget.
  • A service knows nothing about the rest of the app.

Following this one rule gives you most of the benefit. You can replace the API with a different one by changing a service. You can test a view model with a fake repository. You can redesign a screen without reading any data code.

Folder structure #

Group by feature first, then by layer inside the feature.

lib/
  main.dart                     # entry point: creates dependencies, runs the app
  app.dart                      # MaterialApp, theme, router

  features/
    orders/
      data/
        order_api.dart          # service
        order_repository.dart   # repository
      domain/
        order.dart              # model
      ui/
        orders_page.dart        # view
        orders_view_model.dart  # view model
        widgets/
          order_tile.dart
    auth/
      data/
      domain/
      ui/

  core/
    network/
      api_client.dart           # shared HTTP setup
    storage/
      key_value_store.dart
    theme/
      app_theme.dart
    widgets/                    # widgets shared by several features
      error_view.dart
      primary_button.dart
    utils/
      formatters.dart

test/
  features/
    orders/
      order_repository_test.dart
      orders_view_model_test.dart
      orders_page_test.dart

Everything about orders is in one folder. Deleting a feature means deleting a folder. Two developers working on different features rarely touch the same files.

The alternative, layer-first (lib/models/, lib/screens/, lib/services/), is fine for a small app and common in tutorials. In a large app, one feature ends up spread across six distant folders.

What is the “domain” layer? #

Some architectures add a third layer between UI and data, holding use cases: one class per business operation, such as PlaceOrder or GetNearbyShops. It is useful when:

  • Logic combines several repositories.
  • The same logic is needed by several view models.
  • The rules are complicated enough to deserve their own tests.

For most apps, start without it. Add a use case when a view model starts to contain logic that another screen also needs.

Naming #

ThingConventionExample
Filessnake_case.dartorder_repository.dart
A screen...Page or ...ScreenOrdersPage
State holder...ViewModel, ...Notifier, ...CubitOrdersViewModel
Repository...RepositoryOrderRepository
Service...Api, ...Service, ...DaoOrderApi

Pick one set and use it everywhere. Consistency lets anyone find a file without searching.

How much is enough? #

AppReasonable structure
A learning project, one to three screensWidgets with setState. One file per screen
A small app, a handful of screensViews, view models (ChangeNotifier), one repository per data type
A product with a teamFull feature-first layout, repositories, services, dependency injection, tests per layer

Do not build the third for an app that needs the first. Structure should grow with the app, and moving code into layers later is straightforward.

Signs the structure is working #

  • You can describe where any piece of code lives without looking.
  • A change to the API touches files in data/ only.
  • View models can be tested without Flutter widgets.
  • Views contain almost no if statements about business rules.
  • New features follow the same pattern as existing ones.

The next lesson builds one feature this way, end to end.

Try it yourself #

Take an app you built earlier in the course and sort its code into four groups: views, state, repositories and services. Write down each place where a widget makes a network call or reads storage directly. Those are the lines to move first.

Practise in the playground Updated by Santosh Adhikari