Chuyển đến nội dung chính

Records and patterns in Dart: what they replace

Before records, returning two values from a function meant one of three unappealing options: a class you use once, a List<Object> you index into, or two out-parameters via a wrapper. Records make it one line.

({int width, int height}) measure(String text) {
  // ...
  return (width: 120, height: 40);
}

final size = measure('hello');
print(size.width);

That is the headline feature, and it is the smallest part of what records and patterns changed.

Records: structural, not nominal

A record’s type is its shape. (int, String) and (int, String) are the same type regardless of where they were created, which is what distinguishes a record from a class.

// Positional
(String, int) parseEntry(String line) {
  final parts = line.split(':');
  return (parts[0], int.parse(parts[1]));
}

// Named — clearer at the call site
({String name, int score}) parseNamed(String line) {
  final parts = line.split(':');
  return (name: parts[0], score: int.parse(parts[1]));
}

// Mixed
(String, {bool valid}) check(String input) => (input.trim(), valid: true);

Records are immutable and have structural equality, which is more useful than it first appears:

final a = (1, 'x');
final b = (1, 'x');
print(a == b); // true — no operator== to write

That makes them excellent as composite map keys:

final cache = <(int, int), Tile>{};
cache[(3, 7)] = tile;

Before records this required a key class with == and hashCode, or a string like '3,7'. Both work; neither is as good.

Patterns: destructuring

A pattern on the left of = takes a value apart:

final (name, score) = parseEntry(line);
final (:width, :height) = measure(text);   // named shorthand

// In a for loop over a map
for (final MapEntry(key: id, value: user) in users.entries) {
  print('$id${user.name}');
}

The :width shorthand binds a variable of the same name as the field, which is the form you will use most.

Patterns also work in if-case, which is the cleanest way to combine a type test with destructuring:

if (response case {'data': {'items': List<Map<String, Object?>> items}}) {
  return items.map(Item.fromJson).toList();
}

That single line checks that response is a map, that it has a data key holding a map, that the map has an items key, and that its value is a list of maps — binding items only if all of it holds. The alternative is five nested null-and-type checks.

This is the most practically useful pattern feature for anyone parsing JSON. It does not replace a real model class, but it makes the boundary code where you validate untyped data far shorter and considerably harder to get subtly wrong.

Switch expressions and exhaustiveness

Switch became an expression, which changes how you write mapping code:

String describe(Shape shape) => switch (shape) {
  Circle(radius: final r) when r > 100 => 'huge circle',
  Circle(radius: final r) => 'circle of radius $r',
  Rectangle(width: final w, height: final h) when w == h => 'square of $w',
  Rectangle() => 'rectangle',
};

Three things are happening: type test, destructuring, and a when guard, all in the case pattern.

The feature that makes this genuinely safer is exhaustiveness checking over sealed hierarchies:

sealed class Result<T> {}
final class Ok<T> extends Result<T> {
  const Ok(this.value);
  final T value;
}
final class Err<T> extends Result<T> {
  const Err(this.message);
  final String message;
}

String render(Result<int> r) => switch (r) {
  Ok(value: final v) => 'Got $v',
  Err(message: final m) => 'Failed: $m',
};

No default case. Add a third subclass and every switch over Result becomes a compile error, listing exactly which files need updating. That is a materially different experience from discovering the gap at runtime, and it is the strongest argument for modelling states as a sealed hierarchy rather than as a class with nullable fields.

Note the deliberate absence of a default: — adding one silences exhaustiveness checking and gives back the guarantee you came for.

Where records are the wrong tool

Records are anonymous, and anonymity has a cost:

  • They cannot have methods. (double lat, double lng) cannot carry distanceTo. A class can.
  • They cannot enforce invariants. No constructor means no validation. A record cannot guarantee that lat is within range.
  • The field names are the API. Rename width to w and every call site breaks with no deprecation path.
  • They are poor documentation. ({String, String}) at a public API boundary tells the reader nothing about which is which.

My rule: records for local plumbing, classes for concepts. A function returning “the parsed value and whether it was cached” is plumbing. A User is a concept. Anything crossing a package boundary or appearing in public API should be a named type.

Migration, incrementally

None of this requires rewriting anything. Three changes that pay off immediately in existing code:

  1. Replace Map<String, dynamic> returns from private helpers with records — same shape, actual type safety.
  2. Replace long if/else if type-check chains with a switch expression.
  3. Turn state classes with isLoading/error/data fields into sealed hierarchies, so impossible states stop compiling.

The third is the one that changes how code feels. A state class with three nullable fields has eight representable combinations and typically three legal ones; a sealed hierarchy has exactly the legal ones.

FAQ

Do records have runtime overhead?

They are objects, so there is an allocation, but they are lightweight and the compiler optimises common cases. This is not a reason to avoid them in ordinary code.

Can I use a record in a const context?

Yes, if all fields are constant: const point = (x: 1, y: 2);.

How do patterns interact with null safety?

Well. case final String s? matches a non-null string; case null matches null. Exhaustiveness accounts for nullability, so a switch over String? needs a null case or a catch-all.

Should sealed classes replace enums?

Only when the variants carry data. An enum with no payload is still simpler, and enums support exhaustive switches too.

Is when the same as a nested if?

Functionally yes, but the guard participates in the case, so ordering and readability improve. Note that a guard does not count towards exhaustiveness — the compiler cannot prove a guarded case always matches.


Record syntax, structural equality, pattern forms, and exhaustiveness rules for sealed hierarchies described here are documented in the Dart references linked above. The records-for-plumbing rule, the JSON-boundary recommendation, the migration list and the warning about default: defeating exhaustiveness are my own judgement from using these features in production code. Language features evolve — check the Dart SDK version your project targets.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

2026 👏 Google miễn phí một năm gói AI Plus cho sinh viên Việt Nam 🇻🇳🇻🇳🇻🇳

Chúc mừng anh em 👏 Google miễn phí một năm gói AI Plus cho sinh viên Việt Nam 🇻🇳🇻🇳🇻🇳 Đối tượng là sinh viên đại học từ 18–24 tuổi (bao gồm sinh viên mới và sinh viên từng dùng gói AI Pro 2025). Lưu ý là cần xác minh tư cách sinh viên hàng năm. Khi nhận gói, anh em có được các ưu đãi sau: 🔖 Nâng cấp bộ nhớ đám mây lên 400GB. 🔖 Nhân đôi hạn mức truy cập mô hình AI. 🔖 Mở quyền trải nghiệm tính năng tạo video bằng Gemini Omni. Lưu ý chương trình chỉ dành cho sinh viên đủ điều kiện. Hạn nhận ưu đãi: 31 tháng 12, 2026. Các bạn cần có phương thức thanh toán hợp lệ khi đăng ký. Google AI Plus sẽ tự động tính phí 132.000 ₫/tháng sau khi thời gian dùng thử kết thúc, trừ phi bạn đã huỷ trước đó. Huỷ bất cứ lúc nào. CÁC BƯỚC THỰC HIỆN CHI TIẾT 🔹Bước 1: Truy cập cổng đăng ký chương trình 🔖 Mở trình duyệt và truy cập vào trang chính thức: gemini.google/students/ 🔖 Đăng nhập vào Tài khoản Google cá nhân của bạn. 🔖 Nhấn vào nút "Claim your student plan at no cost" (hoặc Nhận gó...