Build a Pomodoro Timer & Package It as a DMG — Compose Desktop | Kotlin Desktop Lesson 20
Video: Build a Pomodoro Timer & Package It as a DMG — Compose Desktop | Kotlin Desktop Lesson 20 by Taught by Celeste AI - AI Coding Coach
Watch full page →Build a Pomodoro Timer & Package It as a DMG — Compose Desktop
This tutorial demonstrates how to create a Pomodoro Timer using Jetpack Compose Desktop featuring a circular progress ring and phase-specific colors. It also covers configuring Gradle's nativeDistributions block to package the app as a native macOS DMG installer with a single command.
Code
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import java.awt.Toolkit
enum class Phase(val durationSeconds: Int, val color: androidx.compose.ui.graphics.Color) {
Work(25 * 60, androidx.compose.ui.graphics.Color(0xFF4CAF50)), // Green
ShortBreak(5 * 60, androidx.compose.ui.graphics.Color(0xFF2196F3)), // Blue
LongBreak(15 * 60, androidx.compose.ui.graphics.Color(0xFFFFC107)) // Amber
}
@Composable
@Preview
fun PomodoroTimer() {
var phase by remember { mutableStateOf(Phase.Work) }
var secondsLeft by remember { mutableStateOf(phase.durationSeconds) }
var sessionCount by remember { mutableStateOf(0) }
// Countdown timer effect
LaunchedEffect(phase, secondsLeft) {
if (secondsLeft > 0) {
delay(1000L)
secondsLeft--
} else {
Toolkit.getDefaultToolkit().beep() // Alert on phase end
// Advance phase logic
when (phase) {
Phase.Work -> {
sessionCount++
phase = if (sessionCount % 4 == 0) Phase.LongBreak else Phase.ShortBreak
secondsLeft = phase.durationSeconds
}
else -> {
phase = Phase.Work
secondsLeft = phase.durationSeconds
}
}
}
}
val progress = 1f - secondsLeft.toFloat() / phase.durationSeconds
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally
) {
// Circular progress ring
Canvas(modifier = Modifier.size(200.dp)) {
val strokeWidth = 20f
val radius = size.minDimension / 2 - strokeWidth / 2
drawArc(
color = phase.color,
startAngle = -90f,
sweepAngle = 360f * progress,
useCenter = false,
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
}
Spacer(Modifier.height(24.dp))
Text(
text = "${secondsLeft / 60}:${(secondsLeft % 60).toString().padStart(2, '0')}",
style = MaterialTheme.typography.displayLarge,
color = phase.color
)
Spacer(Modifier.height(16.dp))
Text(
text = when (phase) {
Phase.Work -> "Work"
Phase.ShortBreak -> "Short Break"
Phase.LongBreak -> "Long Break"
},
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(32.dp))
// Session tracking dots
Row {
repeat(4) { index ->
Canvas(
modifier = Modifier.size(24.dp).padding(4.dp)
) {
drawCircle(
color = if (index < sessionCount % 4) phase.color else phase.color.copy(alpha = 0.3f),
style = if (index < sessionCount % 4) Stroke(width = 0f) else Stroke(width = 2f)
)
}
}
}
}
}
// Gradle nativeDistributions block example (build.gradle.kts):
/*
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg)
packageName = "PomodoroTimer"
packageVersion = "1.0.0"
vendor = "YourName"
description = "A Pomodoro Timer built with Compose Desktop"
macOS {
bundleID = "com.yourname.pomodoro"
}
}
}
}
*/
// To package the app as a DMG on macOS, run:
// ./gradlew packageDmg
Key Points
- Use Compose Desktop's Canvas and drawArc with StrokeCap.Round to create smooth circular progress indicators.
- Manage Pomodoro phases and countdown with LaunchedEffect and mutable state, auto-advancing between work and break sessions.
- Track completed work sessions visually using filled and outlined circles drawn on Canvas.
- Configure Gradle's nativeDistributions block to specify package metadata and target native installer formats like DMG for macOS.
- Build native installers easily via Gradle commands such as
./gradlew packageDmgfor macOS packaging.