Flutter: Custom Cupertino Date Picker

Hitesh Verma
7 min readAug 23, 2022

Hey guys, do you ever required to build a Cupertino date picker, similar to the one shown in the image below? But got frustrated because CupertinoDatePicker widget does not bring much (any) customizations. :-(

This article will help you to create one of your own Cupertino date picker, with unlimited possibilities for customizations. :-)

Find the updated article here.

Custom Cupertino Date Picker

Under the hood, the CupertinoDatePicker widget make use of CupertinoPicker widget, which brings some customizations.

We are also going to use the CupertinoPicker widget to achieve our goal!

1. Create a stateful widget for our custom date picker.

This widget will essentially be a wrapper to the CupertinoPicker widget. Declare all the parameters needed in a CupertinoPicker widget. (You can also limit yourself to the parameters you require.)

class CustomCupertinoDatePicker extends StatefulWidget {
final double itemExtent;
final Widget selectionOverlay;
final double diameterRatio;
final Color? backgroundColor;
final double offAxisFraction;
final bool useMaginifier;
final double magnification;
final double squeeze;
final void Function(DateTime) onSelectedItemChanged;
// Text style of selected item
final TextStyle? selectedStyle;
// Text style of unselected item
final TextStyle? unselectedStyle;
// Text style of disabled item
final TextStyle? disabledStyle;
// Minimum selectable date
final DateTime? minDate;
// Maximum selectable date
final DateTime? maxDate;
// Initially selected date
final DateTime? selectedDate;
const CustomCupertinoDatePicker({
Key? key,
required this.itemExtent,
required this.onSelectedItemChanged,
this.minDate,
this.maxDate,
this.selectedDate,
this.selectedStyle,
this.unselectedStyle,
this.disabledStyle,
this.backgroundColor,
this.squeeze = 1.45,
this.diameterRatio = 1.1,
this.magnification = 1.0,
this.offAxisFraction = 0.0,
this.useMaginifier = false,
this.selectionOverlay = const
CupertinoPickerDefaultSelectionOverlay(),
}) : super(key: key);
@override
State<CustomCupertinoDatePicker> createState() =>
_CustomCupertinoDatePickerState();
}
class _CustomCupertinoDatePickerState extends
State<CustomCupertinoDatePicker> {
@override
Widget build(BuildContext context) {}
}

In the code above, I’ve explained the some parameters in the comments. To know about all other parameters, please refer to CupertinoPicker doc.

2. Declare some properties

Let’s declare some properties in the “_CustomCupertinoDatePickerState” class which will be used in later steps.

late DateTime _minDate;
late DateTime _maxDate;
late DateTime _selectedDate;
late int _selectedDayIndex;
late int _selectedMonthIndex;
late int _selectedYearIndex;
late final FixedExtentScrollController _dayScrollController;
late final FixedExtentScrollController _monthScrollController;
late final FixedExtentScrollController _yearScrollController;
final _days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];final _months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'Novemeber',
'December',
];

You can change the strings of _months list, if you want to display some other texts for months. For example, [‘JAN’, ‘FEB’, ‘MAR’, …].

3. Initialize & validate dates & controllers

We’ve declared our controllers, now it’s time to initialize them.

@override
void initState() {
super.initState();
_validateDates();
_dayScrollController = FixedExtentScrollController();
_monthScrollController = FixedExtentScrollController();
_yearScrollController = FixedExtentScrollController();
_initDates();
}

Also, we need to validate the dates passed as arguments from the user.

void _validateDates() {
if (widget.minDate != null && widget.maxDate != null) {
assert(!widget.minDate!.isAfter(widget.maxDate!));
}
if (widget.minDate != null && widget.selectedDate != null) {
assert(!widget.minDate!.isAfter(widget.selectedDate!));
}
if (widget.maxDate != null && widget.selectedDate != null) {
assert(!widget.selectedDate!.isAfter(widget.maxDate!));
}
}

Initialize dates in case, no dates are passed in the arguments.

void _initDates() {
final currentDate = DateTime.now();
_minDate = widget.minDate ?? DateTime(currentDate.year - 100);
_maxDate = widget.maxDate ?? DateTime(currentDate.year + 100);
if (widget.selectedDate != null) {
_selectedDate = widget.selectedDate!;
} else if (!currentDate.isBefore(_minDate) &&
!currentDate.isAfter(_maxDate)) {
_selectedDate = currentDate;
} else {
_selectedDate = _minDate;
}
_selectedDayIndex = _selectedDate.day - 1;
_selectedMonthIndex = _selectedDate.month - 1;
_selectedYearIndex = _selectedDate.year - _minDate.year;
WidgetsBinding.instance.addPostFrameCallback((_) => {
_scrollList(_dayScrollController, _selectedDayIndex),
_scrollList(_monthScrollController, _selectedMonthIndex),
_scrollList(_yearScrollController, _selectedYearIndex),
});
}

