【Dart】Record型 × typedefで複数データの管理をスッキリ書く

## はじめに

Dart 3で導入された「Record型」は、複数の値を簡潔にまとめて扱える便利な仕組みです。
そして、`typedef` を使えば、その Record にわかりやすい名前をつけて、コードの可読性や再利用性を高めることができます。

この記事では、`typedef` と Record型を組み合わせて使うことで、ちょっとしたデータのやり取りや、Widgetへの引数渡し、Repository内でのデータ管理などを、よりスッキリと記述できる方法を紹介します。

「わざわざクラスを定義するほどではないけど、値をまとまりとして扱いたい…」
そんな場面にちょうどいい、Dartらしい書き方を探している方におすすめの内容です。

### 記事の対象者

- Dart 3の新機能であるRecord型に興味がある方
- `typedef` を使ってコードを整理・簡潔にしたいと考えている方
- 小規模な値のやり取りにクラスを使うのは大げさだと感じている方
- FlutterのWidgetに渡す引数をもっとすっきり書きたいと考えている方

### 記事を執筆時点での筆者の環境

```
[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)
```

## `typedef` とは？

`typedef` は**型エイリアス**と言われるもので対象の型を別名にすることができる機能です。

```dart
// int型を別名で定義
typedef Integer = int;

void fun(Integer value) {
  // 何かしらの処理
  // ...
}

void test1() {
  const int number = 42;

  // Integerはintの別名なので、int型も受け取る
  fun(number);

  const Integer number2 = 42;
  fun(number2);
}
```

上記の使い方だとあまりメリットはないのですが、これを使ってRecord型に型名をつけて取り回すこととができます。

## Record型 × typedef の基本的な使い方
### Record型のみの場合
dartのメソッドは戻り値をRecord型にして複数の値を一つの塊として返すことができます。
```dart
// Record型で値を返す場合
({int id, String content, List<String>? tags}) getRecordData() {
  return (id: 1, content: 'test', tags: null);
}

// Record型で引数を受け取る場合
void setRecordData(({int id, String content, List<String>? tags}) data) {
  print(data.id);
  print(data.content);
  print(data.tags);
}

void test2() {
  final recordData = getRecordData();

  setRecordData(recordData);
}

```
しかし、このままだと戻り値も引数も長くなってしまい可読性がよくありません。
### Record型 × typedef の場合
そこで `typedef` を併用してみます。

```dart
// int と String と List<String>? を返す値を持つ型を定義
typedef Data = ({int id, String content, List<String>? tags});

// typdefのData型で返す場合
Data getTypedefData() {
  return (id: 1, content: 'test', tags: null);
}

// typdefのData型で引数を受け取る場合
void setTypedefData(Data data) {
  print(data.id);
  print(data.content);
  print(data.tags);
}

void test3() {
  final data = getTypedefData();

  setTypedefData(data);
}
```
メソッドの戻り値、引数で `Data` を使うことでだいぶすっきりと書くことができるようになりました。
型を別名にしているだけなので、あまり乱用すると何が何だかわからなくなるという危険もあります。
個人的には一つのクラス内で複数の値のやり取りをRecord型で行う場合は便利だなと思いました。
(リポジトリークラスやサービスクラスなど)

また、これと同じことをRecord型ではなく、クラスにしてしまうという手もあります。

```dart
class SomeData {
  const SomeData({
    required this.id,
    required this.content,
    this.tags,
  });

  final int id;
  final String content;
  final List<String>? tags;
}
```
ただ、個人的にはクラスにしてしまうのはややオーバーエンジニアリングかなと考えます。
アプリ全体で使うものではなく、またはクラスの機能を使うのではなくてただの値の入れ物として使うのであれば、`typedf` × Record型 が適切かなと考えます。

