---
title: "Keeping Flutter architecture clean long term: modules, DI and custom lints"
description: "How does a Flutter app stay maintainable for years? Three FlutterCon 2026 talks: Lego architecture with 800 packages, dependency injection at enterprise scale, and enforcing architecture with custom lints."
canonical_url: "https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln"
last_updated: "2026-10-21T00:00:00.000Z"
locale: en
meta:
  "og:description": "How does a Flutter app stay maintainable for years? Three FlutterCon 2026 talks: Lego architecture with 800 packages, dependency injection at enterprise scale, and enforcing architecture with custom lints."
  "og:image": "https://robocitrus.com/images/blog/flutter-architektur-module-di-und-lint-regeln.webp"
  "og:title": "Keeping Flutter architecture clean long term: modules, DI and custom lints"
---

> Index of every page on this site: [llms.txt](https://robocitrus.com/llms.txt)

![Flutter architecture that doesn't rot: modules, dependency injection and lint rules (AI-generated cover image)](https://robocitrus.com/cdn-cgi/image/w=2048,q=75/images/blog/flutter-architektur-module-di-und-lint-regeln.webp)

# **Flutter architecture that doesn't rot: modules, dependency injection and lint rules**

[![Maximilian Flechtner](https://robocitrus.com/cdn-cgi/image/q=75/images/about-me-maximilian-image.webp)**Maximilian Flechtner**](https://robocitrus.com/en/about)

Published:October 21, 2026

1 min read

[**Flutter**](https://robocitrus.com/en/blog/tags/flutter) [**Dart**](https://robocitrus.com/en/blog/tags/dart) [**Architecture**](https://robocitrus.com/en/blog/tags/architecture) [**Dependency Injection**](https://robocitrus.com/en/blog/tags/dependency%20injection) [**App-Development**](https://robocitrus.com/en/blog/tags/app-development)

## Architecture rots quietly

No app starts out with bad architecture. It gets bad because a hundred small shortcuts pile up over months. An API client in the wrong layer. One feature quickly reaching into another. A widget built around the design spec.

At FlutterCon 2026 I heard three talks that attack this problem from three sides. The first splits a codebase into small, swappable building blocks. The second wires those blocks back together with dependency injection (DI). The third locks the rules in so nobody can bypass them.

All three came from enterprise projects, two of them banking apps. I usually build apps alone or in small teams. Still, a lot of it applies to my own work.

## Part 1: The Lego architecture

The first talk described a financial platform for small and medium businesses. They moved their native apps to Flutter in just under a year. Today the app has over six million users and contributions from more than 100 mobile engineers.

The core idea: small bricks with a uniform connection interface, like Lego. Every brick is simple, and you can still build almost anything from them.

### Almost everything is a package

The team took this seriously. More than 800 Dart and Flutter packages, each with one job. Everything still lives in a single monorepo. The speaker gave four reasons: everyone sees the whole codebase. A change across several packages goes through in one pull request, with no version juggling. All packages share the same external dependency versions, which makes upgrades (Flutter upgrades included) easier. And because everything sits in one place, dependency direction and package boundaries can be checked automatically.

Then there are effects you wouldn't expect from that many packages. Fewer merge conflicts, because two people rarely touch the same piece. Faster tests and code generation. In CI, each pull request only runs the changed packages and the ones that depend on them. And every package has a natural code owner.

### Three layers, hard rules

At the bottom: core and utilities. Helpers, wrappers around libraries, session, caching, validation, shared models, the design system. In the middle: features, like "create invoice" or "make payment". On top: the application, which plugs core and features together into an app.

The rules: no circular dependencies, anywhere. Core only knows core. Features don't know other features. Otherwise every feature ends up depending on every other one, and you can no longer build an app from a selection of features.

### A feature from the inside

Every feature has the same structure: domain (models, APIs, repositories, use cases), presentation, configuration, DI and navigation. All of it stays private. From the outside there is exactly one entry point, a flow widget.

The flow widget has no UI of its own. It picks the right page based on state. When it is created, it initializes the feature's DI registrations and localization. So the app doesn't boot hundreds of features at startup, only the one the user opens.

I like how they handled navigation. Inside a feature it is declarative. Between features it is imperative, through an abstract interface that each feature declares itself. The real implementation lives in the application layer, the only place that knows every feature. Configuration works the same way: the feature asks an abstract configurator, and the application layer supplies values from build flavors and remote flags.

Some choices deliberately depart from Clean Architecture. No extra abstractions without a reason, since any Dart class can act as an interface. One model set for data and domain, with JSON converters where the backend differs. Pragmatism over textbook.

### Testing with real dependencies

This convinced me most. If you mock every dependency, you end up testing the implementation. The team instead describes requirements at a package's public entry point and lets everything inside the package run for real. Only things from outside get mocked. Backend responses arrive as JSON strings through a [**Dio**](https://pub.dev/packages/dio) interceptor, so deserialization is covered too.

According to the talk, that means fewer tests but over 90 percent coverage. And tests that don't break when you refactor internals.

## Part 2: Dependency injection when the app gets big

The second talk was about getting modules wired back together. You know constructor injection, testability and lifecycle control. Large apps add two problems.

The first is wiring modules: the auth module needs an API client from the networking module, so who provides it? The second is complex scopes. A scope is a group of dependencies with the same lifecycle. A banking app has a global scope, one for the signed-in user, one per context (personal or company) and one per process like "transfer". On logout, everything user-related has to go.

The speaker looked at seven projects from his team. The same tools kept showing up: Provider, a service locator ([**GetIt**](https://pub.dev/packages/get_it) or a homegrown one) and [**Riverpod**](https://pub.dev/packages/riverpod).

### Provider alone hits limits

`**MultiProvider**` is unwrapped into a nested tree, and a provider can only look upward. If two modules depend on each other, you have to interleave their dependencies one by one in the right order. Provider also always needs a `**BuildContext**`. Before `**runApp**`, in push handlers or in isolates, you can't reach your objects.

A service locator fixes both: register all factories first, then resolve, no widget tree needed. Resetting is easier too. With Provider it goes through keys and can, in the worst case, throw away form state.

### Riverpod has different friction

To break cycles between packages, Riverpod needs a fake provider that throws and gets overridden later. That kills the promise that every dependency can always be resolved. And the automatic lifecycle is convenient but hard to predict: if a consumer lingers somewhere, the dependency stays alive. That is the speaker's view. I don't use Riverpod myself, so I'll hold back on judging it. My take on state management is in [**Flutter State Management 2026**](https://robocitrus.com/en/blog/flutter-state-management-2026).

### Lindia: Provider feel, locator power

As an answer, the speaker presented a new open-source package from his team, mainly built by his colleague Tomek Koter. My notes spell it "Lindia". I'm not fully sure about the spelling, so double-check before you search for it.

The idea: Provider injects a locator instance into the tree. The API is about 90 percent like Provider, so migrating should be easy. Dependency order doesn't matter, because everything is registered first and resolved afterwards. [**Bloc**](https://bloclibrary.dev) can still be observed with `**context.watch**`. The core is pure Dart, so it also runs before `**runApp**`, in isolates and in notification callbacks. Modules are built in, with a debug label showing who registered what.

My favorite is the shared scope with a key. All widgets with the same key get the same instances, and when the last consumer disappears, reference counting cleans up. It is meant for flows across several screens without a common parent widget, like a three-step transfer.

The speaker said himself that the package is fresh and asked for feedback. I haven't tried it, so I can't say how it holds up in practice.

## Part 3: Rules nobody can ignore

The third talk came from Piotr, a senior Flutter developer. He starts where documentation stops. Architecture rules live in Confluence, ADRs or Markdown. That matters, but someone has to read it, and it doesn't stop a mistake at the moment it happens.

His idea is called "executable architecture": rules that get enforced automatically and that humans, CI and AI agents all understand the same way. Technically that's a lint. Deterministic, always the same, in the IDE, the CLI and CI.

### Examples from a banking app

The project had about 220,000 lines of code and roughly 100 packages. The team shrank from eight developers to three. That's exactly where automation paid off.

One lint forced all golden tests through a custom wrapper, at first just for consistency. Then in April 2023 a CI upgrade came, because Apple required newer tooling. Suddenly every golden test failed, at 0.00 percent difference, because font rendering had changed slightly. Since all tests used the same wrapper, one line in the pixel comparator fixed it. Without the rule, the change would have been needed in about 100 packages.

Another lint makes sure analytics IDs on buttons match the base ID of their page. The app runs fine without it, but the business analysis becomes worthless. Here the lint enforces a business decision.

A third one covers DI. If you register `**ProductionObjectService**` and later look up `**ObjectService**`, type inference gives you a "not found" at runtime. The lint requires the explicit type on registration. One line changed, one production crash fewer.

Add design system rules: own components instead of Material `**Text**` and `**Colors**`, plus limits on which widgets may go inside a component at all. That keeps the design system from turning into a pile of special cases.

The teaching example was a lint called "No API client in Cubits". A cubit must not import an API client and has to go through a repository. The code is about 20 lines: a rule with an error code and severity, plus a check of the imports in the cubit layer.

### The tooling switch

Since 2022, `**custom_lint**` was the de facto standard. Since 2025, the Dart SDK ships an official `**analyzer_plugin**` system, and `**custom_lint**` is no longer actively developed. If you still rely on it, plan the migration.

### Too late is too late

This is the part I'll remember. A second project only turned its lints on after years. The result was hundreds of warnings at once, the team was overwhelmed and fell back to manual reviews. So start early. With legacy code, go step by step, package by package or rule by rule.

He named the downsides too. Lints encode decisions, and if the team disagrees, the rule gets ignored everywhere. False positives destroy trust. Lints are code and need maintenance and tests. Plugins slow analysis down, two to three times in his test. And a lint can only enforce standards that already exist.

### What about AI?

AI agents are probabilistic. The more rules you put into prompts, the more likely some get dropped. A lint works like a reward function instead: the agent writes code, the analysis gives deterministic feedback, the agent fixes it until nothing is reported.

Two tips: write error messages that say what is wrong and what to do instead, in two sentences. And offer quick fixes an agent can apply directly. More on this interplay in [**Flutter with AI agents: skills, hooks and quality gates**](https://robocitrus.com/en/blog/flutter-ki-agenten-skills-hooks-quality-gates).

## What I take from this

This is my own opinion, not the speakers'.

I think the core idea scales down. I don't have 800 packages and don't want them. But "a feature knows no other feature" and a single entry point per feature appeal to me even in a mid-sized app. It saves my head, and context size when working with AI.

I'd introduce lints earlier, not when it starts to hurt. A handful of rules for the things that actually go wrong for me costs an afternoon. Cleaning up later costs weeks. For an MVP I still won't build a package landscape, I wrote about that in [**Why a first app without perfect architecture beats no app**](https://robocitrus.com/en/blog/warum-erste-app-ohne-perfekte-architektur-besser-als-keine-app). I keep the layer boundaries clean from day one, though.

A monorepo with hundreds of packages solves the problems of 100 developers. For one person it is overhead. Take the principles, not the package count.

I use Bloc and Cubit myself. A cubit not talking to the API client directly is a sensible rule to me. Enforcing it as a lint is the logical next step.

## Conclusion

Architecture rots through many small mistakes, not one big one. The three talks attack at three points: keep building blocks small and swappable, make their wiring controllable, and turn the ground rules into code.

What they share: decide what "correct" is first. Then automate it, ideally early. That holds for teams of 100 developers and for projects with one person.

---

## Frequently asked questions (FAQ)

Not necessarily. The example in the talk comes from an app with over 100 developers. For smaller projects, clear folders and layer boundaries are often enough. The principles matter: one entry point per feature, no dependencies between features, a clear direction of dependencies.

For small apps that is usually fine. According to the talk, trouble shows up with many modules, mutual dependencies, access without a BuildContext and complex scopes. That's when a service locator or a solution like the presented package becomes interesting.

A rule written as Dart code that analyzes your code and reports violations in the IDE, the CLI and CI. It lets you check project-specific things, like a cubit not importing an API client. Since 2025 there is the official `**analyzer_plugin**` system in the Dart SDK for this.

Early. The talk shows a project that only enabled lints after years and was buried under hundreds of warnings. For legacy code, a gradual rollout helps, package by package or rule by rule.

Yes. According to the talk they act as a kind of reward function for AI agents: the agent gets deterministic feedback and can fix its output. Clear error messages and quick fixes make that even more effective.

---

*This article was inspired by the talks on scalable Lego architecture, dependency injection in large Flutter apps and enforcing architecture with custom lints (speaker of the lint talk: Piotr) at FlutterCon 2026. For the roadmap and refactoring talks, see the [**separate article**](https://robocitrus.com/en/blog/flutter-roadmap-2026-und-grosse-refactorings).*

[**How to Publish a Flutter App: Step by Step to the App Store** Publish your Flutter app with confidence: From preparation to Google Play release to Apple App Store. Complete guide with code examples, common mistakes, and practical tips.](https://robocitrus.com/en/blog/flutter-app-veroeffentlichen) [**Flutter beyond the phone: embedded hardware and Kotlin Multiplatform in practice** A Flutter UI on a golf launch monitor with no GPU, and a 3D flight simulator that only got smooth with Kotlin Multiplatform. Two real cases on where Flutter hits limits and what helps.](https://robocitrus.com/en/blog/flutter-auf-embedded-hardware-und-kotlin-multiplatform)

## **Let's Connect! 🚀**

### **Ready for your next project? **<br>** Let's get started!**

![Maximilian Flechtner - Gründer](https://robocitrus.com/cdn-cgi/image/w=3840,f=png,q=75/images/about-me-maximilian-image.webp)

#### **Maximilian Flechtner **

[flechtner@robocitrus.com](mailto:flechtner@robocitrus.com) [ +49 0176 41766223](tel:+4917641766223)

### **Let's get started**

Tell me about your vision – just drop me an email or use the contact form. Let's plan your digital success together! 💪

[Send me an email](mailto:flechtner@robocitrus.com) [Contact Form](https://robocitrus.com/en/kontakt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://robocitrus.com/en#website","@type":"WebSite","description":"App Entwickler & Full-Stack Experte aus Deutschland. Ich baue skalierbare Apps, Backends und komplette Startup-Systeme mit Flutter, Go und Kubernetes.","inLanguage":"en-US","name":"Maximilian Flechtner - App Entwickler","url":"https://robocitrus.com/en","publisher":{"@id":"https://robocitrus.com/#/schema/organization/1"},"translationOfWork":{"@id":"https://robocitrus.com/#website"}},{"@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln#webpage","@type":"WebPage","dateModified":"2026-10-21T00:00:00.000Z","datePublished":"2026-10-21","description":"How does a Flutter app stay maintainable for years? Three FlutterCon 2026 talks: Lego architecture with 800 packages, dependency injection at enterprise scale, and enforcing architecture with custom lints.","name":"Keeping Flutter architecture clean long term: modules, DI and custom lints","url":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln","breadcrumb":{"@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln#breadcrumb"},"isPartOf":{"@id":"https://robocitrus.com/en#website"},"potentialAction":[{"@type":"ReadAction","target":["https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln"]}],"primaryImageOfPage":{"@id":"https://robocitrus.com/#logo"}},{"@id":"https://robocitrus.com/#identity","@type":"Organization","alternateName":"Robocitrus","description":"App Entwickler & Full-Stack Experte aus Deutschland. Spezialisiert auf skalierbare Apps, Backend-Systeme und komplette Startup-Lösungen mit Flutter, GoLang und Kubernetes. Von der Idee bis zum Launch - alles aus einer Hand.","email":"flechtner@robocitrus.com","name":"Maximilian Flechtner - App Entwicklung","telephone":"0176 41766223","url":"https://robocitrus.com","address":{"@type":"PostalAddress","streetAddress":"Leppiner Dorfstraße 60","addressLocality":"Arendsee","addressRegion":"Sachsen-Anhalt","postalCode":"39619","addressCountry":"DE"},"contactPoint":{"@type":"ContactPoint","telephone":"0176 41766223","email":"flechtner@robocitrus.com"},"logo":{"@id":"https://robocitrus.com/#logo"}},{"@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln#article","dateModified":"2026-10-21T00:00:00.000Z","datePublished":"2026-10-21","description":"How does a Flutter app stay maintainable for years? Three FlutterCon 2026 talks: Lego architecture with 800 packages, dependency injection at enterprise scale, and enforcing architecture with custom lints.","headline":"Flutter architecture that doesn't rot: modules, dependency injection and lint rules","inLanguage":"en-US","thumbnailUrl":"https://robocitrus.com/images/blog/flutter-architektur-module-di-und-lint-regeln.webp","wordCount":200,"@type":["Article","BlogPosting"],"author":{"@id":"https://robocitrus.com/#/schema/person/1"},"image":{"@id":"https://robocitrus.com/#/schema/image/1"},"isPartOf":{"@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln#webpage"},"keywords":["Flutter","Dart","Architecture","Dependency Injection","App-Development"],"mainEntityOfPage":{"@type":"WebPage","@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln"},"publisher":{"@id":"https://robocitrus.com/#/schema/organization/1"}},{"@id":"https://robocitrus.com/en/blog/flutter-architektur-module-di-und-lint-regeln#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://robocitrus.com/blog","position":1},{"@type":"ListItem","name":"Flutter architecture that doesn't rot: modules, dependency injection and lint rules","position":2}]},{"@id":"https://robocitrus.com/#/schema/person/1","@type":"Person","name":"Maximilian Flechtner","url":"https://robocitrus.com/about","image":{"@id":"https://robocitrus.com/#/schema/image/2"}},{"@id":"https://robocitrus.com/#/schema/organization/1","@type":"Organization","name":"Maximilian Flechtner - App Entwicklung","url":"https://robocitrus.com/","logo":{"@type":"ImageObject","url":"https://robocitrus.com/images/large-logo.webp"}},{"@id":"https://robocitrus.com/#logo","@type":"ImageObject","caption":"Maximilian Flechtner - App Entwicklung","contentUrl":"https://robocitrus.com/images/large-logo.webp","inLanguage":"en-US","url":"https://robocitrus.com/images/large-logo.webp"},{"@id":"https://robocitrus.com/#/schema/image/1","@type":"ImageObject","contentUrl":"https://robocitrus.com/images/blog/flutter-architektur-module-di-und-lint-regeln.webp","inLanguage":"en-US","url":"https://robocitrus.com/images/blog/flutter-architektur-module-di-und-lint-regeln.webp"},{"@id":"https://robocitrus.com/#/schema/image/2","@type":"ImageObject","contentUrl":"https://robocitrus.com/images/about-me-maximilian-image.webp","inLanguage":"en-US","url":"https://robocitrus.com/images/about-me-maximilian-image.webp"}]}
```

## Sitemap

See the full [sitemap](https://robocitrus.com/sitemap.md) for all pages.
