Skip to content

Commit

Permalink
incorporate lrhn review
Browse files Browse the repository at this point in the history
  • Loading branch information
MaryaBelanger committed Feb 19, 2025
1 parent 1436da5 commit 2fab6a5
Showing 1 changed file with 98 additions and 30 deletions.
128 changes: 98 additions & 30 deletions src/content/language/records.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ recordAB = recordXY; // OK.
```

This is similar to how positional parameters
in a function declaration or function typedef
in a [function declaration or function typedef][function-type]
can have names but those names don't affect the signature of the function.

For more information and examples, check out
Expand Down Expand Up @@ -226,25 +226,16 @@ parallelization of futures of different types, which you can read about in the

## Records as simple data structures

The provided example illustrates how developers can leverage this pattern to
define dynamic UI widgets in a type-safe manner, similar to interfaces in
TypeScript but without the overhead of classes. This approach is particularly
useful for scenarios where simplicity and reusability are prioritized.
Records only hold data. When that's all you need,
they're immediately available and easy to use
without needing to declare any new classes.
For a simple list of data tuples that all have the same shape,
a *list of records* is the most direct representation.

```dart
typedef ButtonItem = ({
String label,
Widget? icon,
void Function()? onPressed,
});
```

Here is an example about using `record` and `typedef` as structure of dynamic UI
widgets render:
Take this list of "button definitions", for example:

```dart
// Define list from `buttonItem` interface.
List<ButtonItem> buttons = [
final buttons = [
(
label: "Button I",
icon: const Icon(Icons.upload_file),
Expand All @@ -256,28 +247,105 @@ List<ButtonItem> buttons = [
onPressed: () => print("Action -> Button II"),
)
];
```

This code can be written directly without needing any additional declarations.

### Records and typedefs

You can choose to use [typedefs][] to give the record type itself a name,
and use that rather than writing out the full record type.
This method allows you to state that some fields can be null (`?`),
even if none of the current entries in the list have a null value.

```dart
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
// ...
];
```

Because record types are structural types, giving a name like `ButtonItem`
only introduces an alias that makes it easier to refer to the structural type:
`({String label, Icon icon, void Function()? onPressed})`.

Having all your code refer to a record type by its alias makes it easier to
later change the record's implementation without needing to update every reference.

Code can work with the given button definitions the same way it would
with simple class instances:

```dart
List<Container> widget = [
for (var button in buttons)
Container(
margin: const EdgeInsets.all(4.0),
child: OutlinedButton.icon(
onPressed: button.onPressed,
icon: button.icon,
label: Text(button.label),
),
),
];
```

You could even decide to later change the record type to a class type to add methods:

```dart
class ButtonItem {
final String label;
final Icon icon;
final void Function()? onPressed;
ButtonItem({required this.label, required this.icon, this.onPressed});
bool get hasOnpressed => onPressed != null;
}
```

Or to an [extension type][]:

```dart
extension type ButtonItem._(({String label, Icon icon, void Function()? onPressed}) _) {
String get label => _.label;
Icon get icon => _.icon;
void Function()? get onPressed => _.onPressed;
ButtonItem({required String label, required Icon icon, void Function()? onPressed})
: this._((label: label, icon: icon, onPressed: onPressed));
bool get hasOnpressed => _.onPressed != null;
}
```

List<Container> widget = buttons.map(
(ButtonItem button) => Container(
margin: const EdgeInsets.all(4.0),
child: OutlinedButton.icon(
onPressed: button.onPressed,
icon: button.icon!,
label: Text(button.label),
),
And then create the list of button definitions using that type's constructors:

```dart
final List<ButtonItem> buttons = [
ButtonItem(
label: "Button I",
icon: const Icon(Icons.upload_file),
onPressed: () => print("Action -> Button I"),
),
ButtonItem(
label: "Button II",
icon: const Icon(Icons.info),
onPressed: () => print("Action -> Button II"),
)
)
.toList();
];
```

The example demonstrates how to define a `ButtonItem` structure using records
and `typedef`, which promotes cleaner and more maintainable code by adhering to
the DRY (Don't Repeat Yourself) principle.
Again, all while not needing to change the code that uses that list.

Changing any type does require the code using it to be very careful about
not making assumptions. A type alias does not offer any protection or guarantee
for the code using it that the value is a record.
Extension types, also, offer little protection.
Only a class can provide full abstraction and control over access to its state.

[language version]: /resources/language/evolution#language-versioning
[collection types]: /language/collections
[pattern]: /language/patterns#destructuring-multiple-returns
[`dart:async` documentation]: /libraries/dart-async#handling-errors-for-multiple-futures
[parameters and arguments]: /language/functions#parameters
[function-type]: /language/functions#function-types
[destructure]: /language/patterns#destructuring
[Pattern types]: /language/pattern-types#record
[typedefs]: /language/typedefs
[extension type]: /language/extension-types

0 comments on commit 2fab6a5

Please sign in to comment.