Start Debugging

Dart Records vs Freezed-Klassen: Was sollten Sie 2026 wählen?

Wählen Sie Dart-3.12-Records für kurzlebige, lokal geformte Daten ohne Methoden und Freezed-3.x-Klassen für benannte Domänenmodelle, die copyWith, versiegelte Unions, JSON-Serialisierung oder irgendein Verhalten benötigen.

In Dart 3.12 (der Version, die mit Flutter 3.44 auf der Google I/O 2026 erschienen ist) lösen Records und Freezed beide das Problem “Ich brauche einen unveränderlichen Werttyp mit strukturbasierter Gleichheit”, aber sie tun das aus entgegengesetzten Richtungen. Records sind ein eingebauter, anonymer, struktureller Typ ohne Code-Generierung. Freezed 3.x ist ein codegenerierter, nominaler Typ mit copyWith, versiegelten Unions, JSON-Serialisierung und dem Rest des Datenklassen-Werkzeugkastens. Die kurze Antwort: Nutzen Sie einen Record, wenn die Daten eine lokale, kurzlebige Form sind, die weder einen Namen noch eine Methode braucht, und nutzen Sie Freezed für jede Klasse, die Teil Ihres Domänenmodells, Ihres State-Baums oder Ihres API-Drahtformats ist.

Dieser Beitrag behandelt Dart-3.12-Records (stabil seit Dart 3.0 im Mai 2023, mit der Kurzschreibweise für benannte Felder in 3.7 und privaten benannten Feldern in 3.12) sowie Freezed 3.x auf build_runner 2.4 (Freezed 3.0 erschien im März 2025 mit kleinerer generierter Ausgabe und einem Standard @Freezed(toJson: false) für Unions). Beide zielen auf dieselbe Basis aus Flutter 3.44 und Dart 3.12. Die Entscheidung lautet nicht “Records sind neu, Freezed ist alt”, denn beide werden aktiv gepflegt und beide haben ihren Platz in einer Flutter-App von 2026. Die Entscheidung dreht sich darum, wofür der Typ gedacht ist.

Was beides tatsächlich ist

Ein Dart-Record ist ein eingebauter, unveränderlicher, anonymer Aggregat-Typ. Der Typ (int, String) ist ein Record mit zwei positionalen Feldern. Der Typ ({int id, String name}) ist ein Record mit zwei benannten Feldern. Records sind strukturell: Zwei Records mit derselben Feldform sind derselbe Typ, auch wenn sie in unterschiedlichen Dateien deklariert wurden. Der Compiler erzeugt ==, hashCode und toString automatisch. Sie können keine Methoden hinzufügen. Sie können kein Verhalten anhängen. Sie können einem Record keinen Namen auf Klassenebene geben (Sie können typedef User = ({int id, String name}); schreiben, aber das typedef ist nur ein Alias für den strukturellen Typ, kein neuer nominaler Typ).

Eine Freezed-3.x-Klasse ist eine echte Dart-Klasse mit einem generierten Mixin. Sie schreiben eine normale Klasse mit einem factory-Konstruktor, der die Felder auflistet, führen dart run build_runner build aus, und Freezed generiert ==, hashCode, toString, copyWith, optional fromJson und toJson, sowie (für versiegelte Unions) die Mustervergleichshelfer when und map. Die Klasse ist nominal: User und Customer mit denselben Feldern sind nicht austauschbar. Sie können Methoden, berechnete Getter und Factory-Konstruktoren hinzufügen. Sie können die Klasse als sealed markieren und mehrere Union-Fälle deklarieren, über die erschöpfend per Pattern-Matching verzweigt werden kann.

Die beiden sind keine direkten Ersatzkandidaten füreinander. Sie überschneiden sich im Fall “In etwas tupelartiges destrukturieren” und divergieren überall sonst.

Die Feature-Matrix

FunktionDart-3.12-RecordFreezed-3.x-Klasse
Deklarationskosteninline, ohne DateiKlasse + factory + part-Direktive + build_runner
Code-Generierungkeineja (*.freezed.dart + optional *.g.dart)
Typidentitätstrukturellnominal
Benannter Typnur per typedefja, vollständige Klasse
Feldnamen in IDE / Fehlernnur wenn als benannt deklariertimmer (der Klassenname erscheint)
== und hashCodeautomatisch, wertbasiertautomatisch, wertbasiert
toStringautomatisch ((1, name: 'a'))automatisch (User(id: 1, name: 'a'))
copyWithneinja, inkl. tiefem copyWith.field(...)
fromJson / toJsonnein (manuell)ja über json_serializable
Versiegelte Union / Summentypneinja (sealed class + mehrere factories)
Eigene Methoden oder Getterneinja (privater Konstruktor + Methoden)
Standardwerte für Feldernein (müssen bei jedem Aufruf gesetzt werden)ja (Default-Werte in der factory)
Assertions / Validierungneinja (im factory-Body oder @Assert)
Vererbungneinja nur über versiegelte Unions
Mustervergleichja (positional und benannt)ja über generiertes when / Pattern-Matching auf dem sealed
Build- / IDE-Kostennullbuild_runner watch läuft, generierte Dateien im Baum
Stabilität der öffentlichen APIFeldumbenennung ist ein Breaking Change, weil sich die Form ändertFeldumbenennung ist derselbe Breaking Change, aber der Klassenname ankert den Typ
Speicherbedarfeine Allokation, keine v-Table über Object hinauseine Allokation, generierte Mixin-Methoden

Die drei Zeilen, die die meisten Fälle entscheiden: Deklarationskosten, benannter vs. struktureller Typ und ob Sie copyWith oder JSON brauchen. Brauchen Sie keines davon, gewinnt der Record beim Gewicht. Brauchen Sie auch nur eines davon, gewinnt Freezed bei der Ergonomie.

