NineOne logo

NineOne

Flutter Clean Architecture in the AI Agent Era

How Flutter developers can combine Clean Architecture with AI coding agents like Codex, Claude Code, Cursor, and Copilot without sacrificing maintainability, testing, and code quality.

Introduction

Flutter teams are entering a different kind of workflow. The old baseline was simple: developers read a ticket, write code by hand, run tests, open a pull request, and repeat. The new baseline is more asymmetric. A coding agent can draft a feature, update tests, refactor a repository, wire navigation, and even prepare a review-ready pull request. OpenAI describes Codex as a software engineering agent that can work on many tasks in parallel, answer questions about a codebase, fix bugs, and propose pull requests for review in isolated environments. Anthropic and GitHub are pushing in the same direction with Claude Code and Copilot agents.

That does not make architecture less important. It makes it easier to damage a codebase faster.

In production Flutter apps, the real cost is rarely in generating a screen widget. The cost is in keeping authentication predictable, keeping local storage migrations safe, keeping API contracts contained, keeping tests relevant, and keeping feature growth from turning into a dependency graph nobody wants to touch. I have seen this most clearly in apps with API integrations, local persistence, WebView bridges, release toggles, and a growing set of feature modules. The code that hurts a team is usually not the first version. It is the fifth rushed version with unclear ownership.

That is why Clean Architecture still matters in the AI agent era. AI can accelerate implementation. It cannot own engineering judgment.

Why Clean Architecture Still Matters

Flutter’s official architecture guidance says separation of concerns is the most important principle, with clear UI and data layers, and optionally a domain layer when logic becomes complex enough to justify it. Flutter also explicitly recommends the repository pattern in the data layer and dependency injection to avoid globally accessible objects that make code more error-prone.

That advice becomes more valuable when AI starts editing the codebase.

Without strong boundaries, agents tend to optimize locally. They will often place logic where it is easiest to satisfy the prompt:

  • API calls inside widgets because the screen already has the context
  • storage reads inside a controller because the value is needed quickly
  • duplicated mapping logic because it is easier than finding the existing abstraction
  • new helper classes that partially overlap with the old ones

A human developer notices that this creates long-term drag. An agent usually does not unless you specify the rules clearly.

Clean Architecture is still relevant because it gives the project a durable set of constraints:

  • presentation renders state and sends user intent
  • domain defines business rules and use cases
  • data handles APIs, databases, platform channels, and mapping
  • dependency injection controls object ownership and wiring

When those rules are explicit, AI-generated code becomes easier to review, reject, or repair. The architecture is not there to impress other engineers. It is there to stop accidental chaos from becoming permanent structure.

The New Role of Flutter Developers in the AI Agent Era

The Flutter developer’s job is shifting upward.

You still need to understand widgets, asynchronous flows, plugin boundaries, rendering cost, app lifecycle behavior, and release workflows. But you spend less time typing repetitive scaffolding and more time doing higher-leverage work:

  • defining the feature boundary before code exists
  • deciding whether a new use case belongs in an existing module or a new one
  • protecting domain rules from leaking into UI code
  • reviewing generated code for architecture drift
  • validating behavior on real devices
  • deciding which tests are evidence and which are just ceremony

GitHub’s Copilot agent workflow is explicit about this: after the agent finishes, you still review the output yourself, request changes, make edits manually when needed, and only merge when satisfied. That is the correct mental model. AI is a contributor, not the approver.

For Flutter teams, this role shift is practical. If an agent generates a repository, DTO mapper, and widget test in minutes, your value is no longer in writing those lines slower by hand. Your value is in deciding whether the repository should exist at all, whether it owns the right dependencies, whether the test actually protects behavior, and whether the feature still fits the app’s architecture.

What AI Agents Are Good At

AI agents are useful when the task is bounded, repetitive, and verifiable.

In Flutter projects, that often includes:

  • creating a first draft of a feature module from an existing pattern
  • generating DTOs, mappers, and repository method skeletons
  • wiring dependency injection registrations
  • adding unit tests around pure use cases
  • writing widget tests for known UI states
  • refactoring repetitive boilerplate across multiple files
  • documenting an existing flow after the architecture is already clear

They are especially effective when the repository already has strong conventions. If your auth, profile, and orders features all follow the same module shape, the agent can usually produce a useful implementation draft quickly.

They are also good at tedious but necessary consistency work. In a production Flutter codebase, that might mean renaming a repository method across features, updating constructor injection, adding logging hooks, or writing failing tests for an existing bug before a fix is attempted.

OpenAI’s Codex and Claude Code both emphasize agent workflows that can read files, edit code, run commands, and work within permission boundaries. That capability is useful precisely because many engineering tasks are multi-file tasks. But that same capability is why the codebase needs stronger guardrails, not fewer.

What AI Agents Should Not Own

