A cross-platform countdown & stopwatch app built with Kotlin Multiplatform (KMP).
It shares timer logic between Android and iOS using StateFlow, and integrates with ActivityKit on iOS to show real-time countdowns on the Lock Screen and Dynamic Island.
| Layer | Technology |
|---|---|
| Shared Code | Kotlin Multiplatform (KMP), Coroutines, Flow |
| Android | Jetpack Compose |
| iOS | SwiftUI, ActivityKit, WidgetKit |
| Interop | KMP Native Coroutines (com.rickclephas.kmp.nativecoroutines) |
| DI | Koin |
| Language Level | Kotlin 2.x, Swift 6 concurrency |
- ⏳ Countdown timer & ⏱ Stopwatch
- ⏸ Pause / ▶ Resume / ⏹ Stop support
- 🔁 Real-time synchronization between KMP & SwiftUI
- 📲 iOS Live Activity (Dynamic Island + Lock Screen)
- 🧩 Shared StateFlow observed in Swift via
asyncSequence(for:) - 💾 Local persistence via
TimerPersistence - 💡 Millisecond-accurate pause/resume sync
┌────────────────────────────┐
│ Compose UI │
│ (Android) │
└──────────────┬─────────────┘
│
▼
┌────────────────────────────┐
│ Shared KMP Module │
│ ─ TimerRepository.kt │
│ ─ TimerState (StateFlow) │
│ ─ TimerController expect/actual │
│ ─ TimerPersistence expect/actual│
└──────────────┬─────────────┘
│
▼
┌────────────────────────────┐
│ iOS App │
│ (SwiftUI + ActivityKit) │
│ LiveActivityManager.swift │
│ TimerWidget.swift │
└────────────────────────────┘
val elapsed = if (_timerState.value.mode == TimerMode.STOPWATCH) {
initialMillis + (currentTimeMillis() - start)
} else {
max(initialMillis - (currentTimeMillis() - start), 0L)
}
_timerState.update { it.copy(elapsedTime = elapsed) }let sequence = asyncSequence(for: repository.timerStateFlow)
for try await state in sequence {
if state.isRunning {
await self.updateActivity(
elapsedTime: state.elapsedTime,
mode: state.mode.name
)
}
}Pausing and resuming are synchronized with millisecond accuracy to avoid drift between platforms.
- Android Studio Koala+ (KMP-ready)
- Xcode 15+
- Kotlin 2.x
- Swift 6 (with strict concurrency)
- iOS 17+ for Live Activity support
git clone https://github.com/yourusername/TimerZeerKMP.git
cd TimerZeerKMP# Open in Android Studio
# Select the `androidApp` run configuration
# Run on emulator or device# Open `TimerZeerKMP.xcworkspace` in Xcode
# Choose a physical iPhone target (required for Live Activities)
# Run the app
# Start a timer → Live Activity appears on Lock Screen / Dynamic Islandexpect interface TimerController {
fun start(durationInSeconds: Long)
fun pause()
fun resume()
fun stop()
}public class LiveActivityManager: NSObject, @preconcurrency TimerController {
@MainActor
public func start(durationInSeconds: Int64) { ... }
public func pause() { ... }
public func resume() { ... }
public func stop() { ... }
}| Issue | Cause | Fix |
|---|---|---|
| 1-second drift after pause | rounding seconds | store remaining time in milliseconds |
| Initial lag | delay(1.seconds) before start |
remove it |
| Countdown vs Stopwatch mismatch | wrong elapsed calculation | use max(initial - delta, 0L) for countdown |
- ☄️Firebase notification handling
- 📆Data picker for iOS
- 🔔 Notifications when countdown ends
| Android | IOS | Live Activity |
|---|---|---|
![]() |
![]() |
![]() |
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files...


