device_calendar_plus 0.10.1
device_calendar_plus: ^0.10.1 copied to clipboard
A modern, maintained Flutter plugin for reading and writing device calendar events on Android and iOS.
0.10.1 - 2026-10-01 #
Fixed #
updateRecurringrefuses astartmove to another day with no newrecurrenceRuleunless the existing rule generates both the day the occurrence moves to and the series' new start. An ordinal or BYSETPOS series (the 4th Thursday of November, say) could move to another occurrence of its weekday (the 3rd Thursday), leaving the start on a day the rule never generates; that now throwsDeviceCalendarException(invalidArguments). A move onto another day the rule generates (Monday to Wednesday of a Mon/Wed/Fri rule), which used to throw, is now allowed throughEventSpan.allEvents. WiththisAndFollowinga move to another day still throws while the rule pins days (#189, #194).- iOS:
updateRecurringon an all-day series countsduration(or the kept span, when onlystartmoves) in calendar days, so a multi-day all-day series no longer loses or gains its last day when its span crosses a DST change (#195). - iOS:
updateEventandupdateRecurringno longer add an extra day when they set the end of an all-day event that already exists. The end was written as midnight after the last day, which EventKit reads as one more day on an all-day event; a two-daydurationor end gave three days. It's now written the way EventKit stores it, the last second of the last day (#195).
0.10.0 - 2026-09-30 #
Changed #
-
Breaking:
updateEventanddeleteEventact on one thing — a one-off event or a single occurrence — and takeinstanceIdinstead ofeventId. A bare ID of a recurring series is refused withDeviceCalendarException(invalidArguments)and nothing is written. Moving a series' start throughupdateEventused to shift the whole series and drop every earlier occurrence without an error; series-wide changes now go throughupdateRecurring/deleteRecurring(#175).Migrating:
updateEvent(eventId: event.instanceId, …)→updateEvent(instanceId: event.instanceId, …). Same fordeleteEvent. For a one-off eventinstanceIdequalseventId, so nothing else changes.updateEvent(eventId: seriesId, …)on a recurring event →updateRecurring(seriesId, EventSpan.allEvents, …).startDate/endDatemap tostart/duration; the other fields are the same.deleteEvent(eventId: seriesId)on a recurring event →deleteRecurring(seriesId, EventSpan.allEvents).
Added #
updateRecurringtakesreminders(aPatch<List<Duration>>), to set or clear reminders across a series, asupdateEventdoes for one event (#175).
Fixed #
Recurring events
updateRecurringwiththisAndFollowingon an event that doesn't repeat is refused withinvalidArgumentson both platforms, and nothing is written. iOS matched the timestamp against the event itself, so it edited or deleted the whole event and reported success (#124).- Android: moving an all-day series with
updateRecurringlands on the day you asked for. The new start was compared in the wrong timezone: west of UTC a same-day start was refused and a day-earlier move onto a day the rule doesn't generate got through; east of UTC (e.g. Sydney) a one-day move did nothing (#144). - iOS: switching a timed series to all-day with
updateRecurringkeeps every occurrence on its calendar day. West of UTC the series gained an extra occurrence the day before its start and lost its last one (#187). - iOS:
updateRecurringrefuses a start move with no new rule when it breaks any day the rule pins, as Android does. A yearlyBYMONTH=11;BYDAY=4THseries could be moved to a Thursday in December (#188). createEventandupdateRecurringrefuse a recurrence rule whoseFREQisn'tDAILY/WEEKLY/MONTHLY/YEARLY, or that is malformed, withinvalidArgumentsand write nothing, on both platforms. iOScreateEventdropped the rule and saved a one-off event; Android storedFREQ=HOURLYas an hourly series (#125).- Android:
updateEventordeleteEventon one occurrence of a series on a synced calendar (Google, Exchange) no longer hides the whole series when the series hasn't synced yet (#163).
Native modals
- The native modals (
showEventModal,showCreateEventModal) always complete; several paths used to leave theawaithanging or crash (#123):- One modal at a time: a call while another modal is showing throws
DeviceCalendarException(operationFailed)straight away. It used to orphan the first call's future and, on iOS, fail to present, so both hung. - Android: rotating the device while a modal is open no longer drops the result; the future completes when the calendar app returns.
- iOS: swiping the view modal down completes the future.
- iOS: a modal no longer silently fails to appear when the app is already presenting a sheet; it's presented on top.
- With no Activity (Android) or window (iOS) to present from, the call
throws
DeviceCalendarException(operationFailed), asopenAppSettingsdoes. iOS used to crash, and Android threw an unconvertedPlatformException. - Android:
showEventModalon an event that doesn't exist throwsDeviceCalendarException(notFound), as iOS does. It used to open the calendar app on nothing and complete normally.
- One modal at a time: a call while another modal is showing throws
Permissions
- iOS 18: answering a full-access prompt with Add Events Only reports
writeOnlyrather thannotDetermined, so acreateEventstraight after the prompt passes its permission check (#137).
0.9.0 - 2026-09-26 #
Changed #
- Minimum 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
on every
flutter build(#133). - Event times are stored at whole seconds on both platforms. iOS always did this; Android now floors the times it writes, so a start passed with milliseconds reads back without them (#165).
Fixed #
Recurring events
updateRecurringwith a new rule anchors the series on the first day that rule generates, on both platforms. Changing a weekly series to another weekday left its start on the old day, so the first occurrence was stranded there. A rule that generates no occurrence at all is refused withinvalidArgumentsand the series is left untouched (#140).- Android:
updateEventordeleteEventon a single occurrence of a recurring event in a local calendar no longer makes the other occurrences disappear, the earlier ones for good (#153). - Android:
deleteRecurringwiththisAndFollowingalso removes an occurrence on or after the split that had been edited on its own, matching iOS (#157). - Android:
updateRecurringwiththisAndFollowingcarries an occurrence on or after the split that had been edited on its own into the new series, with its edits, as iOS does. It used to stay on the old series, listed next to a duplicate, and on a synced calendar the edit was lost (#158). - Android:
getEventresolves all-day recurring instance IDs (it always returned null) and returns a recurring master's real end date instead of a zero-length one (#122).
Synced calendars
- Android: deletes and recurring edits on a synced calendar (Google, Exchange) now reach the server. The plugin wrote them as the calendar's own sync adapter, so they were never uploaded and the next sync brought a deleted event back, one duplicate per cycle. Per-occurrence cancellations upload too (#132, #161).
Listing events
- Android:
listEventsreturns an all-day event when the window is a sub-day slice of its date (e.g. 10:00–11:00), and orders all-day events by their local midnight among timed events in non-UTC zones, as iOS does (#122). listEventsrejects anendDatebeforestartDatewithArgumentError, like the other date-range methods, and answers an empty range (endDate == startDate) with no events on both platforms (#162).
Calendars
updateCalendaranddeleteCalendarthrow the documentedreadOnlyfor a calendar that can't be modified, on both platforms. iOS used to surface a refused delete asoperationFailed; Android renamed or deleted any row it was handed (#126).- Android
createCalendarrefuses a non-localaccountTypewithreadOnly, aslistSourcesalready reports and iOS already does for non-creatable sources, instead of leaving a phantom calendar the account's sync adapter can wipe (#126). createCalendar/updateCalendarthrowArgumentErrorfor acolorHexthat isn't#RRGGBB(the#optional) instead of storing it silently as black, and forward the canonical#RRGGBBto the platform (#126).deleteCalendar('')throwsArgumentError, like the other mutations (#126).- Android
listCalendarsno longer crashes on a provider row with a NULL display name (#126).
Docs #
CalendarSource.supportsCalendarCreation,CreateCalendarOptionsIosandCreateCalendarOptionsAndroiddescribe what the code actually does: iOS creates under iCloud or local, Android under the local account type (#126).
0.8.1 - 2026-09-21 #
Fixed #
- iOS: a grant is honoured immediately. The first time a user allowed access,
the very next call could still fail with
permissionDenieduntil the app was restarted, because EventKit briefly kept reportingnotDeterminedafter the grant (#134). showCreateEventModalneeds 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, andautoPermissionsnever prompts for it. On iOS 16 and below the in-process editor still requires full access (#121, #141).- Android read endpoints report
permissionDeniedinstead of a silent empty result whenREAD_CALENDARisn't held, and full-tier mutations require bothREAD_CALENDARandWRITE_CALENDAR, matching iOS (#121). - iOS:
createEventwith a namedcalendarIdunder write-only access reportspermissionDenied(with a hint) instead of a misleadingnotFound(#121).
Docs #
CalendarPermissionStatus.deniedspells out the difference between the permanent denialhasPermissionsreports and the just-declined promptrequestPermissionsreports.
0.8.0 - 2026-07-22 #
Added #
Event.colorHex— the event's custom color as#RRGGBB, with a parsedEvent.colorgetter (read-only, mirrorsCalendar.colorHex). Android reads the event's custom color (EVENT_COLOR),nullwhen the event uses its calendar's color; iOS always reportsnull(EventKit has no per-event color). No write support (#117).
Changed #
- Breaking:
WeeklyRecurrence'swkstfield and constructor parameter are renamed toweekStart, matching the spelled-out naming of the other recurrence fields (daysOfWeek,daysOfMonth,setPositions, …). The emitted RRULE is unchanged (still uses theWKST=token). UpdateWeeklyRecurrence(wkst: …)call sites and.wkstreads toweekStart.
0.7.1 - 2026-06-17 #
Changed #
- Add the
devicepub.dev topic (droppedfederatedto stay within the five-topic limit).
Docs #
- Slimmed the README to an overview plus a getting-started snippet, and moved
the worked examples into focused topic guides under
doc/. Trimmed the API doc comments to the consumer-facing contract and removed native-API implementation details. No code or behavior changes.
0.7.0 - 2026-06-17 #
Added #
- Write-only calendar access.
requestPermissions(level: CalendarAccessLevel.writeOnly)asks for the gentler add-only prompt and a grant reportsCalendarPermissionStatus.writeOnly. On iOS this is not a permanent ceiling — a later full request re-prompts and upgrades the app in-app (#89). - Automatic permission handling. Set
DeviceCalendar.instance.autoPermissionstoAutoPermissionMode.asNeededor.fulland methods request the access they need on first use instead of throwing when permission is undetermined (#90). createEvent'scalendarIdis now optional — omit it to write to the platform's default calendar (iOSdefaultCalendarForNewEvents, Android the primary or first writable calendar). Resolving the default on Android reads the calendar list, so that path needs full access (#88).- Event reminders.
createEventtakesreminders: List<Duration>,updateEventtakesPatch<List<Duration>>, andEvent.remindersis read back. Relative before-start offsets, normalized to whole minutes on both platforms (#87).
0.6.0 - 2026-06-16 #
Changed #
- Breaking:
updateRecurring()now takesstart: DateTimeinstead ofstartTime: EventTimeOfDay.startis 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) - Breaking:
EventTimeOfDayis removed. - Breaking: all-day events now accept
start(only its date is used) instead of throwing.
Added #
updateRecurring()translates implicit-day rules for free: aWeeklyRecurrence()/MonthlyRecurrence()with no pinned day follows the anchor when you move the day.
Behaviour #
- Moving the day of a rule that pins it explicitly (
daysOfWeek,daysOfMonth, positional) without also passing arecurrenceRulethrowsDeviceCalendarException(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.
0.5.2 - 2026-06-15 #
Changed #
- No-op updates are now valid instead of throwing.
updateEvent,updateRecurringandupdateCalendarreturn without a platform write when no fields are provided, so "save with no edits" is a harmless no-op rather than anArgumentError(#95).updateRecurringreturns the targeted scope's event id.updateRecurring'sdurationnow accepts zero (an instantaneous event); only a negative duration is rejected.
Fixed #
- Recurrence parsing accepts a negative
BYMONTHDAY(e.g.-1for the last day of the month) instead of rejecting the rule (#91) - Turning a recurring occurrence non-recurring with
thisAndFollowing+Patch.clear()now splits the series on iOS instead of collapsing the whole series into one event (#93) — see thedevice_calendar_plus_ioschangelog listEventsreturns 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 changelogscreateCalendarfails with a clear error on iOS sources that can't hold calendars, instead of an opaque failure (#96) — see thedevice_calendar_plus_ioschangelog- iOS EventKit operations run off the main thread, preventing UI stalls on
large calendars (#79) — see the
device_calendar_plus_ioschangelog
Docs #
- Documented
listEventsper-instance expansion and theeventId@timestampinstanceId format (#97)
0.5.1 - 2026-06-15 #
Fixed #
- iOS:
showEventModal(edit: true)no longer crashes (#77) — see thedevice_calendar_plus_ios0.5.1 changelog
Docs #
- Clarified
showEventModaldocs: 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 OS
0.5.0 - 2026-06-11 #
Changed #
- Breaking:
updateRecurring()is redesigned around series semantics (#69). Times are now expressed asstartTime(EventTimeOfDay) plusdurationinstead of absolutestartDate/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 aPatch<RecurrenceRule>:Patch.setreplaces it,Patch.clearcollapses the series into a single event. Returns the event ID of the affected scope. - Breaking:
EventSpan.thisInstanceis gone —EventSpanis now justallEventsandthisAndFollowing. Single occurrences are handled byupdateEvent/deleteEventwith an instance ID (below). - Breaking:
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. - Breaking:
deleteEvent()with an instance ID removes only that occurrence; a bare event ID deletes the event (the whole series when recurring).
Added #
EventTimeOfDay— small validating hour/minute value class used byupdateRecurring().
Fixed #
- Occurrence edits with a
startDatepast the occurrence's untouched end are rejected withinvalidArgumentson iOS too, matching Android, instead of saving an inverted event. - Android: events with no status read back as
EventStatus.noneinstead ofEventStatus.tentative— thanks @mauriziopinotti (#70). - Android: all Calendar Provider work runs on a background thread; large calendars could ANR — thanks @mauriziopinotti (#73).
0.4.0 - 2026-05-25 #
Added #
updateRecurring()— update a recurring event with a span choice:EventSpan.allEvents(whole series),thisAndFollowing(this occurrence and every later one), orthisInstance(only this occurrence). Can change or remove the recurrence rule. Resolves the long-standing limitation thatupdateEvent()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), orthisInstance(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)EventSpanenum for choosing the scope of a recurring-event operation, shared byupdateRecurring()anddeleteRecurring()urlparameter onupdateEvent()— based on @SuperKrallan (#38)editparameter onshowEventModal()— whentrue, opens the native editor directly (EKEventEditViewControlleron iOS,ACTION_EDITon Android) instead of the read-only viewer. Based on @xonaman (#45)Calendar.colorgetter — derived FlutterColor?parsed fromcolorHex, saving consumers from writing the same hex-parsing helper. Based on @xonaman (#46)
Changed #
- Breaking:
updateEvent()description,locationandurlnow take aPatch<String>instead of aString.nullleaves the field unchanged,Patch.set(value)assigns a value,Patch.clear()removes it — clearing an optional field was previously impossible.
Fixed #
- Missing
availabilityparameter in platform interface test mock — based on @SuperKrallan (#39)
0.3.5 - 2026-04-20 #
Added #
listSources()to discover calendar accounts/sources — based on @magic-fit (#14)- Source selection on
createCalendar— iOS viaCreateCalendarOptionsIos(sourceId:), Android via optionalaccountType supportsCalendarCreationonCalendarSourceavailabilityparameter onupdateEvent()— thanks @SuperKrallan (#29)urlfield on events (iOS:EKEvent.url, Android:CUSTOM_APP_URI) — thanks @magic-fit (#32)showCreateEventModal()with optional pre-fill (title, dates, location, description)- Read-only
attendeeson events (name, email, role, status)
Fixed #
- Android: all-day events appearing in wrong day's query in non-UTC timezones (#20)
- Android:
hasPermissions()now works from background services without an Activity (#31) - Android: calendar/event queries use application context for background compatibility — thanks @vitalii-vov (#26)
- Android:
notDeterminedpermission status correctly distinguished fromdenied— thanks @Albert221 (#12) - iOS: calendar source lookup fallback when default source is unavailable — thanks @zaqwery (#13)
- iOS:
createCalendardefault fallback now picks iCloud over Gmail CalDAV (#33)
0.3.3 - 2025-12-21 #
Fixed #
- Fixed parsing of
instanceIdfor events with@in their event ID (e.g., Google Calendar IDs likeabc123@google.com)
0.3.2 - 2025-12-19 #
Added #
- Android:
CreateCalendarOptionsAndroidfor specifying custom account name when creating calendars createCalendar()now accepts optionalplatformOptionsparameter for platform-specific configuration
0.3.1 - 2025-11-07 #
Fixed #
showEventModal()now properly awaits until the modal is dismissed (iOS and Android)
0.3.0 - 2024-11-05 #
Changed #
- BREAKING:
deleteEvent()now requires named parametereventIdand always deletes entire series for recurring events - BREAKING:
updateEvent()now uses named parametereventId(renamed frominstanceId) and always updates entire series for recurring events - BREAKING: Removed
deleteAllInstancesandupdateAllInstancesparameters - operations on recurring events now always affect the entire series - Renamed
getEvent()andshowEventModal()parameter frominstanceIdtoidto clarify that both event IDs and instance IDs are accepted
Removed #
- BREAKING:
NOT_SUPPORTEDerror code (no longer needed)
0.2.0 - 2024-11-05 #
0.1.0 - 2024-11-04 #
Initial release.
Added #
- Calendar permissions management (request/check)
- List device calendars with metadata (name, color, read-only status, primary flag)
- Query events by date range with optional calendar filtering
- Get single event by ID with support for recurring event instances
- Create events with full metadata support
- Update events including single-instance and all-instance updates for recurring events
- Delete events (single or all instances)
- Show native event modal
- All-day event support with floating date behavior
- Timezone handling for timed events
- Typed exception model with
DeviceCalendarExceptionandDeviceCalendarErrorenum - Federated plugin architecture (Android + iOS)
- Support for Android API 24+ (target/compile 35)
- Support for iOS 13+