flutter_datetime_picker で5分単位などカスタム選択肢を表示する

2024/03/24に公開

はじめに

flutter_datetime_picker は iOS でおなじみの日時選択のピッカーを Flutter で使えるパッケージです。
dart 3.0/Flutter 3.10 以降に対応した flutter_datetime_picker_plus もフォークされて提供されています。

このパッケージでは分の選択がデフォルトでは1分単位選択できますが、5分や15分単位での選択肢を表示させるオプションが存在していませんでした。
1分単位ではアプリのユースケース上細かすぎたので、選択肢をカスタマイズしてみました。

つまりこういうこと

やったこと

  • 何分単位にしたいかを定義して、外から渡す
  • カスタマイズ方法を参考にカスタマイズ
  • カスタマイズしたモデルを使って、DatePicker を呼び出す

何分単位にしたいかの定義

アプリや機能によって、5分単位, 10分単位など何分間隔で分の選択肢を表示したいかは、まちまちでしょう。
今回は↓のようなenum を定義して、DatePicker 表示時に渡すようにしました。

/// DatePicker の分の選択肢をどの間隔にするか
enum MinuteInterval {
  five(5),
  ten(10),
  fifteen(15),
  thirty(30);

  const MinuteInterval(this.minute);

  final int minute;
}

※20分の選択肢は筆者のユースケースで不要だったので除いていますが、追加しても問題ありません。
※60で割り切れない選択肢を増やした場合の挙動は未確認です
※int で渡すと、負数や 60 以上の数を渡される可能性があるため、enum で取り得るパラメータの選択肢を限定しています。

DatePicker のカスタマイズ

https://pub.dev/packages/flutter_datetime_picker_plus#customize
に記されているとおり、 CommonPickerModel 派生のモデルを定義して、 DatePicker に渡してあげれば OK です。

前提として、

  • Picker には3列表示できる
  • それぞれ、left, middle, right と内部的には管理されている
     (利用シーンによって、年・月・日だったり、時・分・秒だったりするのでこの名前なんだろう)
  • 選択中の DateTime オブジェクトを finalTime 関数で返す

あたりを押さえておけば、実装の理解が捗るかと思います。

実際のコード

class CustomPicker extends CommonPickerModel {
  CustomPicker({
    required DateTime? currentTime,
    required LocaleType locale,
    required this.minuteInterval,
  }) : super(locale: locale) {
    this.currentTime = currentTime ?? DateTime.now();
    // 時は0〜23なのでそのまま index と対応している
    setLeftIndex(this.currentTime.hour);
    // interval を考慮して index を指定
    // 5分の interval の場合、要素は[0,5,10,...55]
    // currentTime.minute が 10 なら index は 2 になる
    setMiddleIndex(this.currentTime.minute ~/ minuteInterval.minute);
    // 秒は使わないので0を渡しておく(指定しないと LateInitializationError になる)
    setRightIndex(0);
  }

  final MinuteInterval minuteInterval;

  // value を length で指定された桁数で表示する。
  // 桁数が足りないときは 0 埋めする
  // ※ライブラリ内で定義されている関数を移植した
  String digits(int value, int length) => '$value'.padLeft(length, '0');

  
  // 時の表示文字列を要素の index から生成
  // null を返せば選択肢として表示されない
  String? leftStringAtIndex(int index) {
    if (index >= 0 && index < 24) {
      // 0 〜 24 は 2 桁で表示
      return digits(index, 2);
    } else {
      // それ以外の要素は表示させない
      return null;
    }
  }

  
  String? middleStringAtIndex(int index) {
    // index の上限は 60 ÷ minuteInterval.minute
    // (60 / minuteInterval.minute).toInt() にすると division_optimization になるので
    // 60 ~/ minuteInterval.minute としている
    if (index >= 0 && index < 60 ~/ minuteInterval.minute) {
      // 表示する分の文字列は index * interval
      return digits(index * minuteInterval.minute, 2);
    } else {
      return null;
    }
  }

  
  // 時と分の間の Divider 文字列
  String leftDivider() => ':';

  
  // 分と秒の間の Divider 文字列
  // 秒を表示しないので空文字を返しておく
  String rightDivider() => '';

  
  // 時、分、秒のそれぞれの列の幅の比率
  // 秒は使わないので 0 を指定
  List<int> layoutProportions() => [1, 1, 0];

  
  DateTime finalTime() {
    return currentTime.isUtc
        ? DateTime.utc(
            currentTime.year,
            currentTime.month,
            currentTime.day,
            currentLeftIndex(),
            // middle の index から分を算出
            currentMiddleIndex() * minuteInterval.minute,
            currentRightIndex(),
          )
        : DateTime(
            currentTime.year,
            currentTime.month,
            currentTime.day,
            currentLeftIndex(),
            // middle の index から分を算出
            currentMiddleIndex() * minuteInterval.minute,
            currentRightIndex(),
          );
  }
}

呼び出し

DatePicker.showPicker(
  context,
  locale: LocaleType.jp,
  onConfirm: (date) { ... },
  pickerModel: CustomPicker(
    currentTime: DateTime.now(),
    minuteInterval: MinuteInterval.five,
    locale: LocaleType.jp,
  ),
);

※showPicker にも pickerModel にも locale を指定するのが気になるところ…
(実装を追えばどちらが優先されるかはわかるかと思いますが、ここでは割愛🙇)

実行結果

意図して通りに動いてました 💯

five ten fifteen thirty

補足

  • 頑張れば繰り上がりの実装もできるかな…と思いつつも、iOS の picker がやってなかったので割愛
  • 12h 表記にしたい場合などは、よしなにカスタマイズして下さい

Discussion