There are parts of a Flutter app I would not hand over without tight human control:

  • architecture decisions for new features
  • authentication and session boundaries
  • payment and WebView bridge security flows
  • destructive local database migrations
  • release configuration, signing, and environment wiring
  • privacy-sensitive analytics and logging decisions
  • final pull request approval

The common theme is irreversible cost. If an agent writes a mediocre widget test, you can replace it. If it quietly spreads auth logic across screens, or mixes local cache invalidation with UI state updates, you inherit structural debt that gets more expensive every sprint.

This is also where Clean Architecture helps. If the app has clear rules such as “widgets do not call services directly” or “repositories are the only source of truth for persisted feature data,” then AI output can be reviewed against a stable checklist instead of a vague feeling.

Human developers should own:

  • the module boundaries
  • the dependency direction
  • the data ownership rules
  • the review standard
  • the release decision

A practical structure for AI-assisted Flutter work is feature-first at the module level, with clear presentation, domain, and data sublayers inside each feature, plus a shared core for app-wide infrastructure.

This works well because it keeps related feature code together while still preserving dependency rules. It also gives agents a visible pattern to follow.

For most production apps, I recommend:

  • presentation for screens, controllers, view models, state notifiers, and widgets
  • domain for entities, repository contracts, and use cases
  • data for repository implementations, remote data sources, local data sources, DTOs, and mappers
  • core for app-wide concerns such as networking, storage engines, logging, theming, and shared utilities
  • app for dependency injection, routing, and environment bootstrapping

For local persistence, keep the storage technology behind a local data source abstraction. Whether the feature uses Hive, Drift, SQLite, or Isar, the rest of the app should not care. The repository should orchestrate remote and local sources, not the widget tree.

Decision Flow for Human and AI Agent Collaboration

flowchart TD
    A[Product Requirement] --> B[Human Developer Defines Architecture]
    B --> C[AI Agent Generates Implementation Draft]
    C --> D[Run Unit and Widget Tests]
    D --> E{Tests Passed?}
    E -- No --> F[AI Agent Fixes Issues]
    F --> D
    E -- Yes --> G[Human Code Review]
    G --> H{Architecture Approved?}
    H -- No --> I[Refactor with Human Guidance]
    I --> D
    H -- Yes --> J[Pull Request Ready]

Example Folder Structure

lib/
|-- app/
|   |-- di/
|   |   `-- injector.dart
|   |-- router/
|   |   `-- app_router.dart
|   `-- bootstrap/
|       `-- app_bootstrap.dart
|-- core/
|   |-- network/
|   |   |-- api_client.dart
|   |   `-- auth_interceptor.dart
|   |-- storage/
|   |   |-- app_database.dart
|   |   `-- secure_storage_service.dart
|   |-- error/
|   |   `-- failures.dart
|   `-- utils/
|       `-- result.dart
|-- features/
|   |-- auth/
|   |   |-- presentation/
|   |   |   |-- pages/
|   |   |   |   `-- login_page.dart
|   |   |   |-- controllers/
|   |   |   |   `-- login_controller.dart
|   |   |   `-- widgets/
|   |   |       `-- login_form.dart
|   |   |-- domain/
|   |   |   |-- entities/
|   |   |   |   `-- session.dart
|   |   |   |-- repositories/
|   |   |   |   `-- auth_repository.dart
|   |   |   `-- usecases/
|   |   |       |-- sign_in.dart
|   |   |       |-- sign_out.dart
|   |   |       `-- restore_session.dart
|   |   `-- data/
|   |       |-- datasources/
|   |       |   |-- auth_remote_data_source.dart
|   |       |   `-- auth_local_data_source.dart
|   |       |-- models/
|   |       |   `-- session_dto.dart
|   |       |-- mappers/
|   |       |   `-- session_mapper.dart
|   |       `-- repositories/
|   |           `-- auth_repository_impl.dart
|   `-- orders/
|       |-- presentation/
|       |-- domain/
|       `-- data/
`-- test/
    |-- features/
    |   |-- auth/
    |   `-- orders/
    `-- core/

This structure is opinionated, but it scales. It is also compatible with Flutter’s recommendation that data-layer objects are organized by type while UI code often benefits from feature grouping. If a team prefers Riverpod, Provider, or get_it, that decision changes the wiring, not the dependency direction.

Example Feature Flow

Take a profile feature that shows user data from an API and caches it locally for offline startup:

  1. ProfilePage triggers LoadProfileController.load().
  2. The controller calls GetProfile in the domain layer.
  3. GetProfile depends on ProfileRepository.
  4. ProfileRepositoryImpl decides whether to read a cached profile from Drift or fetch fresh data from the remote data source.
  5. Remote DTOs are mapped into domain entities before returning upward.
  6. The controller updates UI state.
  7. Widget tests verify the loading, success, and error states.

That flow matters because it gives AI a safe lane. The agent can draft the repository, mapper, local data source, and widget tests. But it should not invent a shortcut such as calling the REST client directly from the page just because it is faster to generate.

