device_calendar_plus
A Flutter plugin for reading and writing calendar events on Android and iOS.
A maintained replacement for the abandoned device_calendar plugin, built for Bullet: a cleaner Dart API, timezones that behave, and no timezone package tagging along. Getting EventKit and Android's Calendar Provider to agree on anything is a slog - this package handles it so your app doesn't have to.
Built for Bullet, a calmer task + notes + calendar app, and running in production there.
What it does
- Permissions - request and check access, including a write-only tier
- Calendars & sources - create, read, update, delete, and target a specific account
- Events - create, read, update, delete, and query by date range
- Recurring events - full RRULE support with a typed
RecurrenceRulemodel; edit or delete a whole series, this-and-following, or a single occurrence - Reminders - relative before-start alarms, read and write
- Native UI - open the OS event viewer/editor, or a pre-filled create screen
- All-day & timezones - floating dates and sane local-time behaviour
One API, both platforms
This plugin exposes only what works the same on Android and iOS. If a feature is read-only on one platform, it's read-only here. If it doesn't exist on one, it isn't included. Where the platforms naturally disagree, Android is conformed to iOS, which sets the contract.
It's best-effort, and honest about the gaps. A few platform realities can't be smoothed over: older iOS has no write-only permission tier, and the native editor screens behave differently across calendar apps. Those cases are flagged in the docs where they show up, not hidden behind an API pretending everything's identical.
Supported versions
| Platform | Min OS / SDK | Target / Compile |
|---|---|---|
| Android | minSdk 24+ | target/compile 35 |
| iOS | iOS 13+ | Latest Xcode / iOS SDK |
Install
dependencies:
device_calendar_plus: <latest version>
iOS
Add usage descriptions to your app's Info.plist:
<!-- iOS 10-16 (legacy key, still valid) -->
<key>NSCalendarsUsageDescription</key>
<string>Access your calendar to view and manage events.</string>
<!-- iOS 17+ -->
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Full access to view and edit your calendar events.</string>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>Add events without reading existing events.</string>
Android
Add calendar permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
ProGuard / R8 rules are applied automatically, so there's nothing to configure.
Getting started
import 'package:device_calendar_plus/device_calendar_plus.dart';
final plugin = DeviceCalendar.instance;
// 1. Permissions - either ask for them yourself...
final status = await plugin.requestPermissions();
if (status != CalendarPermissionStatus.granted) return;
// ...or let methods prompt on first use (set once at app start, then skip
// the explicit request above):
// plugin.autoPermissions = AutoPermissionMode.full;
// 2. Create an event (omit calendarId to use the default calendar).
final eventId = await plugin.createEvent(
title: 'Team Meeting',
startDate: DateTime.now().add(const Duration(hours: 1)),
endDate: DateTime.now().add(const Duration(hours: 2)),
);
// 3. Read events back.
final now = DateTime.now();
final events = await plugin.listEvents(now, now.add(const Duration(days: 7)));
Permissions
Every read or write needs calendar permission. Ask for it yourself:
final status = await plugin.requestPermissions();
if (status != CalendarPermissionStatus.granted) return;
Add-only apps can ask for the gentler write-only tier (requestPermissions(level: CalendarAccessLevel.writeOnly)). Or let methods prompt on first use by setting autoPermissions once at app start. Most apps read, so .full is the one you usually want:
DeviceCalendar.instance.autoPermissions = AutoPermissionMode.full;
See Permissions for write-only, the automatic modes, and upgrading tiers in-app.
Dates & timezones
Every DateTime the plugin hands back is in local time, and there are two kinds (no timezone package required):
- Timed events are instants. A meeting at "3 PM EST" is a specific moment, so convert
startDate/endDateto UTC freely for storage or comparison. SettingtimeZonereinterprets the wall-clock time ("3 PM EST" becomes "3 PM PST"), not the instant. - All-day events are floating dates. A birthday on January 15 stays January 15 in any timezone, so use the date components and don't convert to UTC.
Error handling
Two kinds of errors:
DeviceCalendarException- runtime conditions to handle (permission denied, not found, read-only). It carries aDeviceCalendarErrorcode to switch on.- Standard Dart errors (e.g.
ArgumentError,StateError) - programmer mistakes caught before any platform call, like invalid arguments. Fix them, don't catch them. A call with nothing to change (e.g.updateEventwith no fields) is a harmless no-op, not an error.
try {
await plugin.createEvent(/* ... */);
} on DeviceCalendarException catch (e) {
if (e.errorCode == DeviceCalendarError.permissionDenied) {
// Ask the user to grant access.
}
}
More docs
- Permissions - request, check, write-only, automatic
- Calendars & sources - list, create, update, delete
- Events - create, list, get, update, delete
- Recurring events - rules, series edits, occurrences
- Reminders - relative before-start alarms
- Native UI - view, edit, and create modals
Contributing
Pull requests are closed. Keeping the API surface and the iOS/Android parity consistent works best with a single hand on the wheel. Bug reports and feature requests are very welcome though, so open an issue and let's chat. :)
License
MIT © 2025 Bullet. See LICENSE.
Made by Bullet, a task + notes + calendar app built with Flutter.