On this page25 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is a Record?
- 2. Reading Positional Fields: $1, $2
- 3. Record Types
- 4. Named Fields
- 5. Mixing Positional and Named Fields
- 6. Returning Several Values From a Function
- 7. Records Are Immutable
- 8. Records Compare by Their Fields
- 9. Records in Lists and Maps
- 10. Naming a Record Type With typedef
- 11. Records vs Classes
- 12. A First Look at Destructuring
- 13. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 76 — Records
| Category | 12 — Generics & Advanced Types |
| Lecture | 076 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A function returns only one value, and pairing a name with a mark meant a
List<Object>, a map, or a whole class. A record bundles a few values of different types into one value, with no class to write.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.
Prerequisites
Students should already understand, from Lectures 031, 033, 039, 049, 053, 060 and 071:
- returning a value from a function, and named parameters with
{ } - lists and maps, including
List<int>andMap<String, int> - classes with fields and constructors
- overriding
operator ==andhashCodeto compare objects by value typedeffor giving a type a short name
Connection With Previous Lectures
Ali wants a function that gives back the lowest and the highest of his marks. A function returns one value, so in Lecture 031 we could only do this:
List<int> lowHigh(List<int> marks) {
var sorted = [...marks]..sort();
return [sorted.first, sorted.last];
}
void main() {
var result = lowHigh([80, 65, 92]);
print('Low ${result[0]}, high ${result[1]}');
}Low 65, high 92It works, but nothing says that index 0 is the low mark, and the list could hold three
values or none. Pair a name with a mark and it gets worse: ['Ali', 80] is a
List<Object> (Lecture 039). A class would fix both problems, but writing one just to carry
two values back from a function is a lot of ceremony. Dart has a lighter tool.
Learning Objectives
By the end of this lecture you will be able to:
- Create records with positional fields and with named fields
- Read fields with
$1,$2and with field names - Write record types such as
(String, int)and({String name, int marks}) - Return several values from one function
- Explain why records are immutable
- Compare records with
==and use them in sets, lists and maps - Name a record type with
typedef - Decide when a record is enough and when you need a class
1. What Is a Record?
A record is a single value that holds a fixed number of other values, called its fields. You write one by putting values in round brackets, separated by commas.
Analogy — a record is like a parcel. It may contain a book and a pen, but you hand it over, store it or return it as one thing.
void main() {
var student = ('Ali', 80);
print(student);
}(Ali, 80)student is one value with two fields: a String and an int.
2. Reading Positional Fields: $1, $2
Fields without a name are positional. Read them with $1 for the first, $2 for the
second, and so on:
void main() {
var student = ('Ali', 80);
print(student.$1);
print(student.$2 + 5);
}Ali
85Counting starts at 1, not 0 as with list indexes. Each field keeps its own type, so
student.$2 is an int and maths works on it directly.
3. Record Types
The type of a record lists its field types in round brackets. ('Ali', 80) has the type
(String, int):
void main() {
(String, int) student = ('Ali', 80);
print(student.runtimeType);
student = ('Sara', 92);
print(student);
}(String, int)
(Sara, 92)A new record can replace the old one, as long as it has the same shape: the same number of fields with the same types, in the same order. Swapping the order is a different type:
void main() {
// (String, int) student = (80, 'Ali'); // ✗
}Error: A value of type '(int, String)' can't be assigned to a variable of type '(String, int)'.4. Named Fields
$1 and $2 don't say what a value means. Like named parameters in Lecture 033, a field can
have a name. Write name: value inside the brackets, and read the field with .name:
void main() {
({String name, int marks}) student = (name: 'Sara', marks: 92);
print(student.name);
print(student.marks);
print(student);
}Sara
92
(marks: 92, name: Sara)In the type, named fields go inside curly braces: ({String name, int marks}). Their order
doesn't matter, which is why Dart printed marks first. Their names do matter: they are
part of the type.
void main() {
// ({String name, int marks}) s = (name: 'Ali', mark: 80); // ✗
}Error: A value of type '({int mark, String name})' can't be assigned to a variable of type '({int marks, String name})'.5. Mixing Positional and Named Fields
One record can have both kinds. Positional fields are still counted $1, $2, …, skipping
the named ones:
void main() {
var child = ('Zayan', 'Lahore', age: 6);
print(child.$1);
print(child.$2);
print(child.age);
print(child.runtimeType);
}Zayan
Lahore
6
(String, String, {int age})In practice, pick one style per record. Named fields read best once there are more than two.
6. Returning Several Values From a Function
Here is the job from the start of the lecture. The return type is a record type, and the
return builds a record:
({int low, int high}) lowHigh(List<int> marks) {
var sorted = [...marks]..sort();
return (low: sorted.first, high: sorted.last);
}
void main() {
var result = lowHigh([80, 65, 92]);
print('Low ${result.low}, high ${result.high}');
}Low 65, high 92The function's first line now tells the reader exactly what comes back: two ints called
low and high. No index to remember, and no class to write.
7. Records Are Immutable
Once a record is made, its fields can't change. There is no setter for $2 or .marks:
void main() {
var student = ('Ali', 80);
// student.$2 = 90; // ✗
student = (student.$1, 90); // ✓ build a new record
print(student);
}(Ali, 90)The commented line gives:
Error: The setter '$2' isn't defined for the type '(String, int)'.The variable can point to a new record (unless it is final), but each record itself
stays exactly as it was built. That makes records safe to pass around: no function can
quietly change one behind your back.
8. Records Compare by Their Fields
In Lecture 060 two Student objects with the same data were not equal until we
overrode operator == and hashCode. Records do that for you. Two records are equal when
they have the same shape and equal fields:
void main() {
print(('Ali', 80) == ('Ali', 80));
print((name: 'Ali', marks: 80) == (marks: 80, name: 'Ali'));
print(('Ali', 80) == (80, 'Ali'));
var seen = {('Ali', 80)};
seen.add(('Ali', 80));
print(seen.length);
}true
true
false
1Named fields match by name, so their written order doesn't matter. Positional fields match
by position. Because hashCode comes free as well, the set keeps only one copy.
9. Records in Lists and Maps
A record type works anywhere a type does, including inside < >:
void main() {
List<(String, int)> results = [('Ali', 80), ('Sara', 92), ('Zayan', 65)];
for (var r in results) {
print('${r.$1}: ${r.$2}');
}
Map<(int, int), String> seats = {(1, 1): 'Ali', (1, 2): 'Sara'};
print(seats[(1, 2)]);
}Ali: 80
Sara: 92
Zayan: 65
SaraThe map uses a record as its key: row and seat together. Lookup works because
(1, 2) == (1, 2), as section 8 showed.
10. Naming a Record Type With typedef
Writing ({String name, int marks}) over and over is long. Lecture 071's typedef names it
once:
typedef Student = ({String name, int marks});
Student topOf(List<Student> students) {
var best = students.first;
for (var s in students) {
if (s.marks > best.marks) best = s;
}
return best;
}
void main() {
List<Student> students = [
(name: 'Ali', marks: 80),
(name: 'Sara', marks: 92),
];
print(topOf(students).name);
}SaraStudent is only an alias, so any record with the fields name and marks fits. No
constructor is needed: the literal (name: 'Ali', marks: 80) is already a Student.
11. Records vs Classes
Both group values. They suit different jobs:
| Record | Class | |
|---|---|---|
| Needs a declaration? | No — just ( ) | Yes — class with a constructor |
| Fields can change? | Never | If you allow it |
== compares values? | Automatically | Only if you override it |
| Methods, getters, private fields? | No | Yes |
| Best for | A few values returned or passed together | A thing with behaviour and rules |
A record for Lecture 060's point needs no code at all:
void main() {
var a = (x: 1, y: 2);
var b = (x: 1, y: 2);
print(a == b);
print(a.x + b.y);
}true
3If the point needed a distanceTo method or a rule that x can't be negative, it would
become a class.
12. A First Look at Destructuring
Reading result.low and result.high one by one is fine. Dart can also unpack a record
into separate variables in one line:
(int, int) lowHigh(List<int> marks) {
var sorted = [...marks]..sort();
return (sorted.first, sorted.last);
}
void main() {
var (low, high) = lowHigh([80, 65, 92]);
print('$low $high');
}65 92This is destructuring, the subject of Lecture 078 — Destructuring. It is built on patterns, which begin in Lecture 077.
13. A Real-World Example
Sara's shop keeps cart lines as records and returns a summary as a record:
typedef Line = ({String item, double price, int qty});
({double total, int count}) summarize(List<Line> cart) {
var total = 0.0;
var count = 0;
for (var line in cart) {
total += line.price * line.qty;
count += line.qty;
}
return (total: total, count: count);
}
void main() {
List<Line> cart = [
(item: 'Notebook', price: 250.0, qty: 2),
(item: 'Pen', price: 40.0, qty: 5),
];
var summary = summarize(cart);
print('${summary.count} items, total ${summary.total}');
print(cart.first == (item: 'Notebook', price: 250.0, qty: 2));
}7 items, total 700.0
trueA typedef names the line shape, the function returns two values at once, and == checks
a line by its contents. No class was written.
Code Examples
Example 1 — A Positional Record
void main() {
var city = ('Lahore', 13);
print(city);
print(city.$1);
}(Lahore, 13)
LahoreExample 2 — A Named Record
void main() {
var reading = (city: 'Karachi', celsius: 31.5);
print('${reading.city}: ${reading.celsius}');
}Karachi: 31.5Example 3 — An Explicit Record Type
void main() {
(String, bool) task = ('Homework', false);
task = (task.$1, true);
print(task);
}(Homework, true)Example 4 — Three Fields
void main() {
var date = (2026, 10, 17);
print('${date.$3}/${date.$2}/${date.$1}');
}17/10/2026Example 5 — Equality
void main() {
var a = (name: 'Zayan', age: 6);
var b = (age: 6, name: 'Zayan');
print(a == b);
print(a == (name: 'Zayan', age: 7));
}true
falseExample 6 — Records as Map Keys
void main() {
var board = {(0, 0): 'X', (1, 1): 'O'};
print(board[(1, 1)]);
print(board[(2, 2)]);
}O
nullExample 7 — Student Marks
typedef Result = ({String name, int marks});
void main() {
List<Result> results = [
(name: 'Ali', marks: 80),
(name: 'Sara', marks: 92),
(name: 'Zayan', marks: 45),
];
for (var r in results) {
print('${r.name}: ${r.marks >= 50 ? 'Pass' : 'Fail'}');
}
}Ali: Pass
Sara: Pass
Zayan: FailExample 8 — Shopping Total With Tax
({double subtotal, double tax}) bill(double price, int qty) {
var subtotal = price * qty;
return (subtotal: subtotal, tax: subtotal * 0.1);
}
void main() {
var b = bill(500, 3);
print('Subtotal ${b.subtotal}, tax ${b.tax}');
}Subtotal 1500.0, tax 150.0Example 9 — Bank Balance
(bool, double) withdraw(double balance, double amount) {
if (amount > balance) return (false, balance);
return (true, balance - amount);
}
void main() {
var first = withdraw(1000, 300);
print(first);
var second = withdraw(first.$2, 900);
print(second);
}(true, 700.0)
(false, 700.0)The function reports two things at once: whether it worked, and the new balance.
Example 10 — Temperature Range
({double min, double max}) range(List<double> temps) {
var min = temps.first;
var max = temps.first;
for (var t in temps) {
if (t < min) min = t;
if (t > max) max = t;
}
return (min: min, max: max);
}
void main() {
var week = range([21.5, 30.0, 18.0, 27.5]);
print('Min ${week.min}, max ${week.max}');
}Min 18.0, max 30.0Common Beginner Mistakes
1. Counting fields from zero
void main() {
var s = ('Ali', 80);
// print(s.$0); // ✗
}Error: The getter '$0' isn't defined for the type '(String, int)'.Record fields start at $1.
2. Trying to change a field
void main() {
var s = ('Ali', 80);
// s.$2 = 90; // ✗
}Error: The setter '$2' isn't defined for the type '(String, int)'.Build a new record instead: s = (s.$1, 90);.
3. Using $1 on a named field
void main() {
var s = (name: 'Ali', marks: 80);
// print(s.$1); // ✗
}Error: The getter '$1' isn't defined for the type '({int marks, String name})'.Named fields are read only by name: s.name.
4. Forgetting the comma in a one-field record
void main() {
var s = (42);
// print(s.$1); // ✗
}Error: The getter '$1' isn't defined for the type 'int'.(42) is just 42 in brackets. A one-field record needs a trailing comma: (42,).
5. Misspelling a field name in the type
(name: 'Ali', mark: 80) is not a ({String name, int marks}). The names are part of the
type, as section 4 showed.
Best Practices
- Use a record to return two or three related values from a function.
- Prefer named fields once a record has more than two values or the meaning isn't obvious.
- Give a record type a
typedefwhen it appears in more than one place. - Switch to a class when the data needs methods, validation, or fields that change.
- Use records as map keys or set items when a pair of values is the identity, like a seat.
- Keep records small; a record with eight fields is a class in disguise.
Mini Challenge
Write a function stats that takes a List<int> of marks and returns a named record with
the total and the average (a double). Call it with Ali's marks [80, 75, 85].
Your program should print exactly:
Total 240, average 80.0Try it yourself before opening the solution.
Show solution
({int total, double average}) stats(List<int> marks) {
var total = 0;
for (var m in marks) {
total += m;
}
return (total: total, average: total / marks.length);
}
void main() {
var s = stats([80, 75, 85]);
print('Total ${s.total}, average ${s.average}');
}Total 240, average 80.0/ always gives a double, so average fits its field type.
Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Create a record
('Sara', 18)and print its second field. - Create a named record with
item: 'Pen'andprice: 40and printPen costs 40. - Declare a variable of type
(String, double)holding('Lahore', 24.5)and print it.
Application
- Print whether
(1, 2) == (1, 2)and whether(1, 2) == (2, 1). - Write
minMax(int a, int b)that returns(int, int)with the smaller value first. PrintminMax(9, 4). - Store
List<(String, int)>with('Ali', 80)and('Sara', 92). Print each asAli scored 80. - Write
typedef Point = ({int x, int y});and a functionPoint moveRight(Point p)that returns a new point withxone larger. PrintmoveRight((x: 1, y: 5)). - Put
('Ali', 80)into a set three times and print the set's length.
Challenge
- Write
divide(int a, int b)that returns a named record({int quotient, int remainder}). Print both fields fordivide(17, 5)as17 = 5 × 3 + 2. - A bank stores accounts as
Map<String, ({double balance, bool frozen})>. WithAli: (balance: 500.0, frozen: false)andSara: (balance: 900.0, frozen: true), print only the names of accounts that are not frozen and have more than100.0.
Practice Answers
Question 1 — Solution
void main() {
var person = ('Sara', 18);
print(person.$2);
}18Question 2 — Solution
void main() {
var pen = (item: 'Pen', price: 40);
print('${pen.item} costs ${pen.price}');
}Pen costs 40Question 3 — Solution
void main() {
(String, double) weather = ('Lahore', 24.5);
print(weather);
}(Lahore, 24.5)Question 4 — Solution
void main() {
print((1, 2) == (1, 2));
print((1, 2) == (2, 1));
}true
falseQuestion 5 — Solution
(int, int) minMax(int a, int b) => a < b ? (a, b) : (b, a);
void main() {
print(minMax(9, 4));
}(4, 9)Question 6 — Solution
void main() {
List<(String, int)> scores = [('Ali', 80), ('Sara', 92)];
for (var s in scores) {
print('${s.$1} scored ${s.$2}');
}
}Ali scored 80
Sara scored 92Question 7 — Solution
typedef Point = ({int x, int y});
Point moveRight(Point p) => (x: p.x + 1, y: p.y);
void main() {
print(moveRight((x: 1, y: 5)));
}(x: 2, y: 5)The old point is untouched; moveRight builds a new one.
Question 8 — Solution
void main() {
var seen = <(String, int)>{};
seen.add(('Ali', 80));
seen.add(('Ali', 80));
seen.add(('Ali', 80));
print(seen.length);
}1Question 9 — Solution
({int quotient, int remainder}) divide(int a, int b) {
return (quotient: a ~/ b, remainder: a % b);
}
void main() {
var r = divide(17, 5);
print('17 = 5 × ${r.quotient} + ${r.remainder}');
}17 = 5 × 3 + 2Question 10 — Solution
void main() {
Map<String, ({double balance, bool frozen})> accounts = {
'Ali': (balance: 500.0, frozen: false),
'Sara': (balance: 900.0, frozen: true),
};
accounts.forEach((name, account) {
if (!account.frozen && account.balance > 100.0) {
print(name);
}
});
}AliQuick Revision
- How do you write a record with two positional fields?
- Does
$1read the first field or the second? - What is the type of
(name: 'Ali', marks: 80)? - Can you change a field of a record after it is made?
- When are two records equal?
- How do you write a record with only one field?
- How does
typedefhelp with record types? - When should you use a class instead of a record?
Key Takeaways
- A record groups a fixed number of values, of any types, into one value:
('Ali', 80). - Positional fields are read with
$1,$2, …; named fields with.name. - Record types look like
(String, int)or({String name, int marks}); field names and order of positional fields are part of the type. - Records let a function return several values without writing a class.
- Records are immutable: build a new record to change a value.
==andhashCodecompare records by their fields, so they work in sets and as map keys.typedefgives a record type a short name.- Use records for small groups of data; use classes when data needs behaviour or rules.
What's Next?
Lecture 77 — Introduction to Patterns. This closes Category 12. Section 12 gave a
glimpse of var (low, high) = ..., which pulled a record apart in one line. That line is a
pattern. Next we will learn what patterns are, where Dart lets you use them, and how
they check and unpack values, starting Category 13 — Patterns & Modern Dart.