---
title: "Flutter-Architektur, die nicht verrottet: Module, Dependency Injection und Lint-Regeln"
description: "Wie bleibt eine Flutter-App auch nach Jahren wartbar? Drei Talks von der FlutterCon 2026: Lego-Architektur mit 800 Paketen, Dependency Injection im Enterprise und Architektur per Custom Lint erzwingen."
canonical_url: "https://robocitrus.com/blog/flutter-architektur-module-di-und-lint-regeln"
---

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

## Architektur verrottet leise

Keine App startet mit schlechter Architektur. Sie wird schlecht, weil über Monate hundert kleine Abkürzungen dazukommen. Ein API-Client im falschen Layer. Ein Feature, das schnell ins andere greift. Ein Widget, das an der Design-Vorgabe vorbei gebaut wurde.

Auf der FlutterCon 2026 habe ich drei Talks gehört, die dieses Problem von drei Seiten angehen. Der erste zerlegt eine Codebase in kleine, austauschbare Bausteine. Der zweite verdrahtet diese Bausteine per Dependency Injection (DI). Der dritte schreibt die Regeln so fest, dass man sie nicht mehr umgehen kann.

Alle drei kamen aus Enterprise-Projekten, zwei davon mit Banking-Apps. Ich baue Apps meist alleine oder in kleinen Teams. Trotzdem steckt einiges drin, das ich auch bei mir nutze.

## Teil 1: Die Lego-Architektur

Der erste Talk beschrieb eine Finanz-Plattform für kleine und mittlere Unternehmen. Die Native-Apps wurden dort in knapp einem Jahr auf Flutter umgestellt. Heute hat die App über sechs Millionen Nutzer und Beiträge von über 100 Mobile-Entwicklern.

Die Kernidee: kleine Bausteine mit einheitlicher Steckverbindung, wie bei Lego. Jeder Stein ist einfach, und trotzdem lässt sich fast alles daraus bauen.

### Fast alles ist ein Paket

Das Team hat das konsequent durchgezogen. Über 800 Dart- und Flutter-Pakete, jedes mit genau einer Aufgabe. Trotzdem liegt alles in einem Monorepo. Der Speaker nannte dafür vier Gründe: Alle sehen den ganzen Code. Eine Änderung über mehrere Pakete geht in einem Pull Request durch, ohne Versionen abzustimmen. Alle Pakete nutzen dieselben Versionen externer Abhängigkeiten, was Upgrades (auch Flutter-Upgrades) einfacher macht. Und weil alles an einem Ort liegt, lassen sich Abhängigkeitsrichtung und Paketgrenzen automatisch prüfen.

Dazu kommen Effekte, die man bei so vielen Paketen kaum erwartet. Weniger Merge-Konflikte, weil selten zwei Leute am selben Stück arbeiten. Schnellere Tests und Code-Generierung. In der CI laufen pro Pull Request nur geänderte Pakete und ihre Abhängigen. Und jedes Paket hat einen natürlichen Code-Owner.

### Drei Schichten, harte Regeln

Unten liegen Core und Utilities: Helfer, Wrapper um Bibliotheken, Session, Caching, Validierung, geteilte Modelle, Design System. In der Mitte die Features, etwa "Rechnung erstellen" oder "Zahlung ausführen". Oben die Anwendung, die Core und Features zu einer App zusammensteckt.

Die Regeln dazu: keine zirkulären Abhängigkeiten, nirgends. Core kennt nur Core. Features kennen keine anderen Features. Sonst hängt irgendwann jedes Feature an jedem anderen, und du kannst keine App mehr aus einer Auswahl von Features bauen.

### Ein Feature von innen

Jedes Feature hat dieselbe Struktur: Domain (Modelle, APIs, Repositories, Use Cases), Presentation, Konfiguration, DI und Navigation. Alles bleibt privat. Nach außen gibt es genau einen Eingang, ein Flow-Widget.

Das Flow-Widget hat keine eigene UI. Es wählt anhand des Zustands die passende Seite. Beim Erzeugen initialisiert es die DI-Registrierungen und die Lokalisierung des Features. So werden beim App-Start nicht hunderte Features hochgefahren, sondern nur das, was der Nutzer gerade öffnet.

Die Navigation fand ich clever gelöst. Innerhalb eines Features läuft sie deklarativ. Zwischen Features läuft sie imperativ, über ein abstraktes Interface, das jedes Feature selbst deklariert. Die echte Umsetzung steckt im Anwendungs-Layer, der als Einziger alle Features kennt. Mit der Konfiguration ist es genauso: Das Feature fragt einen abstrakten Configurator, der Anwendungs-Layer liefert die Werte aus Build-Flavors und Remote-Flags.

