Small details that build taste in Flutter.

curated by Kamran BekirovKamran Bekirov

Match the date picker to the platform

CupertinoDatePicker looks polished, but it's iOS-native. Android users don't expect a scroll-wheel picker for dates. An iOS widget dropped into Android can even read as cheap.

To avoid this, use the scroll picker on iOS and the default Material one on Android.

Don't worry if your app has a custom design. The Material picker is platform UI, so users read the exception as normal. You can still theme it. Just keep the skeleton layout, which is muscle memory for Android users.

You could reach for a community package instead, but I've yet to find one neutral enough to justify mixing with a custom design.

The difference, side by side:

iOS vs Android date time picker

Call it and await the picked date:

final DateTime? date = await AdaptiveDatePicker.show(
  context,
  initialDate: _date,
  firstDate: DateTime(2000),
  lastDate: DateTime.now(),
);
  
if (date != null) {
  setState(() => _date = date);
}

It returns null when the user dismisses the sheet, so keep the old value in that case.

Here's the widget. Tweak the values to taste:

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
  
class AdaptiveDatePicker {
  const AdaptiveDatePicker._();
  
  static Future<DateTime?> show(
    BuildContext context, {
    required DateTime firstDate,
    required DateTime lastDate,
    DateTime? initialDate,
  }) {
    final bool isIos = defaultTargetPlatform == .iOS;
  
    if (isIos) {
      return _CupertinoDatePickerSheet.show(
        context,
        initialDate: initialDate,
        minimumDate: firstDate,
        maximumDate: lastDate,
      );
    }
  
    return showDatePicker(
      context: context,
      initialDate: initialDate,
      firstDate: firstDate,
      lastDate: lastDate,
    );
  }
}

class _CupertinoDatePickerSheet extends StatefulWidget {
  final DateTime? initialDate;
  final DateTime? minimumDate;
  final DateTime? maximumDate;
  
  const _CupertinoDatePickerSheet({
    this.initialDate,
    this.minimumDate,
    this.maximumDate,
  });
  
  static Future<DateTime?> show(
    BuildContext context, {
    DateTime? initialDate,
    DateTime? minimumDate,
    DateTime? maximumDate,
  }) {
    return showModalBottomSheet<DateTime?>(
      context: context,
      builder: (BuildContext context) {
        return _CupertinoDatePickerSheet(
          initialDate: initialDate,
          minimumDate: minimumDate,
          maximumDate: maximumDate,
        );
      },
    );
  }
  
  @override
  State<_CupertinoDatePickerSheet> createState() =>
      _CupertinoDatePickerSheetState();
}
  
class _CupertinoDatePickerSheetState extends State<_CupertinoDatePickerSheet> {
  late DateTime _selectedDate = widget.initialDate ?? .now();
  
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 400,
      padding: const .symmetric(horizontal: 16),
      child: SafeArea(
        child: Column(
          children: [
            Flexible(
              child: CupertinoDatePicker(
                mode: .date,
                initialDateTime: widget.initialDate,
                minimumDate: widget.minimumDate,
                maximumDate: widget.maximumDate,
                onDateTimeChanged: (DateTime dateTime) {
                  _selectedDate = dateTime;
                },
              ),
            ),
            const SizedBox(height: 24),
            // TODO: replace with your button widget
            Button(
              label: 'Done',
              onPressed: () {
                Navigator.of(context).pop(_selectedDate);
              },
            ),
          ],
        ),
      ),
    );
  }
}
Kamran Bekirov
Kamran Bekirov

Want this level of care in your Flutter apps?

Work With Me