Skip to main content

How to Make a Calendar App

Copied to clipboard!
No boilerplate memorization required

How to Vibe Code a Native Calendar & Planner App

Stay in your creative flow. Direct AI models with high-level intent, build an interactive month/week strip and time-blocking agenda in Jetpack Compose, and run your personal planner APK in under 30 minutes.

Core Stack

Kotlin Compose

Date Engine

java.time LocalDate

AI Partner

Claude / Gemini

Target Time

20–30 Mins

The Philosophy

The Vibe Coding Mobile Philosophy

Building a calendar app the old-fashioned way meant wrestling legacy java.util.Calendar zero-indexed months, nested RecyclerViews for day cells, and complex alarm broadcasts. Vibe coding lets you focus on the daily planning experience: you describe the date strip, color-coded time blocks, and task checkboxes while your AI assistant generates clean java.time.LocalDate Compose code in a single file.

Old-School Mobile Stress

  • Fighting off-by-one month bugs in legacy java.util.Calendar and SimpleDateFormat.
  • Writing three separate RecyclerView adapters for Month, Week, and Day agenda views.
  • Manually wiring complex SQLite joins to filter events by selected date.
  • Spending hours building a custom time-slot picker dialog in XML.

The Vibe Coder Way

  • Modern java.time.LocalDate math rendered in a reactive Jetpack Compose date strip and month grid.
  • Describe the vibe: "Sleek daily time-blocking planner with event indicator dots and quick-add bottom sheet."
  • Ask AI to group events by LocalDate in memory or Room with zero boilerplate.
  • Deploy directly to your phone over Wi-Fi and plan your real week immediately.
Execution

The 4-Step Blueprint

From an empty Compose project to a daily schedule and time-blocking planner on your phone.

Step 01

Spawning the Empty Compose Canvas

Open Android Studio and select New Project > Empty Activity (with Compose logo). Name it VibeCalendar with minimum SDK 26+ (so java.time works out of the box).

💡 Pro Vibe Tip: Setting minSdk = 26 gives LLMs native access to LocalDate and LocalTime without needing core library desugaring in Gradle.
Step 02

The "One-Shot Architect" Prompt

Prompt the AI for a unified screen: a selectable 7-day or month calendar header, event indicator dots on busy dates, and a chronological daily timeline below.

🔑 Key Constraint: Tell the AI: "Keep all UI, LocalDate state, and event models inside one MainActivity.kt so I can run it immediately."
Step 03

Time-Blocking & Micro-Interactions

Add color-coded category tags (Deep Work, Meeting, Personal, Fitness), a quick-add modal bottom sheet, and smooth animated transitions when switching days.

✨ Iteration Secret: Use Compose @Preview to check how empty days and packed schedules render side by side.
Step 04

Run on Your Real Phone

Pair your Android phone via Wireless Debugging, hit Run (Shift+F10), and test adding and checking off events for today and tomorrow.

🏆 The Grand Payoff: You own a fast, distraction-free daily planner and time-blocker customized around your personal workflow.
Interactive Tool

Vibe Prompt Configurator

Pick your planner aesthetic, storage layer, and scheduling features to generate a ready-to-paste LLM prompt.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file Compose ready
Product Spec

Essential Elements of a Calendar App

To deliver a comprehensive, stress-free scheduling experience, make sure your calendar app includes these core pillars:

  • Effortless Schedule Management

    Let users add, edit, and delete events in just a few taps, and switch smoothly between Day, Week, and Month views to plan ahead.

  • Notifications & Recurring Reminders

    Trigger timely alerts before appointments with location or contact notes, plus recurring schedules so weekly routines are never missed.

  • Integrated Task Management

    Combine calendar appointments with checkable to-do tasks and deadlines in one unified daily view so nothing falls through the cracks.

  • Visual Time Blocking

    Allow users to dedicate color-coded blocks of time for deep work, calls, or workouts to maximize daily focus and productivity.

  • Cross-Device Sync & Export

    Keep schedules consistent across devices or allow quick ICS/JSON import and export so events are accessible anywhere.

  • Privacy, Security & Clean UI

    Protect personal schedule data locally (with optional biometric/PIN lock) inside an uncluttered, high-contrast calendar interface.

