【Flutter】CupertinoDatePickerのminimumDateに今日の日付を設定すると初期値が次の日になる

# 事象
[CupertinoDatePicker](https://api.flutter.dev/flutter/cupertino/CupertinoDatePicker-class.html)を使って日時と時刻を設定する画面にて、CupertinoDatePickerのコンストラクタで`minimumDate`に`DateTime.now()`を指定すると、DateTime.now()で生成される日付の次の日の日付が初期値に表示されてしまいます。

```dart:sample.dart
Widget buildCupertinoDatePicker() {
  return SizedBox(
    height: 200,
    child: CupertinoDatePicker(
      use24hFormat: true,
      minimumDate: DateTime.now(),
      onDateTimeChanged: (DateTime newDateTime) {},
    ),
  );
}
```

上記コードの表示は下記の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136368/9e9b17a8-ba58-3b0f-b81c-158e792fe2a0.png)

本来はこうなるのが期待値。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136368/01283aa4-b8f6-22ca-1f39-39eb0c1c2673.png)

# 原因
それらしい原因は見つけたのですが、根本的に何故そうなるのかまでは追い切れませんでした。
ひとまずそれらしいところまでの処理を順を追って説明します。

## 各日付要素のWidgetはitemBuilderでindex毎に生成される
CupertinoDatePickerはStatefulWidgetで、日付と時刻の表示形式になる`CupertinoDatePickerMode.dateAndTime`の時は`_CupertinoDatePickerDateTimeState`をStateに持ちます。

```dart:
  @override
  State<StatefulWidget> createState() {
    // The `time` mode and `dateAndTime` mode of the picker share the time
    // columns, so they are placed together to one state.
    // The `date` mode has different children and is implemented in a different
    // state.
    if (mode == CupertinoDatePickerMode.time || mode == CupertinoDatePickerMode.dateAndTime)
      return _CupertinoDatePickerDateTimeState();
    else
      return _CupertinoDatePickerDateState();
  }
```

このStateの`_buildMediumDatePicker`メソッドで日付のピッカーが生成され、そこで渡されるitemBuilderによって各日付要素のWidgetが生成されます。

```dart:
      itemBuilder: (BuildContext context, int index) {
        final DateTime dateTime = DateTime(
          initialDateTime.year,
          initialDateTime.month,
          initialDateTime.day,
        ).add(Duration(days: index));

        if (widget.minimumDate != null && dateTime.isBefore(widget.minimumDate))
          return null;
        if (widget.maximumDate != null && dateTime.isAfter(widget.maximumDate))
          return null;

        final DateTime now = DateTime.now();
        String dateText;

        if (dateTime == DateTime(now.year, now.month, now.day)) {
          dateText = localizations.todayLabel;
        } else {
          dateText = localizations.datePickerMediumDate(dateTime);
        }

        return itemPositioningBuilder(
          context,
          Text(
            dateText,
            style: _themeTextStyle(context),
          ),
        );
      }
```

## CupertinoDatePickerは初期値の前後も描画する
最初に掲載したスクリーンショットを見ていただくと分かりますが、フォーカスが当たっている初期値の前後の値も薄く描画されています。

itemBuilderでは、この前後のアイテム数に応じて渡されるindexが変化します。
例えば、最初に記載したコードでは高さが200ですが、この場合は初期値の前後に4つのアイテムが薄く表示されます。（スクリーンショットでは分かりにくいですが、薄く小さく4つ目も描画されています。）
したがって、itemBuilderのindexには-4から値が流れてきます。

## minimumDateより前の日付はnullが返却され描画されない
itemBuilderの中では、minimumDateに設定された日付と薄く描画される前4つの日付を比較して、minimumDateより前の場合はnullを返して描画しないように実装されています。

```dart:
      itemBuilder: (BuildContext context, int index) {
        final DateTime dateTime = DateTime(
          initialDateTime.year,
          initialDateTime.month,
          initialDateTime.day,
        ).add(Duration(days: index));

        if (widget.minimumDate != null && dateTime.isBefore(widget.minimumDate))
          return null;

        // 省略...
      }
```

例えば、8月14日をminimumDateに与えた場合は下記のような結果になります。

```
index: -4, dateTime: 8月10日, minimumDate: 8月14日, return null;
index: -3, dateTime: 8月11日, minimumDate: 8月14日, return null;
index: -2, dateTime: 8月12日, minimumDate: 8月14日, return null;
index: -1, dateTime: 8月13日, minimumDate: 8月14日, return null;
index:  0, dateTime: 8月14日, minimumDate: 8月14日, return null;
```

このように、今日の日付（8月14日）を設定すると、上記のminimumDateの判定文で弾かれてしまいます。
結果として、描画されるはずのWidgetが生成されずに次の日付から描画されてしまうという事象のようです。

# 対応
minimumDateに渡す日付を1日前にずらしました。

```dart:
Widget buildCupertinoDatePicker() {
  return SizedBox(
    height: 200,
    child: CupertinoDatePicker(
      use24hFormat: true,
      minimumDate: DateTime.now().subtract(Duration(days: 1)),
      onDateTimeChanged: (DateTime newDateTime) {},
    ),
  );
}
```