Here, current date, minus 100 years from current date, and plus 100 years from current date is used as default values for _selectedDate, _minDate, and _maxDate respectively, in case no values are provided. But you are free to modify this as per your requirements.

Method to scroll our pickers to selected positions.

void _scrollList(FixedExtentScrollController controller, int index){
controller.animateToItem(
index,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 300),
);
}

Don’t forget to dispose of the controllers! It’s better to do it now.

@override
void dispose() {
_dayScrollController.dispose();
_monthScrollController.dispose();
_yearScrollController.dispose();
super.dispose();
}

4. Add utility methods

/// check if selected year is a leap year
bool _isLeapYear() {
final year = _minDate.year + _selectedYearIndex;
return year % 4 == 0 &&
(year % 100 != 0 || (year % 100 == 0 && year % 400 == 0));
}
/// get number of days for the selected month
int _numberOfDays() {
if (_selectedMonthIndex == 1) {
_days[1] = _isLeapYear() ? 29 : 28;
}
return _days[_selectedMonthIndex];
}

Create an enum to distinguish among selectors, in the same or different file.

enum _SelectorType { day, month, year }

5. Update the selected date & indexes when picker selection changed

This method will check if the newly selected date is valid (within the min-max date range), if yes, then it will update the selected date & indexes, else it will scroll back Cupertino picker to the last position.

void _onSelectedItemChanged(int index, _SelectorType type) {
DateTime temp;
switch (type) {
case _SelectorType.day:
temp = DateTime(
_minDate.year + _selectedYearIndex,
_selectedMonthIndex + 1,
index + 1,
);
break;
case _SelectorType.month:
temp = DateTime(
_minDate.year + _selectedYearIndex,
index + 1,
_selectedDayIndex + 1,
);
break;
case _SelectorType.year:
temp = DateTime(
_minDate.year + index,
_selectedMonthIndex + 1,
_selectedDayIndex + 1,
);
break;
}

// return if selected date is not the min - max date range
// scroll selector back to the valid point

if (temp.isBefore(_minDate) || temp.isAfter(_maxDate)) {
switch (type) {
case _SelectorType.day:
_dayScrollController.jumpToItem(_selectedDayIndex);
break;
case _SelectorType.month:
_monthScrollController.jumpToItem(_selectedMonthIndex);
break;
case _SelectorType.year:
_yearScrollController.jumpToItem(_selectedYearIndex);
break;
}
return;
}
// update selected date
_selectedDate = temp;
// adjust other selectors when one selctor is changed
switch (type) {
case _SelectorType.day:
_selectedDayIndex = index;
break;
case _SelectorType.month:
_selectedMonthIndex = index;
// if month is changed to february &
// selected day is greater than 29,
// set the selected day to february 29 for leap year
// else to february 28

if (_selectedMonthIndex == 1 && _selectedDayIndex > 27) {
_selectedDayIndex = _isLeapYear() ? 28 : 27;
}
// if selected day is 31 but current selected month has only
// 30 days, set selected day to 30

if (_selectedDayIndex == 30 && _days[_selectedMonthIndex] ==
30) {
_selectedDayIndex = 29;
}
break;
case _SelectorType.year:
_selectedYearIndex = index;
// if selected month is february & selected day is 29
// But now year is changed to non-leap year
// set the day to february 28

if (!_isLeapYear() &&
_selectedMonthIndex == 1 &&
_selectedDayIndex == 28) {
_selectedDayIndex = 27;
}
break;
}
setState(() {});
widget.onSelectedItemChanged(_selectedDate);
}

6. Check for disabled values

Check if a picker item is disabled as it exists outside the min-max date range.

/// check if the given day, month or year index is disabled
bool _isDisabled(int index, _SelectorType type) {
DateTime temp;
switch (type) {
case _SelectorType.day:
temp = DateTime(
_minDate.year + _selectedYearIndex,
_selectedMonthIndex + 1,
index + 1,
);
break;
case _SelectorType.month:
temp = DateTime(
_minDate.year + _selectedYearIndex,
index + 1,
_selectedDayIndex + 1,
);
break;
case _SelectorType.year:
temp = DateTime(
_minDate.year + index,
_selectedMonthIndex + 1,
_selectedDayIndex + 1,
);
break;
}
return temp.isAfter(_maxDate) || temp.isBefore(_minDate);
}

7. Create a common CupertinoPicker widget.

In this step, we’ll create a method for the common CupertinoPicker widget, which will be later used for days, months & years picker.

