Added calendar delegate to support custom calendar systems (#161874)

Added `CalendarDelegate` class that supports plugging in custom calendar
logics other than Gregorian Calendar System.

Here is an example implementation for Nepali(Bikram Sambat) Calendar
System:
https://github.com/sarbagyastha/nepali_date_picker/blob/m3/lib/src/nepali_calendar_delegate.dart

Demo using the `NepaliDatePickerDelegate`:
https://date.sarbagyastha.com.np/

Fixes https://github.com/flutter/flutter/issues/77531,
https://github.com/flutter/flutter/issues/161873

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
This commit is contained in:
Sarbagya Dhaubanjar
2025-03-08 08:26:17 +05:45
committed by GitHub
parent 83781ae65c
commit 6d6d7914f9
10 changed files with 1095 additions and 108 deletions

View File

@@ -0,0 +1,148 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample demonstrating how to use a custom [CalendarDelegate]
/// with [CalendarDatePicker] to implement a hypothetical calendar system
/// where even-numbered months have 21 days, odd-numbered months have 28 days,
/// and every month starts on a Monday.
void main() => runApp(const CalendarDatePickerApp());
class CalendarDatePickerApp extends StatelessWidget {
const CalendarDatePickerApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CalendarDatePickerExample());
}
}
class CalendarDatePickerExample extends StatefulWidget {
const CalendarDatePickerExample({super.key});
@override
State<CalendarDatePickerExample> createState() => _CalendarDatePickerExampleState();
}
class _CalendarDatePickerExampleState extends State<CalendarDatePickerExample> {
DateTime? selectedDate;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Calendar')),
body: Column(
spacing: 16,
children: <Widget>[
CalendarDatePicker(
initialDate: DateTime(2025, 2, 8),
firstDate: DateTime(2025),
lastDate: DateTime(2026),
onDateChanged: (DateTime pickedDate) {
setState(() {
selectedDate = pickedDate;
});
},
calendarDelegate: const CustomCalendarDelegate(),
),
const Divider(height: 1),
Text(
selectedDate != null
? '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'
: 'No date selected',
),
],
),
);
}
}
/// A custom calendar system where even-numbered months have 21 days,
/// odd-numbered months have 28 days, and every month starts on a Monday.
///
/// This hypothetical calendar follows a fixed structure:
/// - **Even-numbered months (2, 4, 6, etc.)** always have **21 days**.
/// - **Odd-numbered months (1, 3, 5, etc.)** always have **28 days**.
/// - **The first day of every month is always a Monday**, ensuring a consistent weekly alignment.
class CustomCalendarDelegate extends CalendarDelegate<DateTime> {
const CustomCalendarDelegate();
@override
int getDaysInMonth(int year, int month) {
return month.isEven ? 21 : 28;
}
@override
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
return 1;
}
// ------------------------------------------------------------------------
// All the implementations below are based on the Gregorian calendar system.
@override
DateTime now() => DateTime.now();
@override
DateTime dateOnly(DateTime date) => DateUtils.dateOnly(date);
@override
int monthDelta(DateTime startDate, DateTime endDate) => DateUtils.monthDelta(startDate, endDate);
@override
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
return DateUtils.addMonthsToMonthDate(monthDate, monthsToAdd);
}
@override
DateTime addDaysToDate(DateTime date, int days) => DateUtils.addDaysToDate(date, days);
@override
DateTime getMonth(int year, int month) => DateTime(year, month);
@override
DateTime getDay(int year, int month, int day) => DateTime(year, month, day);
@override
String formatMonthYear(DateTime date, MaterialLocalizations localizations) {
return localizations.formatMonthYear(date);
}
@override
String formatMediumDate(DateTime date, MaterialLocalizations localizations) {
return localizations.formatMediumDate(date);
}
@override
String formatShortMonthDay(DateTime date, MaterialLocalizations localizations) {
return localizations.formatShortMonthDay(date);
}
@override
String formatShortDate(DateTime date, MaterialLocalizations localizations) {
return localizations.formatShortDate(date);
}
@override
String formatFullDate(DateTime date, MaterialLocalizations localizations) {
return localizations.formatFullDate(date);
}
@override
String formatCompactDate(DateTime date, MaterialLocalizations localizations) {
return localizations.formatCompactDate(date);
}
@override
DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) {
return localizations.parseCompactDate(inputString);
}
@override
String dateHelpText(MaterialLocalizations localizations) {
return localizations.dateHelpText;
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/date_picker/custom_calendar_date_picker.0.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
Text getLastDayText(WidgetTester tester) {
final Finder dayFinder = find.descendant(of: find.byType(Ink), matching: find.byType(Text));
return tester.widget(dayFinder.last);
}
testWidgets('Days are based on the calendar delegate', (WidgetTester tester) async {
await tester.pumpWidget(const example.CalendarDatePickerApp());
final Finder nextMonthButton = find.byIcon(Icons.chevron_right);
Text lastDayText = getLastDayText(tester);
expect(find.text('February 2025'), findsOneWidget);
expect(lastDayText.data, equals('21'));
await tester.tap(nextMonthButton);
await tester.pumpAndSettle();
lastDayText = getLastDayText(tester);
expect(find.text('March 2025'), findsOneWidget);
expect(lastDayText.data, equals('28'));
await tester.tap(nextMonthButton);
await tester.pumpAndSettle();
lastDayText = getLastDayText(tester);
expect(find.text('April 2025'), findsOneWidget);
expect(lastDayText.data, equals('21'));
await tester.tap(nextMonthButton);
await tester.pumpAndSettle();
lastDayText = getLastDayText(tester);
expect(lastDayText.data, equals('28'));
});
}