How to Make Your Flutter Codebase AI-Agent Friendly

If you want agents to work safely, optimize the repository for clarity, not just for humans who already know the codebase.

The most useful changes are usually boring:

  • keep module boundaries obvious
  • use consistent naming for repositories, use cases, and state classes
  • keep dependency injection registration centralized
  • isolate plugin and platform-specific code behind adapters
  • avoid giant files that mix UI, business logic, and IO
  • document feature rules in README or module notes
  • make tests easy to run per feature

Martin Fowler’s long-standing point about dependency injection still applies: separate configuration from use. In Flutter terms, that means screens should not be instantiating the services they depend on. Wiring belongs in the composition root, whether that root is get_it, Provider, Riverpod providers, or another DI strategy.

An AI-friendly codebase is not a codebase optimized for generated code. It is a codebase with enough structure that generated code has fewer places to hide mistakes.

Code Review Checklist for AI-Generated Flutter Code

When reviewing AI-generated Flutter code, I would check the following before caring about style:

  • Does the dependency direction still point inward?
  • Did any widget gain direct knowledge of APIs, databases, or storage?
  • Did the agent duplicate an existing repository or use case instead of extending it?
  • Are domain entities still free from framework and transport details?
  • Are DTOs and database models confined to the data layer?
  • Did the agent introduce silent global state?
  • Are plugin calls isolated behind services or data sources?
  • Did the AI invent package APIs or misuse an existing Flutter package?
  • Are error states, loading states, and empty states covered?
  • Do the tests validate behavior or only mirror the implementation?

This review is where most of the value is. Copilot’s own review guidance treats agent output like any other contributor’s pull request. That is the right standard for Flutter as well.

Testing Strategy

Flutter’s testing guidance is still the baseline here: many unit and widget tests, plus enough integration tests to cover the important use cases.

In an AI-assisted codebase, I would split tests this way:

  • unit tests for use cases, validators, mappers, and repository decision logic
  • widget tests for screen states, user input, and UI reactions
  • integration tests for end-to-end flows such as sign-in, checkout, onboarding, and persistence recovery

A few practical rules matter:

  • If the logic is pure, require unit tests.
  • If the feature changes visible state, require widget tests.
  • If the feature touches navigation, persistence, auth, or remote sync, require at least targeted integration coverage.

For apps using local storage, I also want tests around repository behavior when remote and local data disagree. That is where subtle bugs often appear, especially after an agent “helpfully” refactors cache logic.

Common Mistakes When Using AI Agents in Flutter Projects

The most common mistakes are not model mistakes. They are process mistakes:

  • treating generated code as reviewed code
  • prompting for implementation before defining architecture
  • allowing agents to create new abstractions without constraints
  • mixing feature structure styles inside the same codebase
  • trusting passing tests that do not cover the real risk
  • letting agents update packages or plugins without platform validation
  • skipping real-device checks for lifecycle, permissions, WebView, and performance-sensitive flows

In Flutter, a code change can look fine in a diff and still fail where it matters: Android lifecycle restoration, iOS permission prompts, offline cache restore, deep links, or a WebView JavaScript bridge. Those are not places for blind trust.

Practical Workflow: From Ticket to Pull Request

The ideal workflow is explicit and repeatable:

  1. The human developer reads the ticket and decides the feature boundary.
  2. The human defines where the use case, repository contract, and state owner belong.
  3. The AI agent is asked to generate a constrained implementation draft inside that boundary.
  4. The agent adds or updates unit and widget tests.
  5. The team runs tests and static analysis.
  6. The human reviews architecture, naming, dependency direction, and edge cases.
  7. The agent fixes bounded review comments.
  8. The human validates critical flows on devices or emulators.
  9. The pull request is opened only after architecture and tests are acceptable.

That workflow preserves the right division of labor:

  • humans define structure, policy, and quality thresholds
  • agents produce draft implementation and repetitive changes
  • tests provide fast feedback
  • review decides whether the code is allowed to stay

Conclusion

Clean Architecture does not become obsolete when AI agents arrive. It becomes more useful.

In Flutter, AI can accelerate module scaffolding, repository wiring, test generation, and repetitive refactors. But the more capable the agent becomes, the more important it is to preserve explicit boundaries between presentation, domain, and data. That is what keeps API handlers, local databases, state management, and feature growth from collapsing into a pile of convenient shortcuts.

The practical goal is not to block AI from helping. It is to build a Flutter codebase where help does not quietly turn into structural debt.

The strongest teams in the AI agent era will not be the teams that generate the most code. They will be the teams that define architecture clearly, review aggressively, test the right layers, and keep human control over the decisions that shape the app long after the prompt is gone.

References

Next step

Want to see the build side too?

Browse the project archive to see how these notes translate into product work, or get in touch if you want a similar implementation approach on your team.