A comprehensive Flutter plugin for accessing and managing device calendars on Android, iOS, and macOS with clean architecture principles.
- Features
- Platform Support
- Installation
- Platform Setup
- Usage
- Error Handling
- Models
- Example App
- Testing
- Architecture
- Contributing
- License
- Support
- Calendar Management: Create, retrieve, and delete calendars
- Event CRUD Operations: Full create, read, update, delete support for events
- Recurring Events: Support for recurring events using RRULE format
- Timezone Support: Full timezone handling with TZDateTime
- Attendee Management: Add and manage event attendees
- Reminders: Set and manage event reminders
- Permission Handling: Proper permission management across platforms
- Calendar Colors: Support for calendar and event colors
- Clean Architecture: Built with domain-driven design principles
- Well Tested: Comprehensive test coverage
Platform | Supported | Notes |
---|---|---|
Android | Yes | API 21+ |
iOS | Yes | iOS 13+ |
macOS | Yes | macOS 11+ |
Add this to your package's pubspec.yaml
file:
dependencies:
calendar_bridge: ^1.0.0
Then run:
flutter pub get
Add the following keys to your Info.plist
file:
<key>NSCalendarsUsageDescription</key>
<string>This app requires access to your calendar to manage your events.</string>
<!-- Full access for macOS 14+ -->
<key>NSCalendarsFullAccessUsageDescription</key>
<string>This app requires full calendar access to create, edit, and delete your events.</string>
<!-- Additional write access -->
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>This app requires access to create calendar events.</string>
<key>NSRemindersUsageDescription</key>
<string>This app needs access to reminders to set event notifications</string>
Add the following permissions to your android/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
import 'package:calendar_bridge/calendar_bridge.dart';
final calendarApi = CalendarBridge();
Always request permissions before accessing calendar data:
// Check if permissions are granted
final hasPermissions = await calendarApi.hasPermissions();
if (hasPermissions != PermissionStatus.granted) {
// Request permissions
final granted = await calendarApi.requestPermissions();
if (!granted) {
// Handle permission denied
return;
}
}
try {
final calendars = await calendarApi.getCalendars();
for (final calendar in calendars) {
print('Calendar: ${calendar.name} (${calendar.id})');
}
} catch (e) {
print('Error getting calendars: $e');
}
try {
final newCalendar = await calendarApi.createCalendar(
name: 'My Custom Calendar',
color: 0xFF2196F3, // Blue color
localAccountName: 'Local',
);
print('Created calendar: ${newCalendar.name}');
} catch (e) {
print('Error creating calendar: $e');
}
try {
final defaultCalendar = await calendarApi.getDefaultCalendar();
print('Default calendar: ${defaultCalendar.name}');
} catch (e) {
print('Error getting default calendar: $e');
}
try {
final eventId = await calendarApi.createSimpleEvent(
calendarId: calendar.id,
title: 'Team Meeting',
start: DateTime.now().add(Duration(hours: 1)),
end: DateTime.now().add(Duration(hours: 2)),
description: 'Weekly team sync',
location: 'Conference Room A',
);
print('Created event with ID: $eventId');
} catch (e) {
print('Error creating event: $e');
}
final event = CalendarEvent(
calendarId: calendar.id,
title: 'Project Review',
description: 'Quarterly project review meeting',
start: TZDateTime.from(DateTime.now().add(Duration(days: 1)), UTC),
end: TZDateTime.from(DateTime.now().add(Duration(days: 1, hours: 2)), UTC),
location: 'Meeting Room B',
attendees: [
Attendee(
name: 'John Doe',
email: 'john@example.com',
role: AttendeeRole.required,
),
Attendee(
name: 'Jane Smith',
email: 'jane@example.com',
role: AttendeeRole.optional,
),
],
reminders: [
Reminder(minutes: 15), // 15 minutes before
Reminder(minutes: 60), // 1 hour before
],
);
try {
final eventId = await calendarApi.createEvent(event);
print('Created complex event with ID: $eventId');
} catch (e) {
print('Error creating event: $e');
}
try {
// Get all events
final allEvents = await calendarApi.getEvents(calendar.id);
// Get events in a date range
final eventsInRange = await calendarApi.getEvents(
calendar.id,
startDate: DateTime.now(),
endDate: DateTime.now().add(Duration(days: 30)),
);
// Get today's events
final todaysEvents = await calendarApi.getTodaysEvents(calendar.id);
// Get upcoming events (next 7 days by default)
final upcomingEvents = await calendarApi.getUpcomingEvents(calendar.id);
print('Found ${allEvents.length} total events');
} catch (e) {
print('Error getting events: $e');
}
// First, get the event
final events = await calendarApi.getEvents(calendar.id);
if (events.isNotEmpty) {
final eventToUpdate = events.first.copyWith(
title: 'Updated Event Title',
description: 'Updated description',
);
try {
await calendarApi.updateEvent(eventToUpdate);
print('Event updated successfully');
} catch (e) {
print('Error updating event: $e');
}
}
try {
final success = await calendarApi.deleteEvent(calendar.id, eventId);
if (success) {
print('Event deleted successfully');
}
} catch (e) {
print('Error deleting event: $e');
}
Calendar Bridge supports recurring events using the RRULE standard:
import 'package:rrule/rrule.dart';
final recurringEvent = CalendarEvent(
calendarId: calendar.id,
title: 'Daily Standup',
start: TZDateTime.from(DateTime.now(), UTC),
end: TZDateTime.from(DateTime.now().add(Duration(minutes: 30)), UTC),
recurrenceRule: RecurrenceRule(
frequency: Frequency.daily,
count: 30, // Repeat 30 times
),
);
await calendarApi.createEvent(recurringEvent);
Get and set calendar colors:
// Get available calendar colors
final calendarColors = await calendarApi.getCalendarColors();
print('Available calendar colors: $calendarColors');
// Get available event colors for a calendar
final eventColors = await calendarApi.getEventColors(calendar.id);
print('Available event colors: $eventColors');
// Update calendar color
if (calendarColors != null && calendarColors.isNotEmpty) {
final colorKey = calendarColors.keys.first;
await calendarApi.updateCalendarColor(calendar.id, colorKey);
}
Calendar Bridge provides specific exception types for better error handling:
try {
final calendars = await calendarApi.getCalendars();
} on PermissionDeniedException {
print('Calendar permissions are required');
} on CalendarNotFoundException {
print('Calendar not found');
} on EventNotFoundException {
print('Event not found');
} on InvalidArgumentException {
print('Invalid arguments provided');
} catch (e) {
print('Unexpected error: $e');
}
class Calendar {
final String id;
final String name;
final int? color;
final String? accountName;
final String? accountType;
final bool isReadOnly;
final bool isDefault;
}
class CalendarEvent {
final String calendarId;
final String? eventId;
final String? title;
final String? description;
final TZDateTime? start;
final TZDateTime? end;
final bool allDay;
final String? location;
final String? url;
final RecurrenceRule? recurrenceRule;
final List<Attendee> attendees;
final List<Reminder> reminders;
final EventStatus? eventStatus;
final Availability? availability;
final String? organizer;
final String? eventColor;
}
class Attendee {
final String? name;
final String? email;
final AttendeeRole? role;
final AttendeeStatus? status;
}
class Reminder {
final int minutes; // Minutes before event start
}
The plugin comes with a comprehensive example app that demonstrates all features. To run the example:
cd example
flutter run
The example app includes:
- Calendar list view
- Event management (create, edit, delete)
- Calendar view with monthly grid
- Settings and permissions handling
Calendar Bridge includes comprehensive test coverage. Run tests with:
flutter test
For integration tests:
cd example
flutter test integration_test/
This plugin follows clean architecture principles:
- Domain Layer: Contains business logic, entities, and use cases
- Infrastructure Layer: Platform-specific implementations
- API Layer: Simple, clean interface for consumers
Contributions are welcome! Please read the contributing guidelines and submit pull requests to the GitHub repository.
See CHANGELOG.md for a detailed list of changes.
This project is licensed under the MIT License - see the LICENSE file for details.
If you encounter any issues or have questions, please file an issue on GitHub.