## Widgetの引数でRecord型 × typedef を活用する
### コンポーネント側
この `ActionsSection` の仕様は以下のとおりです。
- 共通コンポーネントとして使う想定
- 一番右にくるSettingsButtonは常に表示する
- PersonButtonとRocketButtonは呼び出し場所によっては表示を切り替えられるようにする
- ボタンの設定を入れる必要がある
  - アイコン表示の有無： 必須
  - タップ処理： 必須
  - ロングタップ処理： 任意
```dart
import 'package:flutter/material.dart';

typedef ActionButtonProps = ({
  bool showLabel,
  VoidCallback onPressed,
  VoidCallback? onLongPress,
});

class ActionsSection extends StatelessWidget {
  const ActionsSection({
    this.personButtonProps,
    this.rocketButtonProps,
    super.key,
  });

  final ActionButtonProps? personButtonProps;
  final ActionButtonProps? rocketButtonProps;

  @override
  Widget build(BuildContext context) {
    return Row(
      spacing: 10,
      children: [
        if (personButtonProps case final props?)
          ElevatedButton.icon(
            icon: props.showLabel ? const Icon(Icons.person) : null,
            onPressed: props.onPressed,
            onLongPress: props.onLongPress,
            label: const Text('Person'),
          ),
        if (rocketButtonProps case final props?)
          ElevatedButton.icon(
            icon: props.showLabel ? const Icon(Icons.rocket) : null,
            onPressed: props.onPressed,
            onLongPress: props.onLongPress,
            label: const Text('Rocket'),
          ),
        const Spacer(),
        // 設定ボタンは常に表示
        ElevatedButton.icon(
          icon: const Icon(Icons.settings),
          onPressed: () {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Settings button pressed')),
            );
          },
          label: const Text('Settings'),
        ),
      ],
    );
  }
}
```
`ActionsSection` の引数に二つの `ActionButtonProps` というレコード型 `personButtonProps` と  `rocketButtonProps` を受け取っています。
`personButtonProps` と  `rocketButtonProps` はともにnullableです。
この値が入っていた場合はボタンを表示するようにしています。

:::note
`if (personButtonProps case final props?)` は nullチェックのif-case構文です。
`if (personButtonProps != null)` と同じ意味で、違うのはif文の後に `?` を使わなくて良くなる点です。
:::

### 呼び出し側
```dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:simple_base/presentations/shared/actions_section.dart';

class HomeScreen extends ConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            spacing: 40,
            children: [
              ActionsSection(
                personButtonProps: (
                  showLabel: true,
                  onPressed: () {
                    // タップ処理
                  },
                  onLongPress: null
                ),
                rocketButtonProps: (
                  showLabel: true,
                  onPressed: () {
                    // タップ処理
                  },
                  onLongPress: () {
                    // 長押し処理
                  }
                ),
              ),
              const Divider(),
              ActionsSection(
                personButtonProps: (
                  showLabel: false,
                  onPressed: () {
                    // タップ処理
                  },
                  onLongPress: () {
                    // 長押し処理
                  }
                ),
              ),
              const Divider(),
              ActionsSection(
                rocketButtonProps: (
                  showLabel: true,
                  onPressed: () {
                    // タップ処理
                  },
                  onLongPress: () {
                    // 長押し処理
                  }
                ),
              ),
              const Divider(),
              const ActionsSection(),
              const Divider(),
            ],
          ),
        ),
      ),
    );
  }
}
```

![typedf_and_record.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/acc684a8-b24c-44e7-8f07-5ad8bbb8db65.png)


## 終わりに

Dart 3から使えるようになったRecord型と、型に名前をつけるtypedefを組み合わせることで、コードをより簡潔に・わかりやすく保つことができます。
特に「ちょっとした値のまとまり」を扱う場面では、クラスよりも手軽に使える選択肢としてとても便利です.

一方で、使いすぎると型の意味が曖昧になってしまう恐れもあるため、適切な場面を見極めて使うのがポイントです.

今後もこうしたDartの言語機能を活用して、より良い設計や開発効率を追求していきたいですね！