ujs_date_picker
A Flutter package for picking dates as DD/MM/YYYY and times as HH:mm.
It ships five date views — spin wheels, a calendar grid, dropdowns, a typed field, and a start/end range — and three time views: spin wheels, dropdowns, and a typed field. Two themes style all of them. Every date view uses the same selection engine, so leap years, month lengths, and the allowed range behave the same no matter which picker is on screen.
import 'package:ujs_date_picker/ujs_date_picker.dart';
CustomDateField(
label: 'Travel date',
type: DatePickerType.calendar,
theme: CustomDatePickerThemeData.material,
firstDate: DateTime(2000),
lastDate: DateTime.now(),
onChanged: (date) {
if (date == null) return;
print(formatDdMmYyyy(date)); // 09/10/2026
},
);
CustomTimeField(
label: 'Departure',
type: TimePickerType.spin,
use24Hour: true,
minuteInterval: 15,
onChanged: (time) {
if (time == null) return;
print(formatHhMm(time)); // 14:30
},
);
How to run
The package itself has no main.dart. You run the demo app in example/. That app depends on this package with a path dependency, so changes in lib/ show up in the demo.
What you need
- Flutter stable, with Dart
^3.11.1 - A device: an Android emulator, an iOS simulator, macOS, or Chrome
Check the toolchain:
flutter doctor
Run the demo
From the package root (date Picker/):
cd example
flutter pub get
flutter devices
flutter run
flutter devices lists what is connected. flutter run picks one. To choose it yourself:
flutter run -d chrome
flutter run -d macos
flutter run -d <device-id>
The first launch compiles the app. After it is open:
- Press
rin the terminal for a hot reload - Press
Rfor a hot restart - Press
qto quit
What you should see
The demo title is Date picker. Chips along the top switch the view:
| Chip | What opens |
|---|---|
spin |
Day, month, and year wheels |
calendar |
Month grid |
dropdown |
Three menus |
input |
A field you type into as DD/MM/YYYY |
range |
Start date and end date |
Minimal and Material switch the colors. A Time section below the date uses the same themes. Its chips are spin, dropdown, and input, plus 24-hour and 12-hour.
Pick a date and a time, then tap Save. The formatted values appear under the fields and in a snackbar, for example 09/10/2026 14:30.
Run the tests
From the package root, not from example/:
flutter test
To run the demo's own smoke test:
cd example
flutter test
Those tests cover leap years, February clamping, the DD/MM/YYYY format, a reversed range, and each date picker: spin confirm, a calendar day tap, the January-to-February dropdown clamp, the input mask and the 31/02/2026 error, and a range that swaps its ends. The time tests cover 24-hour and 12-hour formatting, minute snapping, spin confirm, the Now button, a dropdown hour change, the HH:mm mask, 25:00, an off-step minute, and the AM/PM control.
How to use this in a project
Copy the dependency into the other app. Copy the widgets into that app's Dart files. Leave this folder where it is.
1. Add the dependency
After the package is on pub.dev, open the other app's pubspec.yaml:
dependencies:
flutter:
sdk: flutter
ujs_date_picker: ^0.1.1
To use this folder before it is published, point path at it. If that app sits next to this folder:
dependencies:
flutter:
sdk: flutter
ujs_date_picker:
path: ../date Picker
path is relative to the app. If the package lives somewhere else, point path at that folder. The folder name contains a space, so quote it in the shell (cd "date Picker"). In YAML the path works with or without quotes.
From the other app:
flutter pub get
2. Import the public library
import 'package:ujs_date_picker/ujs_date_picker.dart';
That import is the whole public API: the fields, the sheet functions, the themes, and the format helpers. The widgets use Material, so the page sits under a MaterialApp.
3. Put date and time on a page
This page is enough to save a travel date and a departure time. onChanged receives the new value, or null when the user taps the clear icon. validator runs when Save calls validate.
import 'package:ujs_date_picker/ujs_date_picker.dart';
import 'package:flutter/material.dart';
class BookingPage extends StatefulWidget {
const BookingPage({super.key});
@override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
final _formKey = GlobalKey<FormState>();
DateTime? _date;
TimeOfDay? _time;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(24),
children: [
CustomDateField(
label: 'Travel date',
type: DatePickerType.calendar,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 1, 12, 31),
theme: CustomDatePickerThemeData.minimal,
onChanged: (date) => setState(() => _date = date),
validator: (value) => value == null ? 'Select a date' : null,
),
const SizedBox(height: 20),
CustomTimeField(
label: 'Departure',
type: TimePickerType.spin,
use24Hour: true,
minuteInterval: 15,
theme: CustomDatePickerThemeData.minimal,
onChanged: (time) => setState(() => _time = time),
validator: (value) => value == null ? 'Select a time' : null,
),
const SizedBox(height: 20),
Text(_date == null ? 'No date yet' : formatDdMmYyyy(_date!)),
Text(_time == null ? 'No time yet' : formatHhMm(_time!)),
const SizedBox(height: 20),
FilledButton(
onPressed: () {
if (!_formKey.currentState!.validate()) return;
debugPrint('${formatDdMmYyyy(_date!)} ${formatHhMm(_time!)}');
},
child: const Text('Save'),
),
],
),
),
),
);
}
}
firstDate and lastDate are inclusive. Leaving them out allows 1 January 1900 through 31 December 2100. A returned date is date-only: the time component is midnight. A returned time is a TimeOfDay with an hour from 0 to 23 and a minute from 0 to 59.
4. One date: CustomDateField
CustomDateField is a FormField<DateTime>. type chooses the view.
| Argument | Default | What it does |
|---|---|---|
label |
'Date' |
Text above the field |
type |
DatePickerType.spin |
spin, calendar, dropdown, or input |
initialValue |
null |
Date already filled in when the field is created |
firstDate |
1 Jan 1900 | Earliest allowed day, inclusive |
lastDate |
31 Dec 2100 | Latest allowed day, inclusive |
theme |
CustomDatePickerThemeData.minimal |
Colors, sizes, and button labels |
onChanged |
null |
Called with the DateTime, or null when cleared |
validator |
null |
Returns an error string, or null when the value is valid |
autovalidateMode |
null |
When Flutter runs validator |
CustomDateField(
label: 'Date of birth',
type: DatePickerType.calendar,
initialValue: DateTime(1998, 4, 12),
firstDate: DateTime(1900, 1, 1),
lastDate: DateTime.now(),
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: (date) => setState(() => _date = date),
validator: (value) => value == null ? 'Select a date' : null,
)
Switch the view by changing type only:
type |
What the user gets | Value |
|---|---|---|
DatePickerType.spin |
Day, month, and year wheels. Today, Confirm, Cancel. | DateTime |
DatePickerType.calendar |
A Monday-first month grid. | DateTime |
DatePickerType.dropdown |
Three menus: day, month, year. | DateTime |
DatePickerType.input |
A text field. 09102026 becomes 09/10/2026. |
DateTime |
DatePickerType.range belongs on CustomDateRangeField. Putting it on CustomDateField fails an assert.
The input field shows Enter a real date for 31/02/2026 and Date is out of range when the day is outside firstDate / lastDate. Spin, calendar, and dropdown snap an impossible day back onto a real day inside the range. February 31 becomes 28 February, or 29 in a leap year.
5. A start and an end: CustomDateRangeField
CustomDateRangeField is a FormField<DateTimeRange>. It always opens the range calendar.
| Argument | Default | What it does |
|---|---|---|
label |
'Date range' |
Text above the field |
initialValue |
null |
A DateTimeRange already filled in |
firstDate |
1 Jan 1900 | Earliest allowed day, inclusive |
lastDate |
31 Dec 2100 | Latest allowed day, inclusive |
theme |
Minimal | Same theme object as the date field |
onChanged |
null |
Called with the range, or null when cleared |
validator |
null |
Form error text |
autovalidateMode |
null |
When Flutter runs validator |
DateTimeRange? _trip;
CustomDateRangeField(
label: 'Trip',
initialValue: DateTimeRange(
start: DateTime(2026, 10, 9),
end: DateTime(2026, 10, 12),
),
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 1, 12, 31),
onChanged: (range) => setState(() => _trip = range),
validator: (value) => value == null ? 'Select a range' : null,
)
Read the two ends with formatDdMmYyyy:
Text(
_trip == null
? 'Pick a trip'
: '${formatDdMmYyyy(_trip!.start)} – ${formatDdMmYyyy(_trip!.end)}',
)
The first tap sets the start. The second tap sets the end. Confirm stays disabled until both days are set. If the second tap is earlier than the first, start is the earlier day and end is the later day.
6. A time: CustomTimeField
CustomTimeField is a FormField<TimeOfDay>.
| Argument | Default | What it does |
|---|---|---|
label |
'Time' |
Text above the field |
type |
TimePickerType.spin |
spin, dropdown, or input |
initialValue |
null |
TimeOfDay already filled in |
use24Hour |
true |
false shows a 12-hour clock with AM/PM |
minuteInterval |
1 |
Step size. Must be a positive divisor of 60: 1, 5, 10, 15, 20, 30 |
theme |
Minimal | Same theme object as the date fields |
onChanged |
null |
Called with the TimeOfDay, or null when cleared |
validator |
null |
Form error text |
autovalidateMode |
null |
When Flutter runs validator |
CustomTimeField(
label: 'Departure',
type: TimePickerType.dropdown,
initialValue: const TimeOfDay(hour: 9, minute: 30),
use24Hour: false,
minuteInterval: 15,
onChanged: (time) => setState(() => _time = time),
validator: (value) => value == null ? 'Select a time' : null,
)
type |
What the user gets | Shown as |
|---|---|---|
TimePickerType.spin |
Hour and minute wheels. Now, Confirm, Cancel. | 14:30 or 02:30 PM |
TimePickerType.dropdown |
Hour and minute menus. A third menu is AM/PM when use24Hour is false. |
same |
TimePickerType.input |
A text field. 0930 becomes 09:30. In 12-hour mode, AM/PM sits under the field. |
same |
formatHhMm(_time!) prints 14:30. formatHhMm(_time!, use24Hour: false) prints 02:30 PM. The stored hour is still 0–23. Midnight is 00:00 / 12:00 AM. Noon is 12:00 / 12:00 PM.
Spin and dropdown snap to minuteInterval. The typed field keeps an off-step minute on screen and shows Use a 15-minute step. 25:00 shows Enter a real time. Any other interval, such as 7, throws ArgumentError.
7. Open a sheet from your own button
Use these when the page already has a button and only needs the sheet. Cancel returns null.
One date. type is spin, calendar, or dropdown:
final date = await showCustomDatePicker(
context: context,
type: DatePickerType.calendar,
initialDate: DateTime(2026, 10, 9),
firstDate: DateTime(2020, 1, 1),
lastDate: DateTime(2030, 12, 31),
theme: CustomDatePickerThemeData.material,
);
if (date != null) {
debugPrint(formatDdMmYyyy(date));
}
| Argument | Default | What it does |
|---|---|---|
context |
required | The button's BuildContext |
type |
DatePickerType.spin |
spin, calendar, or dropdown |
initialDate |
today | Day the sheet opens on |
firstDate |
1 Jan 1900 | Earliest allowed day |
lastDate |
31 Dec 2100 | Latest allowed day |
theme |
Minimal | Sheet colors and button labels |
showCustomDatePicker throws ArgumentError when type is input or range. Typed dates use CustomDateField with DatePickerType.input. A range uses the next function.
A start and an end:
final range = await showCustomDateRangePicker(
context: context,
initialStart: DateTime(2026, 10, 1),
initialEnd: DateTime(2026, 10, 9),
firstDate: DateTime(2026, 1, 1),
lastDate: DateTime(2026, 12, 31),
theme: CustomDatePickerThemeData.minimal,
);
initialStart and initialEnd are optional. Pass theme, firstDate, and lastDate the same way as the single-date sheet.
A time. type is spin or dropdown:
final time = await showCustomTimePicker(
context: context,
type: TimePickerType.spin,
initialTime: const TimeOfDay(hour: 9, minute: 30),
use24Hour: true,
minuteInterval: 5,
theme: CustomDatePickerThemeData.material,
);
if (time != null) {
debugPrint(formatHhMm(time));
}
| Argument | Default | What it does |
|---|---|---|
context |
required | The button's BuildContext |
type |
TimePickerType.spin |
spin or dropdown |
initialTime |
now | Time the sheet opens on, snapped to minuteInterval |
use24Hour |
true |
false adds AM/PM |
minuteInterval |
1 |
Positive divisor of 60 |
theme |
Minimal | Sheet colors and the Now label |
showCustomTimePicker throws ArgumentError when type is TimePickerType.input.
On the web, a field waits briefly before opening its sheet so the same click does not land on the barrier and dismiss it.
8. Validate date, range, and time together
All three fields are Flutter form fields. One Form and one GlobalKey<FormState> check every validator.
final _formKey = GlobalKey<FormState>();
DateTime? _date;
DateTimeRange? _range;
TimeOfDay? _time;
Form(
key: _formKey,
child: Column(
children: [
CustomDateField(
label: 'Date of birth',
type: DatePickerType.input,
lastDate: DateTime.now(),
onChanged: (value) => _date = value,
validator: (value) => value == null ? 'Select a date' : null,
),
CustomDateRangeField(
label: 'Stay',
onChanged: (value) => _range = value,
validator: (value) => value == null ? 'Select a range' : null,
),
CustomTimeField(
label: 'Check-in time',
type: TimePickerType.input,
use24Hour: true,
onChanged: (value) => _time = value,
validator: (value) => value == null ? 'Select a time' : null,
),
FilledButton(
onPressed: () {
if (!_formKey.currentState!.validate()) return;
debugPrint(formatDdMmYyyy(_date!));
debugPrint(
'${formatDdMmYyyy(_range!.start)} – ${formatDdMmYyyy(_range!.end)}',
);
debugPrint(formatHhMm(_time!));
},
child: const Text('Save'),
),
],
),
)
autovalidateMode: AutovalidateMode.onUserInteraction shows the error as soon as the user changes the field. The clear icon sets the value to null and calls onChanged(null), so a required-field validator fails again until a new value is chosen.
9. Format, parse, and snap
Dates:
formatDdMmYyyy(DateTime(2026, 10, 9)); // 09/10/2026
formatDdMmYyyy(DateTime(2026, 10, 9), separator: '-'); // 09-10-2026
tryParseDdMmYyyy('09/10/2026'); // DateTime(2026, 10, 9)
tryParseDdMmYyyy('31/02/2026'); // null
daysInMonth(2026, 2); // 28
isLeapYear(2000); // true
isLeapYear(1900); // false
dateOnly(DateTime.now()); // today at midnight
clampDate and clampDateParts pull a raw day, month, and year onto a real day inside a range. monthNames and monthAbbreviations are the English month lists. DdMmYyyyInputFormatter is the slash mask the input field uses. Put it on your own TextField when you want the same mask.
Times:
const afternoon = TimeOfDay(hour: 14, minute: 30);
formatHhMm(afternoon); // 14:30
formatHhMm(afternoon, use24Hour: false); // 02:30 PM
tryParseHhMm('14:30'); // 14:30
tryParseHhMm('02:30 PM'); // 14:30
tryParseHhMm('12:00 AM'); // 00:00
tryParseHhMm('25:00'); // null
normalizeTime(const TimeOfDay(hour: 10, minute: 7), minuteInterval: 15);
// 10:00
normalizeTime(const TimeOfDay(hour: 23, minute: 58), minuteInterval: 15);
// 00:00
clockHour(14) is 2. hour24FromClock(2, DayPeriod.pm) is 14. HhMmInputFormatter is the colon mask. Typing 1430 through that formatter yields 14:30.
10. Colors, size, and labels
Pass the same theme to every field and to every show* function. Start from a preset and override the parts you care about. Anything left out keeps the preset. The full list of knobs is in Themes.
final theme = CustomDatePickerThemeData.minimal.copyWith(
accent: const Color(0xFF0B6E4F),
confirmBackground: const Color(0xFF0B6E4F),
confirmForeground: Colors.white,
buttonHeight: 56,
buttonRadius: 8,
confirmText: 'Done',
cancelText: 'Close',
todayText: 'Today',
nowText: 'Current',
);
CustomDateField(theme: theme, onChanged: (date) {});
CustomTimeField(theme: theme, onChanged: (time) {});
CustomDatePickerThemeData.material is the Material 3 skin. todayText is the date sheet's shortcut. nowText is the time sheet's shortcut.
Picker types
DatePickerType chooses the view. A single date is a DateTime. A range is a DateTimeRange. Both are date-only values: the time component is midnight.
| Type | Widget | What the user does | Result |
|---|---|---|---|
spin |
CustomDateField |
Scroll day, month, and year wheels | DateTime |
calendar |
CustomDateField |
Tap a day on a month grid | DateTime |
dropdown |
CustomDateField |
Choose day, month, and year from menus | DateTime |
input |
CustomDateField |
Type the date | DateTime |
range |
CustomDateRangeField |
Tap a start day, then an end day | DateTimeRange |
DatePickerType.range is not a valid type for CustomDateField. Use CustomDateRangeField for a range.
Spin
Three wheels labeled DD, MM, and YYYY. Month rows read 05 May. The highlighted row is the current choice, shown above the wheels as 09 / 10 / 2026.
Scrolling onto a day the month does not have, such as 31 while February is selected, snaps back to the last real day. Dates outside firstDate and lastDate snap back into range. Today jumps all three wheels to the current day, clamped to that range. Confirm returns the date. Cancel returns null.
Calendar
A month grid. The week starts on Monday (Mo through Su). Previous and next arrows move the month, and they stop at the first and last allowed months.
Days that belong to the neighboring month are dimmed. Days outside firstDate and lastDate cannot be tapped. Tap a day, then Confirm. Today selects the current day and shows its month.
Dropdown
Three menus in one sheet: DD (01–31), MM (01–12, with the short month name), and YYYY.
The day menu only lists days that exist. Choosing February while the day is 31 changes the day to 28, or 29 in a leap year. Menus also omit months and days outside the allowed range.
Input
The field itself is the picker. There is no sheet. Typing 09102026 is masked to 09/10/2026. A finished value that is not a real day, such as 31/02/2026, stays in the field and shows Enter a real date. A real day outside firstDate / lastDate shows Date is out of range. Only a valid in-range date is reported through onChanged.
Range
A calendar used twice. The first tap sets the start and clears any previous end. The second tap sets the end. Days between the two are highlighted, and the sheet title area shows both dates as DD/MM/YYYY – DD/MM/YYYY.
If the second tap is earlier than the first, the engine swaps them so start is always on or before end. Confirm stays disabled until both days are set, then returns a DateTimeRange.
Time picker
A time is a TimeOfDay. The default clock is 24-hour (14:30). Pass use24Hour: false for 02:30 PM.
| Type | Widget | What the user does | Result |
|---|---|---|---|
spin |
CustomTimeField |
Scroll hour and minute wheels | TimeOfDay |
dropdown |
CustomTimeField |
Choose hour and minute from menus | TimeOfDay |
input |
CustomTimeField |
Type the time | TimeOfDay |
minuteInterval defaults to 1. Use 5, 15, or 30 when the choice should land on those minutes. It must divide 60.
Spin
Two wheels labeled HH and MM. In 12-hour mode a third wheel is AM / PM. The highlighted row is the current choice, shown above the wheels as 14:30 or 02:30 PM.
Now jumps the wheels to the current time, snapped to minuteInterval. Confirm returns the time. Cancel returns null.
Dropdown
Menus for HH and MM. The minute menu only lists steps of minuteInterval. In 12-hour mode a third menu chooses AM or PM. Now selects the current time.
Input
The field itself is the picker. There is no sheet. Typing 0930 is masked to 09:30. 25:00 stays in the field and shows Enter a real time. A real time whose minute is off the interval, such as 14:10 with a 15-minute step, shows Use a 15-minute step.
In 12-hour mode the field still takes HH:MM from 01 to 12, and an AM / PM control sits under it. 02:30 with PM selected is 14:30.
Themes
Themes are skins. They are not picker types. The same view can be Minimal or Material.
| Theme | Look |
|---|---|
CustomDatePickerThemeData.minimal |
Warm paper sheet, green accent. This is the default. |
CustomDatePickerThemeData.material |
Material 3 purple accent and tonal surfaces. |
Pass theme on the field or on showCustomDatePicker / showCustomDateRangePicker. Start from a preset and override any color, size, or text style with copyWith. A full ButtonStyle replaces the Confirm, Cancel, or Today button when you need more than the built-in knobs.
The class is named CustomDatePickerThemeData so it does not clash with Flutter's DatePickerTheme.
final theme = CustomDatePickerThemeData.minimal.copyWith(
accent: const Color(0xFF0B6E4F),
confirmBackground: const Color(0xFF0B6E4F),
confirmForeground: Colors.white,
cancelForeground: const Color(0xFF0B6E4F),
todayForeground: const Color(0xFF0B6E4F),
buttonHeight: 56,
buttonRadius: 8,
buttonFontSize: 16,
valueFontSize: 24,
previewFontSize: 32,
dayCellSize: 40,
wheelHeight: 220,
iconSize: 28,
sheetRadius: 20,
confirmText: 'Done',
cancelText: 'Close',
);
CustomDateField(
type: DatePickerType.spin,
theme: theme,
)
Anything you do not pass keeps the preset. Useful knobs:
| What you want to change | Theme fields |
|---|---|
| Confirm, Cancel, and Today colors | confirmBackground, confirmForeground, cancelForeground, cancelBorder, todayForeground, disabledButton |
| Button size and shape | buttonHeight, buttonRadius, buttonFontSize, buttonPadding, buttonGap |
| Button label | confirmText, cancelText, todayText, nowText |
| Replace a button's whole style | confirmButtonStyle, cancelButtonStyle, todayButtonStyle |
| Field text and padding | valueFontSize, labelFontSize, captionFontSize, fieldPadding, radius, borderWidth |
| Sheet | sheet, sheetRadius, sheetMargin, sheetPadding |
| Calendar days | dayCellSize, dayGridExtent, dayFontSize, selectedDay, selectedDayText, highlight |
| Spin wheels | wheelHeight, wheelItemExtent, wheelFontSize, wheelSelectedFontSize |
| Icons | iconSize, clearIconSize, iconColor |
labelTextStyle, valueTextStyle, previewTextStyle, buttonTextStyle, and errorTextStyle override the matching font size when you need a full TextStyle.
Use in a form
CustomDateField is a FormField<DateTime>. CustomDateRangeField is a FormField<DateTimeRange>. CustomTimeField is a FormField<TimeOfDay>. The copy-paste page in How to use this in a project puts all three in one Form, with validator, initialValue, and autovalidateMode.
A selected value can be cleared with the close icon on the field. Clearing calls onChanged with null.
showCustomDatePicker, showCustomDateRangePicker, and showCustomTimePicker open a sheet from a button you already have. Argument lists and the values that throw ArgumentError are in step 7. Canceling a sheet returns null.
On the web, the field waits briefly before opening the sheet so the same click does not land on the barrier and dismiss it.
Date rules
The default allowed range is 1 January 1900 through 31 December 2100. Pass firstDate and lastDate to narrow it. Both ends are inclusive.
Rules applied by every view:
- February has 29 days in leap years and 28 otherwise. Years divisible by 100 are not leap years unless they are also divisible by 400 (
1900is not,2000is). - A day past the end of the month is shortened before the range check.
31/02/2026becomes28/02/2026. It does not roll into March. - A date before
firstDatebecomesfirstDate. A date afterlastDatebecomeslastDate. - Returned values are date-only (
DateTime(year, month, day)).
Helpers exported from the package:
| Function | Purpose |
|---|---|
formatDdMmYyyy(date) |
09/10/2026. Optional separator. |
tryParseDdMmYyyy(text) |
Parses dd/MM/yyyy, or returns null when it is not a real day. |
daysInMonth(year, month) |
Length of that month. |
isLeapYear(year) |
Leap-year test. |
dateOnly(date) |
Strips the time. |
clampDate / clampDateParts |
Pull a date onto a real day inside a range. |
DdMmYyyyInputFormatter |
The slash mask used by the date input field. |
formatHhMm(time) |
14:30, or 02:30 PM when use24Hour is false. |
tryParseHhMm(text) |
Parses HH:mm or hh:mm AM / hh:mm PM, or returns null. |
normalizeTime(time) |
Snaps to minuteInterval. Near midnight this can roll to 00:00. |
clockHour(hour24) |
12-hour face. 14 becomes 2. Midnight and noon are 12. |
hour24FromClock(clock, period) |
2 plus DayPeriod.pm becomes 14. |
HhMmInputFormatter |
The colon mask used by the time input field. |
monthNames and monthAbbreviations are also exported.
Architecture
Application
└── Public widgets CustomDateField, CustomDateRangeField, CustomTimeField
├── Selection engine first/last constraints, single date or range, normalization
└── Presentation Minimal or Material theme
└── Views date views, time spin / dropdown / input, sheet chrome
The example app imports only package:ujs_date_picker/ujs_date_picker.dart. A view reports a raw day, month, and year. The engine normalizes that and applies the allowed range. The presentation layer paints the result as DD/MM/YYYY.
lib/
ujs_date_picker.dart Public exports
date_parts.dart Leap years, month lengths, date formatting
time_parts.dart 12/24-hour formatting, minute snapping
src/engine/ Constraints, selection state, selection engine
src/presentation/ Themes, sheet, and the five views
example/ Demo app
Libraries
- date_parts
- Calendar helpers for a day / month / year picker.
- time_parts
- ujs_date_picker