0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】TypeScriptの文字列ユニオン型が恋しい人へ贈る、Dartの拡張Enum活用術

0
Posted at

1. Dartのswitchで網羅性を担保するには?

Dartでswitch文を使うとき、すべてのケースを検証しているのか確証が持てない場合がありました。
TypeScriptであれば文字列リテラルのユニオン型が使えていたので、例えば次のようにして引数の取りうる値を絞り込むことができていました。

function calcTwoNumber(a: number, b: number, calc: "add" | "subtract" | "multiple" |"divide"): number {
    switch (calc) {
        case "add":
            return a + b;
        case "subtract":
            return a - b;
        case "multiple":
            return a * b;
        case "divide":
            return a / b;
    }
}

console.log(calcTwoNumber(10, 10, "add"));

calcに与えられるべき値は文字列リテラルのユニオン型によって定められているため、calcTwoNumber関数を呼び出してcalcに文字列を渡すとき、型であらかじめ定めた文字列以外を渡そうとすると型チェックでエラーを出してくれます。

しかし、Dartにはこのような便利なユニオン型はありません。
このためTypeScriptとは違う方法で、Dartのswitch文で網羅性を担保するための手法を考える必要があります。

(sealed classというものがありますがここでは触れません)

2. enum

Dartにおいては、列挙型変数とよばれるenumを使うことでこの問題を解決することができます。

enum Calc{
  add,
  subtract,
  multiple,
  divide,
}
  
double calcTwoNumber(double a, double b, Calc calc) {
  switch(calc) {
    case Calc.add:
      return a + b;
    case Calc.subtract:
      return a - b;
    case Calc.multiple:
      return a * b;
    case Calc.divide:
      return a / b;
  }
}

void main() {
  final result = calcTwoNumber(10.0, 10.0, Calc.add);
  print(result);
  
}

enumの中で宣言される定数は、頭文字が小文字であることが推奨されます。

3. widgetに値をテキストとして表示する

enumで定義された定数はそのままでは文字列として扱うことができません。しかし、その定数と文字列リテラルを一対一で使いたい時があるともいます。
Flutterにおいて、SegmentedButtonを使うときが好例になると思います。
それぞれのボタンのラベルは文字列であらねばならず、onSelectedChangedで受け取る選択された値も文字列になります。その値を使ってswitchで処理をしようにも先ほど述べたように文字列型では網羅性を担保することができず、このままではenumの値とも紐づけることができません。
このようなとき、拡張enumをつかってその表示名と定数の紐づけを実装することができます。

enum Segments {
  all("All"),
  ongoing("Ongoing"),
  completed("Completed");

  final String displayName;
  const Segments(this.displayName);

  static List<String> getStringDisplayName() {
    return Segments.values.map((filter) => filter.displayName).toList();
  }

  static Segments getEnumValueFromString(String displayName) {
    return Segments.values.firstWhere(
      (filter) => filter.displayName == displayName,
      orElse: () => Segments.all,
    );
  }
}

class Screen extends StatefulWidget {
  const Screen({super.key});

  @override
  State<Screen> createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  final List<String> _segments = Segments.getStringDisplayName();
  Segments _selectedFilter = Segments.all;

  void onHandleFilter(String newSelection) {
    setState(() {
      _selectedFilter = Segments.getEnumValueFromString(newSelection);
    });
  }

  @override
  Widget build(BuildContext context) {
    final todoState = context.watch<TodoState>();
    
    final filteredTodoList = todoState
      .getTodosByDate(DateTime.now())
      .where((todo) {
        switch (_selectedFilter) {
          case Segments.all:
            return true;
          case Segments.ongoing:
            return todo.achievement != Achievement.fulfilled;
          case Segments.completed:
            return todo.achievement == Achievement.fulfilled;
        }
      })
      .toList();

    return Scaffold(
      appBar: AppBar(
        title: Text("$todayStr Task"),
      ),
      body: SegmentedButton<String>(
        segments: _segments.map((e) => ButtonSegment<String>(
          value: e,
          label: Padding(
            padding: EdgeInsets.all(2),
            child: Text(e, style: const TextStyle(fontSize: 14)),
          ),
        )).toList(),
        selected: {_selectedFilter.displayName},
        showSelectedIcon: false,
        onSelectionChanged: (newSelection) {
          onHandleFilter(newSelection.first);
        },
      )
    );
  }
}

拡張enumは、その振る舞いについていえば「継承ができないクラス」のように考えることができると思います。インスタンスやライフサイクルには違いがありますのでそこには注意しておいてください。
定数に独自のプロパティも持たせることができ、静的メソッドを実装することすらできます。すなわち、メンバーとそれに関連したロジックもこのenumの中に閉じ込めることができるということです。
クラスでは、それぞれのメンバーはintやStringなど独自の型を持ちますが、enumの定数はそれ自体enumのインスタンスとなります。

細かく見ていきましょう。
SegmentedButtonのラベルにする値をdisplayNameとして、displayNameの配列を渡す静的メソッドとそのdisplayNameから対応する定数を返却する静的メソッドを実装しています。
以下の部分です。

enum Segments {
  all("All"),
  ongoing("Ongoing"),
  completed("Completed");

  final String displayName;
  const Segments(this.displayName);

  static List<String> getStringDisplayName() {
    return Segments.values.map((filter) => filter.displayName).toList();
  }

  static Segments getEnumValueFromString(String displayName) {
    return Segments.values.firstWhere(
      (filter) => filter.displayName == displayName,
      orElse: () => Segments.all,
    );
  }
}

これを使って、_segmentsにSegmentedButtonのsegments引数に渡す配列を代入したり、ボタンで選択された値(ここではString型)をSegmentsの定数(Segments型)に変換して_selectedFilterという変数を更新したりしています。
_selectedFilterはSegments型であるため、保存されているTodoListから選択されたフィルターに沿って目的のtodoを抽出する際につかうswitch文に網羅性を担保することができます。

その結果がこのswitch文になります。

final filteredTodoList = todoState
  .getTodosByDate(DateTime.now())
  .where((todo) {
    switch (_selectedFilter) {
      case Segments.all:
        return true;
      case Segments.ongoing:
        return todo.achievement != Achievement.fulfilled;
      case Segments.completed:
        return todo.achievement == Achievement.fulfilled;
    }
  })
  .toList();

Wigetのジェネリック

このコードはもともとSegmentedButtonをウィジェットとして切り出して使いまわしていたのを、Screenウィジェットと合体させたものになるので、propsの型としてsegmentsはStringの配列などとしていました。

Dartはジェネリック型をサポートしているため、上のコードだけで見ればわざわざStringとの変換メソッドを作る必要はなく、SegmentedButtonとすれば返り値がSegments型のいずれかの定数になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?