[Dalendar DevLog 7] Advanced Features - Widgets and Notifications
Implementing home screen widgets and smart notification system for enhanced user engagement.
![[Dalendar DevLog 7] Advanced Features - Widgets and Notifications](/images/blog/dalendar_dev_7_widgets.png)
Episode 7: Advanced Features - Widgets and Notifications
Introduction
A great calendar app extends beyond the main app interface. This episode covers implementing home screen widgets for quick access and a smart notification system to keep users informed.
1. Home Screen Widget Design
1.1. Widget Types
We implement three widget variants:
- Month Widget: Full calendar view
- Agenda Widget: Upcoming schedule list
- Mini Widget: Today’s date with schedule count
1.2. Month Widget Implementation
class CalendarWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
MonthWidgetContent()
}
}
}
@Composable
fun MonthWidgetContent() {
val context = LocalContext.current
val repository = remember { getRepository(context) }
val currentRataDie = remember { EAFCalculator.todayRataDie() }
val schedules by repository.getSchedulesForMonth(/* current month */)
.collectAsState(initial = emptyList())
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(8.dp)
) {
MonthHeader()
DayGrid(schedules = schedules)
}
}
1.3. Efficient Widget Updates
class WidgetUpdateWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// Only update if schedules changed
val lastUpdate = preferences.getLastWidgetUpdate()
val hasChanges = repository.hasChangesRataDie(lastUpdate)
if (hasChanges) {
GlanceAppWidgetManager(applicationContext)
.updateAll(CalendarWidget::class.java)
preferences.setLastWidgetUpdate(System.currentTimeMillis())
}
return Result.success()
}
}
// Schedule periodic updates
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"widget_update",
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<WidgetUpdateWorker>(15, TimeUnit.MINUTES).build()
)
1.4. Widget Interactions
// Click on date opens app to that day
Text(
text = day.toString(),
modifier = GlanceModifier.clickable {
actionStartActivity(
MainActivity::class.java,
parameters = actionParametersOf(
KEY_SELECTED_DATE to rataDie
)
)
}
)
2. Notification System
2.1. Notification Channel Setup
fun createNotificationChannels(context: Context) {
val channels = listOf(
NotificationChannel(
CHANNEL_REMINDERS,
"Schedule Reminders",
NotificationManager.IMPORTANCE_HIGH
),
NotificationChannel(
CHANNEL_SUMMARIES,
"Daily Summaries",
NotificationManager.IMPORTANCE_DEFAULT
)
)
val manager = context.getSystemService(NotificationManager::class.java)
channels.forEach { manager.createNotificationChannel(it) }
}
2.2. Schedule Reminder Notifications
fun scheduleReminder(context: Context, schedule: Schedule, minutesBefore: Int) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
// Calculate reminder time
val scheduleDateTimeMillis = rataDieToMillis(schedule.startRataDie) +
parseTimeToMillis(schedule.startTime)
val reminderTime = scheduleDateTimeMillis - (minutesBefore * 60 * 1000)
val intent = Intent(context, ReminderReceiver::class.java).apply {
putExtra(KEY_SCHEDULE_ID, schedule.id)
putExtra(KEY_SCHEDULE_TITLE, schedule.title)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
schedule.id.toInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
reminderTime,
pendingIntent
)
}
2.3. Notification Builder
class ReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val scheduleId = intent.getLongExtra(KEY_SCHEDULE_ID, -1)
val title = intent.getStringExtra(KEY_SCHEDULE_TITLE) ?: return
val notification = NotificationCompat.Builder(context, CHANNEL_REMINDERS)
.setSmallIcon(R.drawable.ic_calendar)
.setContentTitle(title)
.setContentText("Starting soon")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(createOpenAppIntent(context, scheduleId))
.addAction(
R.drawable.ic_snooze,
"Snooze 10 min",
createSnoozeIntent(context, scheduleId)
)
.build()
NotificationManagerCompat.from(context)
.notify(scheduleId.toInt(), notification)
}
}
2.4. Daily Summary Notifications
class DailySummaryWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val tomorrow = EAFCalculator.todayRataDie() + 1
val schedules = repository.getSchedulesForDay(tomorrow)
if (schedules.isEmpty()) return Result.success()
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_SUMMARIES)
.setSmallIcon(R.drawable.ic_calendar)
.setContentTitle("Tomorrow's Schedule")
.setContentText("You have ${schedules.size} event(s)")
.setStyle(NotificationCompat.InboxStyle().apply {
schedules.take(5).forEach { schedule ->
addLine("${schedule.startTime} - ${schedule.title}")
}
if (schedules.size > 5) {
addLine("and ${schedules.size - 5} more...")
}
})
.build()
NotificationManagerCompat.from(applicationContext)
.notify(NOTIFICATION_DAILY_SUMMARY, notification)
return Result.success()
}
}
// Schedule daily at 8 PM
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"daily_summary",
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<DailySummaryWorker>(1, TimeUnit.DAYS)
.setInitialDelay(calculateDelayUntil20_00(), TimeUnit.MILLISECONDS)
.build()
)
3. Smart Notification Features
3.1. Adaptive Timing
fun calculateOptimalReminderTime(schedule: Schedule): Long {
val userPreferences = getUserPreferences()
val scheduleType = classifySchedule(schedule)
return when (scheduleType) {
ScheduleType.MEETING -> userPreferences.meetingReminderMinutes
ScheduleType.TASK -> userPreferences.taskReminderMinutes
ScheduleType.EVENT -> userPreferences.eventReminderMinutes
else -> 15 // Default 15 minutes
} * 60 * 1000
}
3.2. Notification Grouping
fun createGroupedNotification(schedules: List<Schedule>) {
// Individual notifications
schedules.forEach { schedule ->
val notification = createScheduleNotification(schedule)
.setGroup(GROUP_SCHEDULE_REMINDERS)
.build()
notify(schedule.id.toInt(), notification)
}
// Summary notification
val summaryNotification = NotificationCompat.Builder(context, CHANNEL_REMINDERS)
.setSmallIcon(R.drawable.ic_calendar)
.setContentTitle("${schedules.size} upcoming events")
.setGroup(GROUP_SCHEDULE_REMINDERS)
.setGroupSummary(true)
.build()
notify(NOTIFICATION_GROUP_SUMMARY, summaryNotification)
}
4. Widget Performance Optimization
4.1. Lazy Loading
@Composable
fun DayGrid(schedules: List<Schedule>) {
LazyVerticalGrid(
columns = GridCells.Fixed(7),
modifier = GlanceModifier.fillMaxSize()
) {
items(42) { index -> // 6 weeks × 7 days
val rataDie = calculateRataDieForGridPosition(index)
DayCell(
rataDie = rataDie,
hasSchedules = schedules.any { it.startRataDie == rataDie }
)
}
}
}
4.2. Minimal Data Transfer
// Don't transfer full schedule objects to widget
data class WidgetScheduleData(
val rataDie: Int,
val hasSchedule: Boolean,
val scheduleCount: Int
)
// In repository
suspend fun getWidgetData(startRataDie: Int, endRataDie: Int): List<WidgetScheduleData> {
return dao.getScheduleCountsByDay(startRataDie, endRataDie)
}
// Optimized query
@Query("""
SELECT startRataDie as rataDie, COUNT(*) as count
FROM schedules
WHERE startRataDie >= :start AND startRataDie <= :end
GROUP BY startRataDie
""")
suspend fun getScheduleCountsByDay(start: Int, end: Int): List<DayScheduleCount>
5. Permission Handling
5.1. Notification Permissions (Android 13+)
fun requestNotificationPermission(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_NOTIFICATION_PERMISSION
)
}
}
}
5.2. Exact Alarm Permission
fun requestExactAlarmPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
if (!alarmManager.canScheduleExactAlarms()) {
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).also {
context.startActivity(it)
}
}
}
}
6. Testing Widgets and Notifications
6.1. Widget Preview
@Preview(widthDp = 200, heightDp = 200)
@Composable
fun MonthWidgetPreview() {
MonthWidgetContent()
}
6.2. Notification Testing
@Test
fun testReminderScheduling() {
val schedule = Schedule(
id = 1,
title = "Meeting",
startRataDie = EAFCalculator.todayRataDie() + 1,
startTime = "14:00"
)
scheduleReminder(context, schedule, minutesBefore = 15)
// Verify alarm was scheduled
// (Would use ShadowAlarmManager in Robolectric test)
}
7. User Preferences
7.1. Notification Settings
@Composable
fun NotificationSettingsScreen() {
var enableReminders by rememberPreference(KEY_ENABLE_REMINDERS, true)
var defaultReminderTime by rememberPreference(KEY_DEFAULT_REMINDER, 15)
var enableDailySummary by rememberPreference(KEY_ENABLE_SUMMARY, true)
Column {
SwitchPreference(
title = "Enable Reminders",
checked = enableReminders,
onCheckedChange = { enableReminders = it }
)
SliderPreference(
title = "Default Reminder Time",
value = defaultReminderTime,
valueRange = 5f..60f,
steps = 11,
onValueChange = { defaultReminderTime = it.toInt() },
valueLabel = "$defaultReminderTime minutes before"
)
SwitchPreference(
title = "Daily Summary",
summary = "Get tomorrow's schedule at 8 PM",
checked = enableDailySummary,
onCheckedChange = { enableDailySummary = it }
)
}
}
Conclusion
Advanced features implemented:
✅ Three widget variants (Month, Agenda, Mini) ✅ Efficient widget update system ✅ Smart reminder notifications ✅ Daily summary notifications ✅ Notification grouping ✅ Permission handling ✅ User preference controls
These features keep users engaged even when the app isn’t open!
Next Episode Preview:
- Final polish and release preparation
- Play Store listing optimization
- Analytics integration
- Post-launch monitoring
Making the calendar truly ubiquitous!
![[Dalendar DevLog 2] Solid Foundation - Architecture and Data Structure Design](/images/blog/dalendar_dev_2_architecture.png)
![[Dalendar DevLog 3] Database Integration and Schedule Management](/images/blog/dalendar_dev_3_database.png)
![[Dalendar DevLog 4] UI Implementation - Schedule Creation and Editing](/images/blog/dalendar_dev_4_ui.png)