Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Dart3を試してみる

最近2匹の猫との生活を始めました、アプリエンジニアチームの務台です。

今回は今年中旬に正式リリースと噂のDart3を使って、みんな大好きTodoアプリを作成してみたので、その感想等を共有できればと思います。 なお、2023/03/17時点ではまだアルファ版のため、今後機能の変更や削除の可能性があります。

作成したアプリのソースはこちらです。

github.com

実行環境

  • Flutter: 3.8.0-7.0.pre.2 (masterチャンネル)
  • Dart: Dart 3.0.0 (build 3.0.0-204.0.dev)

開発時の注意

Dart3の機能はまだ正式リリースされていないため、コンパイル時に以下のオプションで明示的に指定する必要があります。

--enable-experiment=records,class-modifiers,sealed-class,patterns

また、lintや自動整形も効かなくなってしまうため注意が必要です。

新機能について

Dart3で追加される新機能については以下にまとめられています。

medium.com

ここにあるように、Dart3の新機能には大きく以下のものがあります。

  • record型の追加
  • パターンマッチングの追加
  • クラス修飾子の追加

それぞれの説明、使用した感想等についてまとめていきます。

record型

概要

複数の値をまとめたものを、簡単に新たな型として使うことができる機能です。

以下のような、天気温度湿度を取得する関数を作るとします。

[気候型] fetchClimate() {
  // 天気、温度、湿度を取得して返す
}

従来では気候型のクラスを作って、それを返すようにする必要がありました。

@immutable
class Climate {
  const Climate(
    this.weather,
    this.tempereture,
    this.humidity,
  );

  final String weather;
  final double tempereture;
  final double humidity;
}

Climate fetchClimate() {
  return Climate('晴れ', 18.4, 70.4);
}

record型を使うことでわざわざクラスを新設せず、以下のように簡易的に記述することができるようになりました。

(String weather, double temperature, double humidity) fetchClimate() {
  return ('晴れ', 18.4, 70.4);
}

感想

かなり短く書くことができるようになりますが、個人的には基本的にrecord型は使わない方が良いかと思っています。 その理由として、record型は関数ごとに新たに返却するための型を宣言し直す必要があるため、同じデータを返すはずなのに型が違っている、という状況が起こり得ます。また、型の名前をつけることができないため、取得できるデータはなんなのかが推測しづらくなってしまいます。

…と思っているので、今回のサンプルアプリではrecord型は使用していません。スミマセン。

パターンマッチング

概要

パターンマッチングとは特定のパターンにマッチするデータを取得することができる機能です。 後述のsealed classと組み合わせることにより、以下のように実際の型に合わせて処理を変える、ということができるようになります。

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(radius: var r) => math.pi * r * r
};

このような分岐は、既存ではFreezedを使用することで実現できていましたが、これでパッケージ無しで同様のことができるようになります。

特に嬉しいのがswitchを式として利用できるようになったことで、以前ではreturnを複数書かなければいけなかったものを簡潔に記述することができるようになりました。

感想

サンプルではTodoStatussealed classとして用意して、Screen側で値によってアイコンを出し分ける、という処理を行っています。

lib/feature/todo/domain/value/todo_status.dart

sealed class TodoStatus{
  String get statusString;
}

@immutable
final class NotStarted implements TodoStatus{
  @override
  String get statusString => 'NotStarted';
}

@immutable
final class InProgress implements TodoStatus{
  @override
  String get statusString => 'InProgress';
}

@immutable
final class Finished implements TodoStatus{
  @override
  String get statusString => 'Finished';
}

lib/feature/todo/presentation/todo_list/widget/todo_item.dart

// 27行目 アイコンの切り替え
leading: Icon(
  switch(status) {
    NotStarted() => Icons.radio_button_unchecked,
    InProgress() => Icons.more_horiz,
    Finished() => Icons.radio_button_checked,
  }, 
  color: Colors.grey,
),

// 61行目 背景色の切り替え
tileColor: switch(status) {
  NotStarted() => Colors.transparent,
  InProgress() => const Color.fromARGB(255, 207, 253, 209),
  Finished() => const Color.fromARGB(255, 207, 207, 207),
},

ステータスによる表示の切り替えを簡潔に記述することができ、とても使い勝手が良かったです。

クラス修飾子の追加

概要

ここでのクラス修飾子とはclassの前につく修飾子のことで、今までDartにはabstractくらいしか存在しませんでした。 Dart3からは一気に増えて、以下のキーワードが使用できるようになります。

宣言 説明
interface class このクラスの継承を不可にする。他の言語でのinterfaceと同様だが、interface内でメソッドを実装する必要がある。
base class 暗黙的インターフェースを無効にすることで、implementによる実装を無効にする。
final class このクラスの継承、実装、ミックスインを禁止する。
sealed class パターンマッチングで使用したように、複数のクラスを一つのグループとしてまとめる。
mixin class 他クラスへのミックスインを有効化する

感想

今までは暗黙的インターフェースにより意図しない継承、実装が可能だった点が微妙だと思っていたのですが、finalによりそれを封じることができるようになった点はとてもありがたいと感じました。プロダクト開発では基本的にfinalを付けてクラス定義していくのが良さそうです。

interfaceについて、抽象メソッドを持つことができないため、UnimplementedErrorを投げる等、不要な実装が必要になってしまうのが微妙かなと感じました。

lib/feature/todo/repository/todo_repository.dart

interface class TodoRepository {
  Future<int> add(domain.Todo todo) => throw UnimplementedError();
  Future<int> update(domain.Todo todo) => throw UnimplementedError();
  Future<void> delete(int id) => throw UnimplementedError();
  Stream<List<domain.Todo>> watchAll() => throw UnimplementedError();
  Future<domain.Todo> fetch(int id) => throw UnimplementedError();
}

ただし、2023/03/17現在ではinterface classを継承してもコンパイルエラーにならなかったため、まだまだ変更、修正の可能性はあるのかなぁと思っています。

まとめ

個人的には暗黙的インターフェースの仕組みがあまり好きではなかったので、明示的に無効化できるようになってかなり便利だなぁと感じました。 その他の新機能についても、これまでFreezedが必要だった部分が言語としてサポートされて、今後に期待できそうです。

次はバリューオブジェクト型が増えるといいなぁ。