Einiges weicht bewusst von Clean Architecture ab. Keine zusätzlichen Abstraktionen ohne Not, denn in Dart kann jede Klasse als Interface dienen. Ein Modellsatz für Daten und Domain, mit JSON-Convertern bei Abweichungen vom Backend. Pragmatismus vor Lehrbuch.

### Testen mit echten Abhängigkeiten

Das hat mich am meisten überzeugt. Wer jede Abhängigkeit mockt, testet am Ende die Implementierung. Das Team beschreibt stattdessen Anforderungen am öffentlichen Einstieg eines Pakets und lässt alles innerhalb des Pakets echt laufen. Gemockt wird nur, was von außen kommt. Backend-Antworten kommen als JSON-String über einen Dio-Interceptor, damit die Deserialisierung mitgetestet wird.

Laut Talk heißt das weniger Tests, aber über 90 Prozent Abdeckung. Und Tests, die beim internen Umbau nicht brechen.

## Teil 2: Dependency Injection, wenn die App groß wird

Der zweite Talk drehte sich um die Frage, wie man Module wieder zusammenbringt. Constructor Injection, Testbarkeit und Lifecycle-Kontrolle kennst du. In großen Apps kommen zwei Probleme dazu.

Das erste ist das Verdrahten der Module: Das Auth-Modul braucht einen API-Client aus dem Netzwerk-Modul, wer stellt ihn bereit? Das zweite sind komplexe Scopes. Ein Scope ist eine Gruppe von Abhängigkeiten mit gleichem Lebenszyklus. Eine Banking-App hat einen globalen Scope, einen für den eingeloggten Nutzer, einen pro Kontext (privat oder Firma) und einen pro Prozess wie "Überweisung". Beim Logout muss alles Nutzerbezogene weg.