UX Checklist Before Shipping: Highlight "Today" unmistakably, show subtle indicator dots on days that have events, and always provide a one-tap "Today" button when browsing future months.
Starter Template

The "Golden" Single-File Compose Template

Replace the contents of your MainActivity.kt with this to run a working time-blocking calendar app right now.

app/src/main/java/.../MainActivity.kt
Kotlin + Compose
package com.example.vibecalendar

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.time.LocalDate
import java.time.format.TextStyle
import java.util.Locale
import java.util.UUID

data class CalendarEvent(
    val id: String = UUID.randomUUID().toString(),
    val date: LocalDate,
    val timeBlock: String,
    val title: String,
    val category: String = "Work",
    val isDone: Boolean = false
)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                colorScheme = darkColorScheme(
                    background = Color(0xFF0B111E),
                    surface = Color(0xFF151F32),
                    primary = Color(0xFF6366F1)
                )
            ) {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    VibeCalendarScreen()
                }
            }
        }
    }
}

@Composable
fun VibeCalendarScreen() {
    val today = remember { LocalDate.now() }
    var selectedDate by remember { mutableStateOf(today) }
    var events by remember {
        mutableStateOf(
            listOf(
                CalendarEvent(date = today, timeBlock = "09:00 AM", title = "Deep Work: Architecture Review", category = "Focus"),
                CalendarEvent(date = today, timeBlock = "01:30 PM", title = "Product Design Sync", category = "Work"),
                CalendarEvent(date = today.plusDays(1), timeBlock = "10:00 AM", title = "Ship Calendar APK to Phone", category = "Launch")
            )
        )
    }
    var newTitle by remember { mutableStateOf("") }
    var newTime by remember { mutableStateOf("11:00 AM") }

    val weekDays = remember(today) { (-2..11).map { today.plusDays(it.toLong()) } }
    val dayEvents = events.filter { it.date == selectedDate }

    Scaffold(
        topBar = {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .statusBarsPadding()
                    .padding(horizontal = 20.dp, vertical = 16.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column {
                    Text(
                        text = "${selectedDate.month.getDisplayName(TextStyle.FULL, Locale.US)} ${selectedDate.year}",
                        fontSize = 24.sp,
                        fontWeight = FontWeight.ExtraBold,
                        color = Color.White
                    )
                    Text(
                        text = "${dayEvents.size} scheduled items • time-block flow",
                        fontSize = 13.sp,
                        color = Color(0xFF94A3B8)
                    )
                }
                TextButton(onClick = { selectedDate = today }) {
                    Text("Today", color = Color(0xFF818CF8), fontWeight = FontWeight.Bold)
                }
            }
        },
        containerColor = Color.Transparent
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .fillMaxSize()
                .padding(horizontal = 20.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // Horizontal Date Strip
            LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
                items(weekDays) { date ->
                    val isSelected = date == selectedDate
                    val hasEvents = events.any { it.date == date }
                    Column(
                        modifier = Modifier
                            .width(58.dp)
                            .clip(RoundedCornerShape(16.dp))
                            .background(if (isSelected) Color(0xFF6366F1) else Color(0xFF151F32))
                            .clickable { selectedDate = date }
                            .padding(vertical = 12.dp),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.US),
                            fontSize = 11.sp,
                            color = if (isSelected) Color.White else Color(0xFF94A3B8)
                        )
                        Spacer(modifier = Modifier.height(4.dp))
                        Text(
                            text = date.dayOfMonth.toString(),
                            fontSize = 18.sp,
                            fontWeight = FontWeight.ExtraBold,
                            color = Color.White
                        )
                        Spacer(modifier = Modifier.height(6.dp))
                        Box(
                            modifier = Modifier
                                .size(6.dp)
                                .clip(CircleShape)
                                .background(
                                    when {
                                        hasEvents && isSelected -> Color.White
                                        hasEvents -> Color(0xFF34D399)
                                        else -> Color.Transparent
                                    }
                                )
                        )
                    }
                }
            }

            // Quick Add Time-Block Row
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                OutlinedTextField(
                    value = newTime,
                    onValueChange = { newTime = it },
                    modifier = Modifier.width(108.dp),
                    singleLine = true,
                    label = { Text("Time", fontSize = 11.sp) }
                )
                OutlinedTextField(
                    value = newTitle,
                    onValueChange = { newTitle = it },
                    modifier = Modifier.weight(1f),
                    singleLine = true,
                    label = { Text("Add time block or task...", fontSize = 11.sp) }
                )
                FloatingActionButton(
                    onClick = {
                        if (newTitle.isNotBlank()) {
                            events = events + CalendarEvent(
                                date = selectedDate,
                                timeBlock = newTime.ifBlank { "All Day" },
                                title = newTitle.trim()
                            )
                            newTitle = ""
                        }
                    },
                    containerColor = Color(0xFF6366F1),
                    contentColor = Color.White,
                    modifier = Modifier.size(50.dp)
                ) {
                    Icon(Icons.Default.Add, contentDescription = "Add Event")
                }
            }

            // Daily Agenda List
            LazyColumn(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                items(dayEvents, key = { it.id }) { ev ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clip(RoundedCornerShape(16.dp))
                            .background(Color(0xFF151F32))
                            .clickable {
                                events = events.map { if (it.id == ev.id) it.copy(isDone = !it.isDone) else it }
                            }
                            .padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Column(modifier = Modifier.width(76.dp)) {
                            Text(ev.timeBlock, color = Color(0xFF818CF8), fontWeight = FontWeight.Bold, fontSize = 12.sp)
                            Text(ev.category, color = Color(0xFF64748B), fontSize = 11.sp)
                        }
                        Column(modifier = Modifier.weight(1f)) {
                            Text(
                                text = ev.title,
                                color = if (ev.isDone) Color(0xFF64748B) else Color.White,
                                textDecoration = if (ev.isDone) TextDecoration.LineThrough else TextDecoration.None,
                                fontWeight = FontWeight.SemiBold,
                                fontSize = 15.sp
                            )
                        }
                        IconButton(onClick = { events = events.filter { it.id != ev.id } }) {
                            Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color(0xFFF43F5E))
                        }
                    }
                }
            }
        }
    }
}
Troubleshooting

