NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #2685 most downloaded on pub.dev
Android implementation of the device_calendar_plus plugin.
Last release 7 days ago
02 Oct 2026
Ships fairly regularly
a new release about every 6 weeks
Nearly every release is documented
notes for 20 of 20 stable releases
Nothing withdrawn
no release was ever pulled
11 months old
20 releases · first in 2025
One column per month.
updateRecurring refuses a start move with no new rule unless the existing rule generates the series' new start. It compared only the weekday, day of m
updateRecurring refuses a start move with no new rule unless the existing
rule generates the series' new start. It compared only the weekday, day of
month and month the rule pins, so an ordinal BYDAY or a BYSETPOS series
(the 4th Thursday of November, say) could move to another occurrence of its
weekday (the 3rd Thursday), leaving DTSTART on a day the rule never
generates (#189). A move onto another day the rule generates (Monday to
Wednesday of a Mon/Wed/Fri rule) is now allowed through allEvents; with
thisAndFollowing a move to another day still throws while the rule pins
days, until iOS can split such a series (#194).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…
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.
updateEvent and deleteEvent without a timestamp refuse
a recurring series with INVALID_ARGUMENTS and write nothing; series-wide
changes go through updateRecurring / deleteRecurring. This also closes
the path that wrote DTEND onto a recurring master (#175, #125).
deleteEvent on a bare ID that is gone or only a DELETED=1 tombstone
reports NOT_FOUND; collecting a tombstoned series is
deleteRecurring(allEvents)'s job.updateRecurring forwards a reminders patch (#175).updateRecurring on an all-day series converts the new start to the
stored UTC midnight before comparing it, as updateEvent already did. 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 a one-day move counted as
zero days and the series stayed put (#144).showEventModal/showCreateEventModal
while one is showing fails OPERATION_FAILED instead of orphaning the
first reply; a pending modal survives a configuration change (rotation)
and resolves when the activity goes away for good; no Activity replies
OPERATION_FAILED rather than an unconverted error; and
showEventModal looks the event up first and fails NOT_FOUND for one
that doesn't exist, matching iOS.updateEvent or deleteEvent on a single occurrence of a recurring event
on a synced calendar (Google, Exchange) no longer hides the whole series
when the series hasn't synced yet. Until the calendar's first sync of the
series, it lists as it was, and the per-occurrence change shows once the
calendar syncs (#163, the synced-calendar side of #153).createEvent and updateRecurring refuse a recurrence rule whose FREQ
isn't DAILY/WEEKLY/MONTHLY/YEARLY (or that is malformed) with
INVALID_ARGUMENTS before writing, matching iOS. FREQ=HOURLY used to be
stored as an hourly series, and malformed input failed as
OPERATION_FAILED (#125).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.
getEvent on the series and its listed occurrences disagreed by up to a
second. createEvent, updateEvent and updateRecurring now floor start
and end to the second, and a series re-anchored by updateRecurring drops
any milliseconds it was stored with (#165).updateRecurring with thisAndFollowing now carries an occurrence that
was edited on its own, and falls on or after the split, into the new
series, as iOS does. It keeps its own title, time, reminders and other
edits, and the new series no longer lists a second copy beside it. When
the split moves the start, the occurrence's slot moves by the same number
of days. An occurrence deleted on its own stays deleted if the start stays
put, and comes back if the start moves, matching iOS. On a synced calendar
the occurrence is re-created on the new series and the old one is deleted,
since the server ties it to the old series (#158).deleteEvent, deleteRecurring (whole series, one occurrence, or
thisAndFollowing, including the detached occurrences it sweeps) and
updateRecurring now write as the app they are, so the adapter finds a
DELETED/DIRTY row to upload; local calendars, which have no adapter,
keep the direct deletes (#132, #161).deleteRecurring with thisAndFollowing now removes a detached occurrence
that falls on or after the split. Truncating the series' rule left an
occurrence that had been edited on its own behind as an orphan row, which
came back in listEvents once the provider next rebuilt its Instances
cache; iOS removes it, so Android does too.updateCalendar and deleteCalendar refuse a calendar whose access level
is below contributor — the same calendars listCalendars reports as
readOnly — with READ_ONLY before writing, instead of renaming or
deleting any row they were handed (#126).createCalendar refuses a non-local accountType with READ_ONLY, as
listSources already reports, instead of sync-adapter-inserting a phantom
calendar into another account's namespace (#126).listCalendars reads a NULL display name as empty instead of handing Dart
a null it casts (#126).getEvent resolves the instance ID of an all-day recurring occurrence. The
lookup went through the all-day date filter with a two-second window, which
collapses to an empty date range in every timezone, so it always returned
null; it now matches the Instances row on EVENT_ID and BEGIN (#122).getEvent with a bare recurring ID returns the master's real end date.
A recurring row stores DURATION with no DTEND, and the end used to fall
back to the start (#122).listEvents sorts on the start date it reports, so an all-day event lands
at its local midnight among timed events in non-UTC zones instead of at its
stored UTC-midnight instant. Matches iOS (#122)._sync_id, which a local calendar
never gets, so the exception insert wiped the master's occurrences from its
Instances cache — the earlier ones for good. A local series is now given a
_sync_id before its first exception is written, and deleting the series
removes its detached occurrences with it (#153).getEvent, updateEvent, updateRecurring, and deleteEvent /
deleteRecurring for anything short of the whole series no longer see an
event that another app has deleted but the provider only tombstoned
(DELETED=1, the fate of any event with a _sync_id deleted outside a
sync adapter, which now includes a local series edited per occurrence).
Such an event reads as not found instead of accepting edits against a row
that never shows in listEvents. On a local calendar a whole-series delete
still collects the tombstone.updateRecurring with a new rule anchors the series on the first day that
rule generates, as iOS does. 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 with
INVALID_ARGUMENTS and the series is left untouched (#140).listEvents returns an all-day event when the window is a sub-day slice of
its date (e.g. 10:00–11:00). The all-day date filter mapped both window
edges to the same UTC midnight, so the range collapsed to nothing; the end
edge now rounds up to the next UTC midnight when it isn't on a local
midnight. Matches iOS.flutter build and keeps the plugin building once Flutter rejects
plugins that apply KGP themselves (#133).showCreateEventModal no longer requires READ_CALENDAR. ACTION_INSERT needs no permission, so the gate only blocked the one path that still works after
showCreateEventModal no longer requires READ_CALENDAR. ACTION_INSERT
needs no permission, so the gate only blocked the one path that still works
after a denial (#121, #141).listCalendars, listSources, listEvents and getEvent throw
permissionDenied when READ_CALENDAR isn't held instead of returning an
empty result (#121).READ_CALENDAR and WRITE_CALENDAR; WRITE_CALENDAR
alone is the write-only tier and could previously mutate calendars that iOS
rejects. The error message names the tier that's missing (#121).showCreateEventModal writes EXTRA_EVENT_ALL_DAY as a boolean extra, so
the all-day prefill is honoured (#121).PermissionGates.kt; event cursor
projections go through EventColumns presets (#119). No behaviour change.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/.colorHex read from Events.EVENT_COLOR when the event
has a custom color; absent otherwise (#117).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.
writeOnly request asks for WRITE_CALENDAR only.
READ_CALENDAR and WRITE_CALENDAR share the CALENDAR group, so a later
full request escalates to read access with no dialog (#89).createEvent with no calendarId resolves a default calendar — the primary
writable calendar, falling back to the first writable one (#88).CalendarContract.Reminders rows and Events.HAS_ALARM
(#87).WRITE_CALENDAR, and a cancelled
permission dialog no longer reports denied (#108).createEvent with no calendarId reports permissionDenied rather than
"no writable calendar" when it can't read the calendar list to resolve a
default (#112).chore(release): prepare 0.6.0
chore(release): prepare 0.6.0
updateRecurring takes the anchored occurrence's new start
(newStartMillis) instead of startMinuteOfDay. shiftDate translates the
series anchor by the wall-clock delta (calendar-day count + time-of-day) in
the event's EVENT_TIMEZONE, so a move can change the day and time together
and stays correct across DST (#103).allEvents edit now re-writes the unchanged RRULE so the
CalendarProvider re-expands the Instances cache; without it a moved DTSTART
could read back as a single occurrence.updateRecurring rejects a start that moves the day of a series whose
RRULE pins it (BYDAY / BYMONTHDAY / BYMONTH) unless a new rule is also
supplied (dayMoveConflictsWithRule).listEvents now returns a zero-duration (instantaneous) event that sits exactly on the query's start time; the half-open overlap check previously exclu
listEvents now returns a zero-duration (instantaneous) event that sits
exactly on the query's start time; the half-open overlap check previously
excluded it (#416)iOS: fix showEventModal(edit: true) crash
Breaking: recurring-edit split — updateRecurring / deleteRecurring accept only allEvents and thisAndFollowing; single occurrences go through updateEve
updateRecurring / deleteRecurring
accept only allEvents and thisAndFollowing; single occurrences go through
updateEvent / deleteEvent with an occurrence timestamp (detached
exception rows; deletes via STATUS_CANCELED exceptions)updateRecurring time changes preserve each occurrence's date, replacing
only the time-of-day; thisAndFollowing truncates the master with UNTIL
to match iOS's EKSpan.futureEvents splitSTATUS column reads back as none; it was defaulted to 0, which
is STATUS_TENTATIVE, so status-less events came back tentative (#70) —
thanks @mauriziopinottiupdateRecurring() — series-level recurring-event edits with EventSpan (allEvents / thisAndFollowing / thisInstance). thisAndFollowing truncates the ma
updateRecurring() — series-level recurring-event edits with EventSpan (allEvents / thisAndFollowing / thisInstance). thisAndFollowing truncates the master with UNTIL and starts a new series; thisInstance writes a detached exception event.deleteRecurring() — allEvents deletes the master; thisAndFollowing truncates via UNTIL; thisInstance appends to the master's EXDATE column (no separate exception event needed).url field on events via Events.CUSTOM_APP_URIPatch<T> support in updateEvent() — null leaves a field unchanged, Patch.set writes, Patch.clear writes the empty string to removeedit flag on showEvent() — fires Intent.ACTION_EDIT instead of ACTION_VIEWAll-day events appearing in wrong day's query in non-UTC timezones
PermissionService accepts Context — hasPermissions() works without an Activity (#31)Version sync with other packages. No functional changes.
Version sync with other packages. No functional changes.
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 accountName parameter via platform optionsshowEvent() now uses startActivityForResult() to properly await until the calendar activity is dismissed
showEvent() now uses startActivityForResult() to properly await until the calendar activity is dismissedchore: bump version and update changelogs
chore: bump version and update changelogs
deleteEvent() now always deletes entire series for recurring events (removed deleteAllInstances parameter)updateEvent() now always updates entire series for recurring events (removed updateAllInstances parameter)NOT_SUPPORTED error code (no longer needed as single-instance operations are not attempted)openAppSettings() implementation to open Android app settings via Intent
openAppSettings() implementation to open Android app settings via IntentgetPlatformVersion() implementation (unused boilerplate)ProGuard/R8 rules to prevent code stripping in release builds
Initial release.
Initial release.
Your coding agent can read these notes before it upgrades. Set up the MCP server →