← All writing

Flutter architecture that grows with your app

Start with clear boundaries, keep widgets focused, and give your app room to grow without a folder for every idea.

A new Flutter project can become complicated before the first feature works. There are folders to name, state management packages to compare, and patterns to choose. It is easy to mistake those decisions for progress.

A more useful starting point is one small feature with clear responsibilities. Consider a reading list: load saved articles, display them, and let the reader save another one. That is enough to discover the boundaries your application actually needs.

Start with responsibilities

Flutter’s architecture guide separates presentation from data, with views and view models on one side, and repositories and services on the other. These are useful responsibilities to recognize even if your project uses different names. See the official Flutter architecture guide.

For a reading list, the split could look like this:

  • View: displays articles, a loading indicator, an empty state, or an error.
  • State holder: decides what the screen should display and responds to user actions.
  • Repository: provides the saved articles in the format the feature understands.
  • Service: communicates with the database or API.

The important question is whether a change to your API response forces you to edit widget code. If it does, the boundary probably needs attention.

Give the feature a small interface

Begin with the operation the screen needs. Here is a deliberately small Dart example:

class SavedArticle {
  const SavedArticle({required this.id, required this.title});

  final String id;
  final String title;
}

abstract interface class ReadingListRepository {
  Future<List<SavedArticle>> load();
}

class InMemoryReadingListRepository implements ReadingListRepository {
  @override
  Future<List<SavedArticle>> load() async {
    return const [
      SavedArticle(id: 'first-note', title: 'A useful first feature'),
    ];
  }
}

This interface does not need a generic base repository, a networking framework, or a dependency injection package. It gives you a seam: production code can load remote data while a test can return a predictable list.

Keep the example’s limitation in mind. It only loads articles. Saving, caching, and conflict handling should be designed when the feature actually needs them.

Make every screen state intentional

A list of articles is only one state. A usable feature also accounts for the moments before and around that result.

State What the reader should see
Loading A clear indication that the request started
Empty An explanation and a useful next action
Loaded The reading list
Failed A readable error and a way to try again
Refreshing Existing content with a refresh indicator

Refreshing deserves special care. Throwing away useful content just because a new request started makes the screen feel less reliable. Decide whether your feature should keep the last successful result while refreshing.

Organize around the change you expect

For a small feature, an understandable folder layout might be enough:

lib/
  reading_list/
    saved_article.dart
    reading_list_repository.dart
    reading_list_state.dart
    reading_list_screen.dart

Split it further when the files or responsibilities become difficult to navigate. The test is practical: can another developer find the code that owns a behavior without opening half the project?

A folder structure will not enforce a dependency boundary for you. Code review, imports, and tests still matter.

Test the boundary before adding layers

A useful first test asks what happens when the repository fails. Another checks an empty result. A third checks that a later refresh does not accidentally replace newer data with an older request.

These tests reveal more about the design than testing whether a widget contains a particular padding value.

Start with one feature, make its states explicit, and keep the data contract small. Add another layer when you can explain the problem it solves in the app you have today.

Something to add? I’d love to hear it.

Let’s talk about it ↗