Der Speaker hat sieben Projekte seines Teams ausgewertet. Es tauchten immer dieselben Werkzeuge auf: Provider, ein Service Locator ([GetIt](https://pub.dev/packages/get_it) oder Eigenbau) und [Riverpod](https://pub.dev/packages/riverpod).

### Provider allein stößt an Grenzen

`MultiProvider` wird intern zu einem verschachtelten Baum, und ein Provider kann nur nach oben schauen. Hängen zwei Module gegenseitig voneinander ab, musst du ihre Abhängigkeiten einzeln in der richtigen Reihenfolge einstreuen. Außerdem braucht Provider immer einen `BuildContext`. Vor `runApp`, in Push-Handlern oder in Isolates kommst du damit nicht an deine Objekte.

Ein Service Locator löst beides: erst alle Factories registrieren, dann auflösen, ohne Widget-Baum. Auch Zurücksetzen geht einfacher. Bei Provider läuft das über Keys und kann im schlimmsten Fall einen Formularzustand wegwerfen.

### Riverpod hat andere Reibung

Um Zyklen zwischen Paketen zu brechen, braucht man bei Riverpod einen Fake-Provider, der einen Fehler wirft und später überschrieben wird. Damit geht das Versprechen verloren, dass sich jede Abhängigkeit immer auflösen lässt. Und der automatische Lifecycle ist bequem, aber schwer vorhersagbar: Bleibt irgendwo ein Konsument hängen, lebt die Abhängigkeit weiter. Das ist die Sicht des Speakers. Ich nutze Riverpod selbst nicht und halte mich mit einem Urteil zurück. Meinen Blick auf Zustandsverwaltung findest du in [Flutter State Management 2026](https://robocitrus.com/blog/flutter-state-management-2026).

### Lindia: Provider-Gefühl mit Locator-Kraft

Als Antwort stellte der Speaker ein neues Open-Source-Paket seines Teams vor, hauptsächlich entwickelt von seinem Kollegen Tomek Koter. In meinen Notizen steht es als "Lindia". Die Schreibweise ist nicht ganz sicher, prüf sie also, bevor du suchst.

Die Idee: Provider injiziert eine Locator-Instanz in den Baum. Die API ist zu rund 90 Prozent wie Provider, die Migration soll leicht fallen. Die Reihenfolge der Abhängigkeiten ist egal, weil erst alles registriert und dann aufgelöst wird. [Bloc](https://bloclibrary.dev) lässt sich weiter per `context.watch` beobachten. Der Kern ist reines Dart und läuft damit auch vor `runApp`, in Isolates und in Notification-Callbacks. Module sind eingebaut, samt Debug-Label, wer was registriert hat.

Mein Favorit ist der geteilte Scope mit Schlüssel. Alle Widgets mit demselben Key bekommen dieselben Instanzen, und wenn der letzte Konsument verschwindet, räumt die Referenzzählung auf. Das ist gedacht für Abläufe über mehrere Screens ohne gemeinsamen Eltern-Widget, etwa eine Überweisung in drei Schritten.

Der Speaker sagte selbst, dass das Paket frisch ist, und bat um Feedback. Ich habe es nicht ausprobiert und kann zur Praxistauglichkeit nichts sagen.

## Teil 3: Regeln, die man nicht ignorieren kann

Der dritte Talk kam von Piotr, einem Senior Flutter Developer. Er setzt dort an, wo Dokumentation aufhört. Architektur-Regeln stehen in Confluence, ADRs oder Markdown. Das ist wichtig, aber jemand muss es lesen, und es verhindert keinen Fehler in dem Moment, in dem er passiert.

Sein Vorschlag heißt "executable architecture": Regeln, die automatisch erzwungen werden und die Menschen, CI und KI-Agenten gleich verstehen. Technisch ist das ein Lint. Deterministisch, immer gleich, in der IDE, im CLI und in der CI.

### Beispiele aus einer Banking-App

Das Projekt hatte rund 220.000 Zeilen Code und etwa 100 Pakete. Das Team schrumpfte von acht auf drei Entwickler. Genau da zahlte sich Automatisierung aus.

Ein Lint zwang alle Golden Tests durch einen eigenen Wrapper, anfangs nur aus Konsistenz. Dann kam im April 2023 ein CI-Upgrade, weil Apple neuere Tooling-Versionen verlangte. Plötzlich schlugen alle Golden Tests fehl, bei 0,00 Prozent Unterschied, weil sich das Font-Rendering minimal geändert hatte. Weil alle Tests denselben Wrapper nutzten, reichte eine Zeile im Pixel-Comparator. Ohne die Regel wäre die Änderung in rund 100 Paketen nötig gewesen.

Ein anderer Lint stellt sicher, dass Analytics-IDs von Buttons zur Basis-ID ihrer Seite passen. Die App läuft auch ohne, aber die Business-Analyse wird wertlos. Der Lint erzwingt hier eine Geschäftsentscheidung.

Ein dritter betrifft DI. Wer `ProductionObjectService` registriert und später `ObjectService` abfragt, bekommt wegen Typ-Inferenz erst zur Laufzeit "nicht gefunden". Der Lint verlangt den expliziten Typ bei der Registrierung. Eine Zeile Änderung, ein Produktions-Crash weniger.

Dazu Design-System-Regeln: eigene Komponenten statt Material-`Text` und `Colors`, und Vorgaben, welche Widgets in eine Komponente überhaupt hineindürfen. So wuchert das Design System nicht zu einem Haufen Sonderfälle.

Das Lehrbeispiel war ein Lint "No API client in Cubits". Ein Cubit darf keinen API-Client importieren, sondern muss über ein Repository gehen. Der Code dafür umfasst rund 20 Zeilen: eine Regel mit Fehlercode und Schweregrad plus eine Prüfung der Imports in der Cubit-Schicht.

### Der Tooling-Wechsel

Seit 2022 war `custom_lint` der de-facto-Standard. Seit 2025 liefert das Dart SDK ein offizielles `analyzer_plugin`-System, `custom_lint` wird nicht mehr aktiv entwickelt. Wer noch darauf baut, sollte die Migration einplanen.

### Zu spät ist zu spät

Den Teil merke ich mir. Ein zweites Projekt schaltete seine Lints erst nach Jahren ein. Das Ergebnis waren hunderte Warnungen auf einen Schlag, das Team war überfordert und fiel in manuelle Reviews zurück. Also früh anfangen. Bei Altbestand geht es schrittweise, Paket für Paket oder Regel für Regel.

Die Nachteile nannte er auch. Lints kodieren Entscheidungen, und ist das Team nicht einverstanden, wird die Regel überall ignoriert. Falsche Positive zerstören das Vertrauen. Lints sind Code und brauchen Pflege und Tests. Plugins verlangsamen die Analyse, im Test auf das Zwei- bis Dreifache. Und ein Lint kann nur Standards durchsetzen, die es schon gibt.

### Und die KI?

KI-Agenten sind probabilistisch. Je mehr Regeln du in Prompts schreibst, desto eher fallen welche unter den Tisch. Ein Lint wirkt dagegen wie eine Belohnungsfunktion: Der Agent schreibt Code, die Analyse liefert deterministisches Feedback, der Agent bessert nach, bis nichts mehr gemeldet wird.

Zwei Tipps dazu: Schreib Fehlermeldungen so, dass sie sagen, was falsch ist und was stattdessen zu tun ist, in zwei Sätzen. Und biete Quick Fixes an, die ein Agent direkt anwenden kann. Mehr zu diesem Zusammenspiel steht in [Flutter mit KI-Agenten: Skills, Hooks und Quality Gates](https://robocitrus.com/blog/flutter-ki-agenten-skills-hooks-quality-gates).

## Was ich daraus mitnehme

Das ist meine Einschätzung, nicht die der Speaker.

Ich glaube, die Grundidee skaliert nach unten. Ich habe keine 800 Pakete und will sie auch nicht. Aber "Feature kennt kein anderes Feature" und ein einziger Einstiegspunkt pro Feature gefallen mir auch bei einer mittelgroßen App. Das schont den Kopf und die Kontextgröße beim Arbeiten mit KI.

Lints würde ich früher einführen, nicht erst wenn es weh tut. Eine Handvoll Regeln für die Dinge, die bei mir wirklich schiefgehen, kostet einen Nachmittag. Späteres Aufräumen kostet Wochen. Bei einem MVP baue ich trotzdem keine Paketlandschaft, dazu steht mehr in [Warum die erste App ohne perfekte Architektur besser ist als keine](https://robocitrus.com/blog/warum-erste-app-ohne-perfekte-architektur-besser-als-keine-app). Die Schichtgrenzen halte ich aber von Anfang an sauber.

Ein Monorepo mit hunderten Paketen löst Probleme von 100 Entwicklern. Für eine Person ist es Overhead. Nimm dir die Prinzipien, nicht die Paketzahl.

Ich nutze Bloc und Cubit selbst. Dass ein Cubit nicht direkt mit dem API-Client spricht, finde ich eine sinnvolle Regel. Sie als Lint zu erzwingen, ist der logische nächste Schritt.

## Fazit

Architektur verrottet durch viele kleine Fehler statt durch einen großen. Die drei Talks greifen an drei Stellen an: Bausteine klein und austauschbar halten, ihre Verdrahtung kontrollierbar machen und die Spielregeln in Code gießen.

Gemeinsam ist allen: Entscheide erst, was "richtig" ist. Dann automatisiere es, am besten früh. Das gilt für Teams mit 100 Entwicklern genauso wie für Projekte mit einer Person.

---

## Häufig gestellte Fragen (FAQ)

<accordion>
<accordion-item icon="i-lucide-circle-help" label="Brauche ich für meine App ein Monorepo mit vielen Paketen?">

Nicht unbedingt. Das Beispiel im Talk kommt von einer App mit über 100 Entwicklern. Für kleinere Projekte reichen oft klare Ordner und Schichtgrenzen. Wichtig sind die Prinzipien: ein Eingang pro Feature, keine Abhängigkeiten zwischen Features, eine klare Richtung der Abhängigkeiten.

</accordion-item>

<accordion-item icon="i-lucide-circle-help" label="Warum nicht einfach nur Provider oder nur Riverpod für DI?">

Für kleine Apps reicht das meist. Probleme zeigen sich laut Talk bei vielen Modulen, gegenseitigen Abhängigkeiten, Zugriff ohne BuildContext und komplexen Scopes. Dann wird ein Service Locator oder eine Lösung wie das vorgestellte Paket interessant.

</accordion-item>

<accordion-item icon="i-lucide-circle-help" label="Was ist ein Custom Lint in Dart?">

Eine Regel, die als Dart-Code deinen Code analysiert und Verstöße meldet, in der IDE, im CLI und in der CI. Damit kannst du projektspezifische Dinge prüfen, etwa dass ein Cubit keinen API-Client importiert. Seit 2025 gibt es dafür das offizielle `analyzer_plugin`-System im Dart SDK.

</accordion-item>

<accordion-item icon="i-lucide-circle-help" label="Ab wann lohnen sich eigene Lint-Regeln?">

Früh. Der Talk zeigt ein Projekt, das erst nach Jahren Lints aktiviert hat und von hunderten Warnungen erschlagen wurde. Bei einem Altbestand hilft ein schrittweises Vorgehen, Paket für Paket oder Regel für Regel.

</accordion-item>

<accordion-item icon="i-lucide-circle-help" label="Helfen Lints auch bei KI-generiertem Code?">

Ja. Laut Talk sind sie für KI-Agenten eine Art Belohnungsfunktion: Der Agent bekommt deterministisches Feedback und kann nachbessern. Klare Fehlermeldungen und Quick Fixes machen das noch wirksamer.

</accordion-item>
</accordion>

---

*Dieser Artikel wurde inspiriert von den Talks zu skalierbarer Lego-Architektur, Dependency Injection in großen Flutter-Apps und zu Architektur-Durchsetzung mit Custom Lints (Speaker: Piotr) auf der FlutterCon 2026. Zur Roadmap und zu Refactorings gibt es einen eigenen Artikel.*

## Sitemap

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