How to Debug Without Losing Your Vibe

Keep date math and notifications smooth with these three vibe debugging rules:

The Error Dump Rule

If LocalDate shows an API level warning, paste the warning into your AI chat: "Set minSdk = 26 in build.gradle.kts or show me coreLibraryDesugaring so java.time compiles cleanly."

Immutable LocalDate Keys

Avoid mutable `Date` objects that fail to trigger Compose recomposition: "Use immutable java.time.LocalDate and YearMonth for all calendar state so Compose updates immediately."

Instant Wireless Run

Connect your Android phone over Wi-Fi once and use Apply Changes to test horizontal week-strip scrolling at 60fps.

Extensions

The Vibe Prompt Vault

Copy-paste prompts to expand your planner with full month grids, notifications, and Pomodoro time-blocking.

🗓️ Prompt: Add an Expandable 7-Column Full Month Grid View
Prompt Template

"Update my Jetpack Compose Calendar app in MainActivity.kt so tapping the Month/Year title smoothly expands and collapses between the 7-day week strip and a full 7-column YearMonth calendar grid with previous/next month arrows and colored event indicator dots."

🔔 Prompt: Schedule Exact Event Reminders with AlarmManager
Prompt Template

"Add local push notifications to my Compose Calendar app. When a user creates an event with a reminder toggle, schedule a notification 15 minutes before the event time using WorkManager or AlarmManager, including Android 13+ POST_NOTIFICATIONS permission handling."

⏱️ Prompt: Add a Focus Timer Inside Each Time Block
Prompt Template

"Allow tapping any scheduled time block in my Compose Calendar app to launch a 25-minute countdown focus timer sheet tied to that event, automatically marking the time block as completed when the timer finishes."

We use cookies for ads and analytics.