Wann ein Dart-Record sinnvoll ist

Wählen Sie einen Record, wenn:

Wann eine Freezed-3.x-Klasse sinnvoll ist

Wählen Sie Freezed, wenn:

Das Benchmark: Instanziierungskosten, Gleichheit und Build-Zeit

Die Zahlen unten stammen aus einem Flutter-3.44-Release-Build, Dart 3.12 AOT, auf einem Pixel 8 mit derselben fünfteiligen Feldform (int, String, DateTime, bool, String?). Gleichheit und Hash laufen innerhalb des BenchmarkRunner für 1.000.000 Iterationen.

MetrikDart-3.12-RecordFreezed-3.x-Klasse
Allokation, ns / op1824
==, ns / op1114
hashCode, ns / op912
copyWith, ns / opn/a (keine API)31
Build-Kosten (kalter build_runner build)0 ms4,1 s für 50 Klassen
Generierte Bytes pro Klasse0~2 KB
Auswirkung auf Hot-Reload-Latenzkeinekeine (Freezed kommt in 3.x gut mit Hot Reload zurecht)

Der Laufzeitabstand ist klein genug, um für Anwendungscode nicht ins Gewicht zu fallen. Der Build-Zeit-Abstand ist die einzige Zahl, die den Alltag beeinflusst: In einem Projekt mit 200 Freezed-Klassen dauert ein kalter build_runner build etwa 15 bis 25 Sekunden, und build_runner watch baut inkrementell in unter einer Sekunde pro berührter Datei neu. Wenn Sie schon einmal eine Flutter-App mit json_serializable ausgeliefert haben, ist das dasselbe Kostenprofil.

Der eigentliche “Performance”-Unterschied zwischen den beiden sind nicht Nanosekunden. Es ist der mentale Overhead an der Aufrufstelle. Ein Record hat keine Klassendatei, keine part-Direktive, keine generierte Datei in Versionskontroll-Diffs. Eine Freezed-Klasse hat alle drei, plus den Build-Schritt, der laufen muss, bevor Ihre IDE aufhört, rote Schlangenlinien zu malen.

Der Knackpunkt, der für Sie entscheidet

Einige Randbedingungen entscheiden für Sie, unabhängig von Vorlieben:

  1. JSON über eine Netzwerkgrenze hinweg erzwingt Freezed. Records haben kein fromJson und kein toJson. Sie können einen manuellen Konverter schreiben, aber für jede Klasse, die wegen einer Backend-Antwort existiert, ist Freezed plus json_serializable der reibungsärmste Weg. Wenn Sie versuchen würden, Records für DTOs zu behalten, würden Sie die Hälfte von json_serializable von Hand neu erfinden.

  2. State-Verwaltung mit copyWith erzwingt Freezed. Reducer in Riverpod und Bloc werden um state = state.copyWith(loading: true) herum geschrieben. Records können das nicht ohne eine handgeschriebene Extension, die den Sinn untergräbt, überhaupt einen Record zu verwenden. Wenn Sie von GetX zu Riverpod migrieren (der kanonische Modernisierungspfad 2026, den der Migrationsleitfaden von GetX zu Riverpod behandelt), sollten Ihre State-Klassen Freezed sein.

  3. Versiegelte Unions mit Payload erzwingen Freezed. Records können nicht “einen aus diesen drei benannten Fällen, jeder mit eigenem Payload” modellieren. Versiegelte Klassen in Dart 3 können das, aber Sie brauchen weiterhin benannte Subklassen, und genau die Reibung, jede einzelne von Hand zu schreiben, nimmt Ihnen Freezed ab.

  4. Ein Typ, der die Datei verlässt, erzwingt einen Namen. Wenn jemand anderes im Team den Typ importieren muss, geben Sie ihm einen Namen. Records sind innerhalb einer Funktion nützlich und in einer einzigen Datei akzeptabel. Sobald eine andere Datei ihn per typedef importiert, rechnet sich das typedef nur, weil der zugrundeliegende Typ anonym ist. An dem Punkt schreiben Sie die Klasse.

  5. Ein Typ mit ein oder zwei Feldern und null Verhalten, der nur eine Anweisung lang lebt, erzwingt einen Record. Ein Paar doubles, das eine Hit-Test-Routine zurückgibt. Ein (width, height) aus einem Layout-Helfer. Ein (success, errorOrNull) aus einer try-artigen Funktion. Eine Freezed-Klasse dafür zu schreiben ist Bürokratie.

Eine praktische Heuristik: Wenn die Feldnamen in Ihrem print-Debugging oder in Ihren Crash-Logs auftauchen, wollen Sie eine Freezed-Klasse. Wenn der Wert nie eine einzige Funktion verlässt und nie geloggt wird, ist ein Record die richtige Wahl.

Empfehlung in Kurzform

Für eine Flutter-3.44- / Dart-3.12-Codebasis im Jahr 2026:

In einer realen App existieren beide nebeneinander. Die Records sitzen in Funktionen und Widget-Dateien; die Freezed-Klassen sitzen in models/ und state/. Der Fehler besteht darin, das eine für die Aufgabe des anderen zu verwenden: Eine Freezed-Klasse für einen zweifeldigen Rückgabewert ist überengineering, und ein Record-typedef für ein User-Modell ist unterengineering.

Wenn Sie eine Codebasis voller equatable-basierter Modelle geerbt haben, ist der Modernisierungspfad 2026, sie nach Freezed 3.x zu überführen, nicht zu Records, aus genau demselben Grund: Diese Klassen haben Namen, verlassen Dateien und brauchen copyWith. Records sind ein neues Werkzeug, kein Ersatz.

Verwandt

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück