NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #2671 most downloaded on pub.dev
A modern, maintained Flutter plugin for reading and writing device calendar events on Android and iOS.
Last release 6 days ago
02 Oct 2026
Ships fairly regularly
a new release about every 6 weeks
Nearly every release is documented
notes for 21 of 21 stable releases
Nothing withdrawn
no release was ever pulled
11 months old
21 releases · first in 2025
One column per month.
updateRecurring refuses a start move to another day with no new recurrenceRule unless the existing rule generates both the day the occurrence moves to
updateRecurring refuses a start move to another day with no newrecurrenceRule unless the existing rule generates both the day theDeviceCalendarException(invalidArguments). A move onto another day theEventSpan.allEvents. WiththisAndFollowing a move to another day still throws while the rule pinsupdateRecurring on an all-day series counts duration (or the keptstart moves) in calendar days, so a multi-day all-dayupdateEvent and updateRecurring no longer add an extra day whenduration or end gave three days. It'sBreaking: updateEvent and deleteEvent act on one thing — a one-off event or a single occurrence — and take instanceId instead of eventId . A bare ID o
Breaking: updateEvent and deleteEvent act on one thing — a one-off
event or a single occurrence — and take instanceId instead of eventId.
A bare ID of a recurring series is refused with
DeviceCalendarException(invalidArguments) and nothing is written. Moving a
series' start through updateEvent used to shift the whole series and drop
every earlier occurrence without an error; series-wide changes now go
through updateRecurring / deleteRecurring (#175).
Migrating:
updateEvent(eventId: event.instanceId, …) →updateEvent(instanceId: event.instanceId, …). Same for deleteEvent.instanceId equals eventId, so nothing elseupdateEvent(eventId: seriesId, …) on a recurring event →updateRecurring(seriesId, EventSpan.allEvents, …). startDate /endDate map to start / duration; the other fields are the same.deleteEvent(eventId: seriesId) on a recurring event →deleteRecurring(seriesId, EventSpan.allEvents).updateRecurring takes reminders (a Patch<List<Duration>>), to set orupdateEvent does for one event (#175).Recurring events
updateRecurring with thisAndFollowing on an event that doesn't repeatinvalidArguments on both platforms, and nothing isupdateRecurring lands on the dayupdateRecurring keeps everyupdateRecurring refuses a start move with no new rule when it breaksBYMONTH=11;BYDAY=4TH series could be moved to a Thursday in DecembercreateEvent and updateRecurring refuse a recurrence rule whose FREQDAILY/WEEKLY/MONTHLY/YEARLY, or that is malformed, withinvalidArguments and write nothing, on both platforms. iOS createEventFREQ=HOURLYupdateEvent or deleteEvent on one occurrence of a series on aNative modals
showEventModal, showCreateEventModal) alwaysawait hanging or crash (#123):
DeviceCalendarException(operationFailed) straight away. It used toDeviceCalendarException(operationFailed), as openAppSettingsPlatformException.showEventModal on an event that doesn't exist throwsDeviceCalendarException(notFound), as iOS does. It used to open thePermissions
writeOnly rather than notDetermined, so a createEvent straight afterMinimum supported SDK is now Flutter 3.44 / Dart 3.12. Android migrated to Flutter's built-in Kotlin, so the KGP deprecation warning no longer prints…
flutter build (#133).Recurring events
updateRecurring with a new rule anchors the series on the first day thatinvalidArguments and the series is left untouched (#140).updateEvent or deleteEvent on a single occurrence of adeleteRecurring with thisAndFollowing also removes anupdateRecurring with thisAndFollowing carries an occurrence ongetEvent resolves all-day recurring instance IDs (it alwaysSynced calendars
Listing events
listEvents returns an all-day event when the window is a sub-daylistEvents rejects an endDate before startDate with ArgumentError,endDate == startDate) with no events on both platforms (#162).Calendars
updateCalendar and deleteCalendar throw the documented readOnly for aoperationFailed; Android renamed or deleted any row itcreateCalendar refuses a non-local accountType with readOnly,listSources already reports and iOS already does for non-creatablecreateCalendar / updateCalendar throw ArgumentError for a colorHex#RRGGBB (the # optional) instead of storing it silently as#RRGGBB to the platform (#126).deleteCalendar('') throws ArgumentError, like the other mutations (#126).listCalendars no longer crashes on a provider row with a NULLCalendarSource.supportsCalendarCreation, CreateCalendarOptionsIos andCreateCalendarOptionsAndroid describe what the code actually does: iOSPlatform packages: device_calendar_plus_android 0.8.0, device_calendar_plus_ios 0.8.0.
iOS: a grant is honoured immediately. The first time a user allowed access, the very next call could still fail with permissionDenied until the app wa
permissionDenied until the app was
restarted, because EventKit briefly kept reporting notDetermined after the
grant (#134).showCreateEventModal needs no calendar permission on Android or iOS 17+ —
the system editor saves with its own access — so it now works as a fallback
after a denial, and autoPermissions never prompts for it. On iOS 16 and
below the in-process editor still requires full access (#121, #141).permissionDenied instead of a silent empty
result when READ_CALENDAR isn't held, and full-tier mutations require both
READ_CALENDAR and WRITE_CALENDAR, matching iOS (#121).createEvent with a named calendarId under write-only access reports
permissionDenied (with a hint) instead of a misleading notFound (#121).CalendarPermissionStatus.denied spells out the difference between the
permanent denial hasPermissions reports and the just-declined prompt
requestPermissions reports.Per-event color lands, plus one rename to swallow.
Per-event color lands, plus one rename to swallow.
Event.colorHex — the event's custom color as #RRGGBB, with a parsed Event.color getter (read-only, mirrors Calendar.colorHex). Android reads it from EVENT_COLOR; null when the event just uses its calendar's color, and always null on iOS (EventKit has no per-event colors). No write support (#117).WeeklyRecurrence's wkst field and constructor parameter are renamed to weekStart, matching the spelled-out naming of the other recurrence fields. The emitted RRULE is unchanged (still WKST=). Update WeeklyRecurrence(wkst: …) call sites and .wkst reads to weekStart.Ships device_calendar_plus 0.8.0 and device_calendar_plus_android 0.7.1. The platform interface and iOS packages are unchanged.
Event.colorHex — the event's custom color as #RRGGBB, with a parsed
Event.color getter (read-only, mirrors Calendar.colorHex). Android reads
the event's custom color (EVENT_COLOR), null when the event uses its
calendar's color; iOS always reports null (EventKit has no per-event
color). No write support (#117).WeeklyRecurrence's wkst field and constructor parameter are
renamed to weekStart, matching the spelled-out naming of the other
recurrence fields (daysOfWeek, daysOfMonth, setPositions, …). The
emitted RRULE is unchanged (still uses the WKST= token). Update
WeeklyRecurrence(wkst: …) call sites and .wkst reads to weekStart.Docs + packaging polish. No code or behaviour changes.
Docs + packaging polish. No code or behaviour changes.
device pub.dev topic (dropped federated to stay within the five-topic limit).doc/.device pub.dev topic (dropped federated to stay within the
five-topic limit).doc/. Trimmed the API
doc comments to the consumer-facing contract and removed native-API
implementation details. No code or behavior changes.Feature release. App-facing API is additive; device_calendar_plus_platform_interface has breaking signature changes for platform implementers.
Feature release. App-facing API is additive; device_calendar_plus_platform_interface has breaking signature changes for platform implementers.
requestPermissions(level: CalendarAccessLevel.writeOnly) for the gentler add-only prompt (#89).DeviceCalendar.instance.autoPermissions = AutoPermissionMode.asNeeded | .full; methods request access on first use (#90).calendarId — omit it on createEvent to write to the default calendar (#88).reminders: List<Duration> on create, Patch<List<Duration>> on update, Event.reminders read back; minute-granular relative offsets (#87).permissionDenied (not "no writable calendar") when a default can't be resolved (#112); tighter denial reporting (#108).All four packages bumped 0.6.0 → 0.7.0 in lockstep.
requestPermissions(level: CalendarAccessLevel.writeOnly) asks for the gentler add-only prompt and a
grant reports CalendarPermissionStatus.writeOnly. On iOS this is not a
permanent ceiling — a later full request re-prompts and upgrades the app
in-app (#89).DeviceCalendar.instance.autoPermissions
to AutoPermissionMode.asNeeded or .full and methods request the access
they need on first use instead of throwing when permission is undetermined
(#90).createEvent's calendarId is now optional — omit it to write to the
platform's default calendar (iOS defaultCalendarForNewEvents, Android the
primary or first writable calendar). Resolving the default on Android reads
the calendar list, so that path needs full access (#88).createEvent takes reminders: List<Duration>,
updateEvent takes Patch<List<Duration>>, and Event.reminders is read
back. Relative before-start offsets, normalized to whole minutes on both
platforms (#87).chore(release): prepare 0.6.0
chore(release): prepare 0.6.0
updateRecurring() now takes start: DateTime instead of
startTime: EventTimeOfDay. start is the anchored occurrence's new start;
the whole scope translates by the wall-clock delta, so a single call can move
the time and the day together — a nightshift 11 PM → 1 AM (crosses
midnight) or a weekly meeting Monday → Tuesday. The delta is measured in the
event's timezone, so it is DST-safe. (#103, thanks @SuperKrallan)EventTimeOfDay is removed.start (only its date is used)
instead of throwing.updateRecurring() translates implicit-day rules for free: a
WeeklyRecurrence() / MonthlyRecurrence() with no pinned day follows the
anchor when you move the day.daysOfWeek,
daysOfMonth, positional) without also passing a recurrenceRule throws
DeviceCalendarException(invalidArguments). Moving one day of a multi-day
rule is genuinely ambiguous (Mon of Mon/Wed/Fri → Tue could mean Tue/Wed/Fri
or Tue/Thu/Sat), so the API hands the decision back to you. Time-only,
duration-only and whole-week shifts never throw.No-op updates are now valid instead of throwing. updateEvent, updateRecurring and updateCalendar return without a platform write when no fields are pr
updateEvent,
updateRecurring and updateCalendar return without a platform write when
no fields are provided, so "save with no edits" is a harmless no-op rather
than an ArgumentError (#95). updateRecurring returns the targeted scope's
event id. updateRecurring's duration now accepts zero (an instantaneous
event); only a negative duration is rejected.BYMONTHDAY (e.g. -1 for the last
day of the month) instead of rejecting the rule (#91)thisAndFollowing +
Patch.clear() now splits the series on iOS instead of collapsing the whole
series into one event (#93) — see the device_calendar_plus_ios changeloglistEvents returns every event across spans longer than ~4 years without
dropping or duplicating recurring instances (iOS, #94), and includes
zero-duration events sitting exactly on the query start (Android, #416) — see
the platform changelogscreateCalendar fails with a clear error on iOS sources that can't hold
calendars, instead of an opaque failure (#96) — see the
device_calendar_plus_ios changelogdevice_calendar_plus_ios changeloglistEvents per-instance expansion and the eventId@timestamp
instanceId format (#97)iOS: fix showEventModal(edit: true) crash
showEventModal(edit: true) no longer crashes (#77) — see the
device_calendar_plus_ios 0.5.1 changelogshowEventModal docs: the view modal (edit: false) is not
read-only — on both iOS and Android the native screen lets the user edit the
event, and those edits are saved directly by the OSBreaking: updateRecurring() is redesigned around series semantics (#69). Times are now expressed as startTime (EventTimeOfDay) plus duration instead o
updateRecurring() is redesigned around series semantics (#69).
Times are now expressed as startTime (EventTimeOfDay) plus duration
instead of absolute startDate/endDate, so every occurrence keeps its own
date — changing a series' time no longer re-anchors the series to the
occurrence you happened to edit (#68, thanks @SuperKrallan). The recurrence
rule is now a Patch<RecurrenceRule>: Patch.set replaces it, Patch.clear
collapses the series into a single event. Returns the event ID of the
affected scope.EventSpan.thisInstance is gone — EventSpan is now just
allEvents and thisAndFollowing. Single occurrences are handled by
updateEvent / deleteEvent with an instance ID (below).updateEvent() with an instance ID (eventId@timestamp)
edits only that occurrence, detaching it from the series; a bare event ID
on a recurring event updates the whole series.deleteEvent() with an instance ID removes only that
occurrence; a bare event ID deletes the event (the whole series when
recurring).EventTimeOfDay — small validating hour/minute value class used by
updateRecurring().startDate past the occurrence's untouched end
are rejected with invalidArguments on iOS too, matching Android, instead
of saving an inverted event.EventStatus.none instead of
EventStatus.tentative — thanks @mauriziopinotti (#70).updateRecurring() — update a recurring event with a span choice: EventSpan.allEvents (whole series), thisAndFollowing (this occurrence and every later
updateRecurring() — update a recurring event with a span choice:
EventSpan.allEvents (whole series), thisAndFollowing (this
occurrence and every later one), or thisInstance (only this occurrence).
Can change or remove the recurrence rule. Resolves the long-standing
limitation that updateEvent() could not edit recurrence.
Based on @SuperKrallan (#36)deleteRecurring() — delete part of a recurring event with a span choice:
EventSpan.allEvents (whole series), thisAndFollowing (this occurrence
and every later one), or thisInstance (only this occurrence). Now
supported on both iOS and Android (Android uses EXDATE on the master
rather than a cancelled exception event).
Based on @SuperKrallan (#43)EventSpan enum for choosing the scope of a recurring-event operation,
shared by updateRecurring() and deleteRecurring()url parameter on updateEvent() — based on @SuperKrallan (#38)edit parameter on showEventModal() — when true, opens the native editor
directly (EKEventEditViewController on iOS, ACTION_EDIT on Android)
instead of the read-only viewer. Based on @xonaman (#45)Calendar.color getter — derived Flutter Color? parsed from colorHex,
saving consumers from writing the same hex-parsing helper. Based on @xonaman (#46)updateEvent() description, location and url now take a
Patch<String> instead of a String. null leaves the field unchanged,
Patch.set(value) assigns a value, Patch.clear() removes it — clearing an
optional field was previously impossible.availability parameter in platform interface test mock — based on @SuperKrallan (#39)listSources() to discover calendar accounts/sources — based on @magic-fit
listSources() to discover calendar accounts/sources — based on @magic-fit (#14)createCalendar — iOS via CreateCalendarOptionsIos(sourceId:), Android via optional accountTypesupportsCalendarCreation on CalendarSourceavailability parameter on updateEvent() — thanks @SuperKrallan (#29)url field on events (iOS: EKEvent.url, Android: CUSTOM_APP_URI) — thanks @magic-fit (#32)showCreateEventModal() with optional pre-fill (title, dates, location, description)attendees on events (name, email, role, status)hasPermissions() now works from background services without an Activity (#31)notDetermined permission status correctly distinguished from denied — thanks @Albert221 (#12)createCalendar default fallback now picks iCloud over Gmail CalDAV (#33)iOS: Swift Package Manager support
Fixed parsing of instanceId for events with @ in their event ID (e.g., Google Calendar IDs like abc123@google.com)
instanceId for events with @ in their event ID (e.g., Google Calendar IDs like abc123@google.com)feat(android): add CreateCalendarOptionsAndroid for custom account na…
feat(android): add CreateCalendarOptionsAndroid for custom account na…
CreateCalendarOptionsAndroid for specifying custom account name when creating calendarscreateCalendar() now accepts optional platformOptions parameter for platform-specific configurationshowEventModal() now properly awaits until the modal is dismissed (iOS and Android)
showEventModal() now properly awaits until the modal is dismissed (iOS and Android)chore: bump version and update changelogs
chore: bump version and update changelogs
deleteEvent() now requires named parameter eventId and always deletes entire series for recurring eventsupdateEvent() now uses named parameter eventId (renamed from instanceId) and always updates entire series for recurring eventsdeleteAllInstances and updateAllInstances parameters - operations on recurring events now always affect the entire seriesgetEvent() and showEventModal() parameter from instanceId to id to clarify that both event IDs and instance IDs are acceptedNOT_SUPPORTED error code (no longer needed)openAppSettings() method to guide users to system settings when permissions are denied
openAppSettings() method to guide users to system settings when permissions are deniedgetPlatformVersion() method (unused boilerplate)Android: ProGuard/R8 rules for release build compatibility
Calendar permissions management (request/check)
Initial release.
DeviceCalendarException and DeviceCalendarError enumYour coding agent can read these notes before it upgrades. Set up the MCP server →