Widget _selector({
required List<dynamic> values,
required int selectedValueIndex,
required bool Function(int) isDisabled,
required void Function(int) onSelectedItemChanged,
required FixedExtentScrollController scrollController,
}) {
return CupertinoPicker.builder(
childCount: values.length,
squeeze: widget.squeeze,
itemExtent: widget.itemExtent,
scrollController: scrollController,
useMagnifier: widget.useMaginifier,
diameterRatio: widget.diameterRatio,
magnification: widget.magnification,
backgroundColor: widget.backgroundColor,
offAxisFraction: widget.offAxisFraction,
selectionOverlay: widget.selectionOverlay,
onSelectedItemChanged: onSelectedItemChanged,
itemBuilder: (context, index) => Container(
height: widget.itemExtent,
alignment: Alignment.center,
child: Text(
'${values[index]}',
style: index == selectedValueIndex
? widget.selectedStyle
: isDisabled(index)
? widget.disabledStyle
: widget.unselectedStyle,
),
),
);
}

You can see our “_selector” method has some parameters. Their purpose is:

values: list of items to be displayed in the CupertinoPicker, for example, list of months [“Jan”, “Feb”, …].
Must be able to interpolate in the string ‘${values[index]}’ (for now) or you can tweak the code accordingly.

selectedValueIndex: index of selected item from “values”.

isDisabled: callback function to determine if an item is disabled, in case it is out of min-max date range.

onSelectedItemChanged: callback function when CupertinoPicker selection changes.

scrollController: scroll controller for CupertinoPicker.

8. Add picker for days

Widget _daySelector() {
return _selector(
values: List.generate(_numberOfDays(), (index) => index + 1),
selectedValueIndex: _selectedDayIndex,
scrollController: _dayScrollController,
isDisabled: (index) => _isDisabled(index, _SelectorType.day),
onSelectedItemChanged: (v) => _onSelectedItemChanged(
v,
_SelectorType.day,
),
);
}

9. Add picker for months

Widget _monthSelector() {
return _selector(
values: _months,
selectedValueIndex: _selectedMonthIndex,
scrollController: _monthScrollController,
isDisabled: (index) => _isDisabled(index, _SelectorType.month),
onSelectedItemChanged: (v) => _onSelectedItemChanged(
v,
_SelectorType.month,
),
);
}

10. Add picker for years

Widget _yearSelector() {
return _selector(
values: List.generate(
_maxDate.year - _minDate.year + 1,
(index) => _minDate.year + index,
),
selectedValueIndex: _selectedYearIndex,
scrollController: _yearScrollController,
isDisabled: (index) => _isDisabled(index, _SelectorType.year),
onSelectedItemChanged: (v) => _onSelectedItemChanged(
v,
_SelectorType.year,
),
);
}

11. Combine all three pickers for days, months & years

@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(child: _monthSelector()),
Expanded(child: _daySelector()),
Expanded(child: _yearSelector()),
],
);
}

And that’s it! Now we have our own custom CupertinoDatePicker.

You can now use custom CupertinoDatePicker like this.

class CustomCupertinoPickerApp extends StatefulWidget {
const CustomCupertinoPickerApp({Key? key}) : super(key: key);
@override
State<CustomCupertinoPickerApp> createState() =>
_CustomCupertinoPickerAppState();
}
class _CustomCupertinoPickerAppState extends
State<CustomCupertinoPickerApp> {
late final DateTime _minDate;
late final DateTime _maxDate;
late DateTime _selecteDate;
@override
void initState() {
super.initState();
final currentDate = DateTime.now();
_minDate = DateTime(
currentDate.year - 100,
currentDate.month,
currentDate.day,
);
_maxDate = DateTime(
currentDate.year - 18,
currentDate.month,
currentDate.day,
);
_selecteDate = _maxDate;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
height: 300,
child: CustomCupertinoDatePicker(
itemExtent: 50,
minDate: _minDate,
maxDate: _maxDate,
selectedDate: _selecteDate,
selectionOverlay: Container(
width: double.infinity,
height: 50,
decoration: const BoxDecoration(
border: Border.symmetric(
horizontal:BorderSide(color:Colors.grey,width:1),
),
),
),
selectedStyle: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 24,
),
unselectedStyle: TextStyle(
color: Colors.grey[800],
fontSize: 18,
),
disabledStyle: TextStyle(
color: Colors.grey[400],
fontSize: 18,
),
onSelectedItemChanged: (date) => _selecteDate = date,
),
),
),
),
);
}
}

Thank you for reading this article. Hope it helped you. :-)

Suggestions are welcomed!

Disclaimer: This article only gives an idea on how we can create a custom CupertinoDatePicker, and may not be in optimized form.

--

--