In Dart 3, how do you return both the minimum and maximum temperature from one function, and how does the caller read them?
answer
- no class, no List, no Map
- a record type as the return type
- named fields read like getters
- positional fields are $1, $2
- Dart 3.0 language feature
basics
~20 sDeclare a record return type such as ({double min, double max}) and return (min: lo, max: hi). The caller reads range.min and range.max, or $1 and $2 for positional fields, with each field keeping its static type.
solid answer
~40 sSince Dart 3.0 a function can return a **record**: an anonymous, immutable, fixed-size bundle of typed fields. Write the return type as `({double min, double max})` and return `(min: lo, max: hi)`. The caller gets one value and reads `range.min` and `range.max`, both statically typed `double`. With positional fields, `(double, double)`, the getters are `$1` and `$2` instead, which is fine for a private helper but less readable at a distance, so named fields suit anything public. Records replace the old workarounds: a `List<double>` loses which element is which and gives no per-position types, a `Map<String, Object?>` loses the types entirely, and a dedicated class is a whole declaration for two numbers. The caller can also destructure the result into two locals with a pattern.
code
dart · 18 lines({double min, double max}) temperatureRange(List<double> readings) {
if (readings.isEmpty) {
throw ArgumentError.value(readings, 'readings', 'must not be empty');
}
var min = readings.first;
var max = readings.first;
for (final r in readings.skip(1)) {
if (r < min) min = r;
if (r > max) max = r;
}
return (min: min, max: max);
}
void main() {
final range = temperatureRange([12.5, 9.5, 17.25]);
print('${range.min} to ${range.max}'); // 9.5 to 17.25
print(range.max - range.min); // 7.75
}go deeper
Be able to write a function that returns ({double min, double max}) and read the two values with .min and .max, or $1 and $2 for positional fields.
Explain why a record beats a List or Map for multiple returns: fixed shape, per-field static types, and no declaration, and when named fields beat positional ones.
Choose records for local, private multiple returns and recognise when a returned bundle has grown into something that deserves a named type or a class.
Set conventions for where records may appear in public APIs of shared packages, since their structural types make every same-shaped value interchangeable.
## The problem records solve A Dart function has exactly one return value. Before Dart 3.0, returning two things — here the lowest and highest reading from a list of temperatures — meant choosing a workaround: | Approach | What you lose | |---|---| | `List<double>` of two items | which index means what; a wrong length is not a compile error | | `Map<String, Object?>` | every value's static type; typos in keys compile | | a small class `TempRange` | nothing, but it is a full declaration for two numbers | | callbacks or mutable holders | readability, and they fight the language | A **record** is the language-level answer: an **anonymous, immutable, aggregate** value that bundles a fixed number of **typed fields** without declaring a class. ## Writing the function ```dart ({double min, double max}) temperatureRange(List<double> readings) { if (readings.isEmpty) { throw ArgumentError.value(readings, 'readings', 'must not be empty'); } var min = readings.first; var max = readings.first; for (final r in readings.skip(1)) { if (r < min) min = r; if (r > max) max = r; } return (min: min, max: max); } ``` Three pieces of syntax matter: - The **record type** `({double min, double max})` is the return type. Named fields sit inside curly braces within the parentheses, just like named parameters. - The **record expression** `(min: min, max: max)` builds the value, like named arguments in a call. - Record fields are **never optional**: every field in the type must be supplied, and there is no `required` keyword because none is needed. ## Reading the result The record exposes one **getter per field**: ```dart final range = temperatureRange([12.5, 9.5, 17.25]); print('${range.min} to ${range.max}'); // 9.5 to 17.25 ``` Each getter keeps its own static type, so `range.min.toStringAsFixed(1)` compiles with no cast. There are **no setters**: a record's fields cannot be reassigned. If you choose **positional** fields instead: ```dart (double, double) minMax(List<double> readings) { final sorted = [...readings]..sort(); return (sorted.first, sorted.last); } final r = minMax([12.5, 9.5, 17.25]); print(r.$1); // 9.5 print(r.$2); // 17.25 ``` Positional getters are named `$1`, `$2` and so on. They are fine for a short private helper, but at a call site several screens away `r.$1` says nothing about meaning — and a `(double, double)` accepts any two doubles in any order, so swapping them in the `return` compiles. Named fields make the type itself say `min` and `max`. The caller can also pull both fields into local variables in one declaration with a **record pattern**, `final (:min, :max) = temperatureRange(readings);` — destructuring is its own topic, but it is the idiom you will see most often next to multiple returns. ## Where it fits in a Flutter app - A widget's helper that computes a chart's axis range returns `({double min, double max})` and the `build` method reads the two fields. - A repository method returns `(List<Reading> items, bool hasMore)` for one page of results. - A list of simple same-shaped rows, such as button definitions with a label, an icon and a callback, can be a `List` of records without any class declaration. ## Common mistakes - **Returning a `List` and documenting the order.** The compiler cannot check that index 0 is the minimum; a record field named `min` can be checked. - **Swapping positional fields.** `return (hi, lo);` for a `(double, double)` compiles; named fields make the mistake visible. - **Expecting `[0]`-style access.** Records are not collections: there is no index operator, no `length` and no iteration over fields — only the getters. - **Trying to mutate the result.** There are no setters; build a new record with the changed field instead. ## Things to keep in mind 1. Records require **language version 3.0** or later; a package's default language version is the lower bound of its SDK constraint, so a constraint starting at 3.0 or above enables them. 2. A record is a **value**: you can store it in variables, lists, maps and sets, pass it to functions and nest records inside records. 3. When the same record type appears in several signatures, give it a name with a **typedef** so a change happens in one place. 4. When the bundle starts needing behaviour or validation, that is the signal to move to a class.
- Why prefer `({double min, double max})` over `(double, double)` for a public function?With positional fields the caller reads `$1` and `$2`, which carry no meaning, and any two doubles in either order satisfy the type, so swapping them compiles silently. Named fields put `min` and `max` into the type itself, the call site reads `range.min`, and returning `(min: hi, max: lo)` at least looks wrong in review.
- Can you add a field to a record value after it is created, or reassign one?No. A record has a fixed shape and exposes only getters, so there is no way to add, remove or reassign a field. To change one, build a new record, for example `(min: range.min, max: newMax)`.
saying these in an interview costs you the question
- Returns a List<double> and documents which index is the minimum
- Thinks a record field can be reassigned like a class field
- Believes records need a class or typedef declared first
- Says record fields lose their types and must be cast
- Claims positional record fields are read with [0] and [1]