
Unified clipboard change monitoring with configurable polling and debouncing, duplicate filtering, reactive streams, builder presets for high-frequency/low-power, rich content detection and error handling.
A Kotlin Multiplatform library for monitoring system clipboard changes across all major platforms.
Clipboard Manager provides a unified interface for tracking clipboard changes in real-time. The library automatically selects the optimal implementation based on your target platform, using native clipboard APIs for efficient monitoring.
Add the dependency to your project:
dependencies {
implementation("io.github.niyajali:clipboard-manager:1.0.0")
}val listener = object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
println("Clipboard changed: ${content.text}")
}
}
// Simple usage (backward compatible)
val monitor = ClipboardMonitorFactory.create(listener)
monitor.start()
// Or using new Builder API (recommended)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.build()
monitor.start()
// When finished
monitor.stop()val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.setDebounceDelay(100) // Android debouncing (0-1000ms)
.setPollingInterval(250) // Polling for macOS/Linux/iOS/JS (50-5000ms)
.enableDuplicateFiltering(true) // Filter duplicate clipboard content
.setErrorHandler { error -> // Custom error handling
logger.error("Clipboard error", error)
}
.build()
monitor.start()// High-frequency monitoring (clipboard history apps)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useHighFrequencyPreset() // 0ms debounce, 100ms polling
.build()
// Low-power monitoring (mobile/battery-conscious apps)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useLowPowerPreset() // 200ms debounce, 1000ms polling
.build()
// Balanced monitoring (default settings)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useBalancedPreset() // 50ms debounce, 200ms polling
.build()The library initializes automatically via AndroidX Startup. No manual setup required.
For manual initialization (if AndroidX Startup is disabled):
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
ClipboardMonitorFactory.init(this)
}
}// Monitor all clipboard changes
ClipboardMonitorFactory.create(listener)
.asFlow()
.collect { content ->
println("Content: ${content.text}")
}
// Monitor only text changes
ClipboardMonitorFactory.create(listener)
.textFlow()
.collect { text ->
println("Text: $text")
}
// Monitor file changes
ClipboardMonitorFactory.create(listener)
.filesFlow()
.collect { files ->
println("Files: ${files.joinToString()}")
}
// Monitor image availability
ClipboardMonitorFactory.create(listener)
.imageAvailableFlow()
.collect { hasImage ->
println("Image available: $hasImage")
}Main interface for monitoring clipboard changes. Provides lifecycle methods for starting and stopping monitoring.
Methods:
start() - Begins monitoring clipboard changesstop() - Stops monitoring and releases resourcesisRunning() - Returns monitoring statusgetCurrentContent() - Retrieves current clipboard content synchronouslyCallback interface for receiving clipboard change notifications.
Method:
onClipboardChange(content: ClipboardContent) - Called when clipboard content changesData class representing clipboard content with support for multiple formats.
Properties:
text: String? - Plain text contenthtml: String? - HTML formatted contentrtf: String? - Rich Text Format contentfiles: List<String>? - List of file paths or URIsimageAvailable: Boolean - Indicates if image is presenttimestamp: Long - Unix timestamp in millisecondsHelper Methods:
isEmpty() - Checks if clipboard is emptyisNotEmpty() - Checks if clipboard has contenttoString() - Returns concise content summaryFluent builder for creating configured clipboard monitors.
Configuration Methods:
setListener(listener) - Set clipboard change listener (required)setDebounceDelay(ms) - Set debounce delay for Android (0-1000ms, default: 50ms)setPollingInterval(ms) - Set polling interval for platforms that use polling (50-5000ms,
default: 200ms)enableDuplicateFiltering(enable) - Enable/disable duplicate content filtering (default: true)setErrorHandler(handler) - Set custom error handler (optional)Preset Methods:
useHighFrequencyPreset() - Optimized for responsiveness (0ms debounce, 100ms polling)useLowPowerPreset() - Optimized for battery life (200ms debounce, 1000ms polling)useBalancedPreset() - Balanced settings (50ms debounce, 200ms polling)Build Methods:
buildConfig() - Build configuration only (returns ClipboardConfig)build() - Build and create monitor (returns ClipboardMonitor)Immutable configuration data class for clipboard monitors.
Properties:
listener: ClipboardListener - Clipboard change listenerdebounceDelayMs: Long - Debounce delay in milliseconds (default: 50ms)pollingIntervalMs: Long - Polling interval in milliseconds (default: 200ms)enableDuplicateFiltering: Boolean - Enable duplicate filtering (default: true)errorHandler: ((Throwable) -> Unit)? - Optional error handlerFactory Methods:
default(listener) - Create config with default settingshighFrequency(listener) - Create high-frequency configlowPower(listener) - Create low-power configFactory object for creating platform-specific clipboard monitors using Strategy Pattern.
Methods:
create(config: ClipboardConfig) - Create monitor with configurationcreate(listener: ClipboardListener) - Create monitor with default configuration (backward
compatible)Android-specific:
init(context: Context) - Manual initialization (optional)isInitialized() - Check initialization status| Option | Default | Range | Applicable Platforms |
|---|---|---|---|
debounceDelayMs |
50ms | 0-1000ms | Android, Windows |
pollingIntervalMs |
200ms | 50-5000ms | macOS, Linux, iOS, JS |
enableDuplicateFiltering |
true | boolean | All platforms |
errorHandler |
null | function | All platforms |
Uses Android ClipboardManager with OnPrimaryClipChangedListener.
Features:
Limitations:
Permissions: No special permissions required.
Uses native Win32 clipboard notifications via message-only window.
Features:
Implementation:
AddClipboardFormatListener Win32 APIUses AWT Toolkit clipboard with periodic polling.
Features:
Implementation:
Toolkit.getDefaultToolkit().systemClipboardScheduledExecutorService for pollingUses UIPasteboard with change count polling.
Features:
Permissions: iOS 14+ shows paste notification banner on first access. No explicit permission required.
Uses browser Clipboard API with periodic polling.
Features:
Requirements:
Limitations:
Browser Compatibility:
// All clipboard changes as Flow
fun ClipboardMonitor.asFlow(): Flow<ClipboardContent>
// Only text changes
fun ClipboardMonitor.textFlow(includeEmpty: Boolean = false): Flow<String?>
// Only file changes
fun ClipboardMonitor.filesFlow(): Flow<List<String>>
// Image availability changes
fun ClipboardMonitor.imageAvailableFlow(): Flow<Boolean>// Transform successful results
fun <T, R> ClipboardResult<T>.map(transform: (T) -> R): ClipboardResult<R>
// Chain result operations
fun <T, R> ClipboardResult<T>.flatMap(transform: (T) -> ClipboardResult<R>): ClipboardResult<R>
// Extract values
fun <T> ClipboardResult<T>.getOrNull(): T?
fun <T> ClipboardResult<T>.getOrDefault(default: T): T
fun <T> ClipboardResult<T>.getOrThrow(): T
// Callbacks for different result types
fun <T> ClipboardResult<T>.onSuccess(block: (T) -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onEmpty(block: () -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onPermissionDenied(block: (String?) -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onError(block: (String, Throwable?) -> Unit): ClipboardResult<T>
// Status checks
fun <T> ClipboardResult<T>.isSuccess(): Boolean
fun <T> ClipboardResult<T>.isFailure(): Boolean
fun <T> ClipboardResult<T>.isEmpty(): BooleanAll monitor implementations are thread-safe and can be called from any thread.
Callback Threading:
UI updates are safe from callbacks on Android and iOS. Other platforms require proper thread handling.
Always call stop() when finished monitoring to release resources:
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.build()
try {
monitor.start()
// ... use monitor ...
} finally {
monitor.stop()
}Or use Kotlin coroutines with automatic cleanup:
coroutineScope {
ClipboardMonitor.Builder()
.setListener(listener)
.build()
.asFlow()
.collect { content ->
// Process content
}
// Automatically cleaned up when scope exits
}| Content Type | Android | Windows | macOS | Linux | iOS | JS/WasmJS |
|---|---|---|---|---|---|---|
| Plain Text | Yes | Yes | Yes | Yes | Yes | Yes |
| HTML | Yes | Yes | Yes | Yes | Yes | Limited |
| RTF | No | Yes | Yes | Yes | Yes | No |
| Files | Yes¹ | Yes | Yes | Yes | Yes² | Limited |
| Images | Yes³ | Yes³ | Yes³ | Yes³ | Yes³ | Limited |
¹ Android provides content URIs (content://...)
² iOS provides file paths via URLs
³ Detection only; actual image data requires platform-specific APIs
The library handles errors gracefully with configurable error handling:
val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.setErrorHandler { error ->
when (error) {
is SecurityException -> Log.e("Clipboard", "Permission denied")
is IllegalStateException -> Log.e("Clipboard", "Invalid state")
else -> Log.e("Clipboard", "Error: ${error.message}", error)
}
}
.build()Listener exceptions are caught internally to prevent monitoring interruption.
class ClipboardHistory {
private val history = mutableListOf<ClipboardContent>()
private val monitor = ClipboardMonitor.Builder()
.setListener(object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
if (content.isNotEmpty()) {
history.add(content)
if (history.size > 100) {
history.removeAt(0)
}
}
}
})
.useHighFrequencyPreset() // Immediate updates
.build()
fun start() = monitor.start()
fun stop() = monitor.stop()
fun getHistory(): List<ClipboardContent> = history.toList()
}class ClipboardSync(private val remoteSync: suspend (String) -> Unit) {
private val monitor = ClipboardMonitor.Builder()
.setListener(object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
content.text?.let { text ->
coroutineScope.launch {
remoteSync(text)
}
}
}
})
.setDebounceDelay(100) // Debounce before syncing
.setErrorHandler { error ->
logger.error("Sync failed", error)
}
.build()
fun start() = monitor.start()
fun stop() = monitor.stop()
}// Android or iOS
val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.useLowPowerPreset() // Battery-conscious settings
.setErrorHandler { error ->
Timber.e(error, "Clipboard error")
}
.build()single<ClipboardMonitor> {
ClipboardMonitor.Builder()
.setListener(get())
.setDebounceDelay(100)
.build()
}@Module
@InstallIn(SingletonComponent::class)
object ClipboardModule {
@Provides
@Singleton
fun provideClipboardMonitor(
listener: ClipboardListener,
@ApplicationContext context: Context
): ClipboardMonitor {
ClipboardMonitorFactory.init(context)
return ClipboardMonitor.Builder()
.setListener(listener)
.setDebounceDelay(100)
.build()
}
}Copyright 2025 Sk Niyaj Ali
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome. Please ensure:
Sk Niyaj Ali - https://github.com/niyajali
A Kotlin Multiplatform library for monitoring system clipboard changes across all major platforms.
Clipboard Manager provides a unified interface for tracking clipboard changes in real-time. The library automatically selects the optimal implementation based on your target platform, using native clipboard APIs for efficient monitoring.
Add the dependency to your project:
dependencies {
implementation("io.github.niyajali:clipboard-manager:1.0.0")
}val listener = object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
println("Clipboard changed: ${content.text}")
}
}
// Simple usage (backward compatible)
val monitor = ClipboardMonitorFactory.create(listener)
monitor.start()
// Or using new Builder API (recommended)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.build()
monitor.start()
// When finished
monitor.stop()val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.setDebounceDelay(100) // Android debouncing (0-1000ms)
.setPollingInterval(250) // Polling for macOS/Linux/iOS/JS (50-5000ms)
.enableDuplicateFiltering(true) // Filter duplicate clipboard content
.setErrorHandler { error -> // Custom error handling
logger.error("Clipboard error", error)
}
.build()
monitor.start()// High-frequency monitoring (clipboard history apps)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useHighFrequencyPreset() // 0ms debounce, 100ms polling
.build()
// Low-power monitoring (mobile/battery-conscious apps)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useLowPowerPreset() // 200ms debounce, 1000ms polling
.build()
// Balanced monitoring (default settings)
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.useBalancedPreset() // 50ms debounce, 200ms polling
.build()The library initializes automatically via AndroidX Startup. No manual setup required.
For manual initialization (if AndroidX Startup is disabled):
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
ClipboardMonitorFactory.init(this)
}
}// Monitor all clipboard changes
ClipboardMonitorFactory.create(listener)
.asFlow()
.collect { content ->
println("Content: ${content.text}")
}
// Monitor only text changes
ClipboardMonitorFactory.create(listener)
.textFlow()
.collect { text ->
println("Text: $text")
}
// Monitor file changes
ClipboardMonitorFactory.create(listener)
.filesFlow()
.collect { files ->
println("Files: ${files.joinToString()}")
}
// Monitor image availability
ClipboardMonitorFactory.create(listener)
.imageAvailableFlow()
.collect { hasImage ->
println("Image available: $hasImage")
}Main interface for monitoring clipboard changes. Provides lifecycle methods for starting and stopping monitoring.
Methods:
start() - Begins monitoring clipboard changesstop() - Stops monitoring and releases resourcesisRunning() - Returns monitoring statusgetCurrentContent() - Retrieves current clipboard content synchronouslyCallback interface for receiving clipboard change notifications.
Method:
onClipboardChange(content: ClipboardContent) - Called when clipboard content changesData class representing clipboard content with support for multiple formats.
Properties:
text: String? - Plain text contenthtml: String? - HTML formatted contentrtf: String? - Rich Text Format contentfiles: List<String>? - List of file paths or URIsimageAvailable: Boolean - Indicates if image is presenttimestamp: Long - Unix timestamp in millisecondsHelper Methods:
isEmpty() - Checks if clipboard is emptyisNotEmpty() - Checks if clipboard has contenttoString() - Returns concise content summaryFluent builder for creating configured clipboard monitors.
Configuration Methods:
setListener(listener) - Set clipboard change listener (required)setDebounceDelay(ms) - Set debounce delay for Android (0-1000ms, default: 50ms)setPollingInterval(ms) - Set polling interval for platforms that use polling (50-5000ms,
default: 200ms)enableDuplicateFiltering(enable) - Enable/disable duplicate content filtering (default: true)setErrorHandler(handler) - Set custom error handler (optional)Preset Methods:
useHighFrequencyPreset() - Optimized for responsiveness (0ms debounce, 100ms polling)useLowPowerPreset() - Optimized for battery life (200ms debounce, 1000ms polling)useBalancedPreset() - Balanced settings (50ms debounce, 200ms polling)Build Methods:
buildConfig() - Build configuration only (returns ClipboardConfig)build() - Build and create monitor (returns ClipboardMonitor)Immutable configuration data class for clipboard monitors.
Properties:
listener: ClipboardListener - Clipboard change listenerdebounceDelayMs: Long - Debounce delay in milliseconds (default: 50ms)pollingIntervalMs: Long - Polling interval in milliseconds (default: 200ms)enableDuplicateFiltering: Boolean - Enable duplicate filtering (default: true)errorHandler: ((Throwable) -> Unit)? - Optional error handlerFactory Methods:
default(listener) - Create config with default settingshighFrequency(listener) - Create high-frequency configlowPower(listener) - Create low-power configFactory object for creating platform-specific clipboard monitors using Strategy Pattern.
Methods:
create(config: ClipboardConfig) - Create monitor with configurationcreate(listener: ClipboardListener) - Create monitor with default configuration (backward
compatible)Android-specific:
init(context: Context) - Manual initialization (optional)isInitialized() - Check initialization status| Option | Default | Range | Applicable Platforms |
|---|---|---|---|
debounceDelayMs |
50ms | 0-1000ms | Android, Windows |
pollingIntervalMs |
200ms | 50-5000ms | macOS, Linux, iOS, JS |
enableDuplicateFiltering |
true | boolean | All platforms |
errorHandler |
null | function | All platforms |
Uses Android ClipboardManager with OnPrimaryClipChangedListener.
Features:
Limitations:
Permissions: No special permissions required.
Uses native Win32 clipboard notifications via message-only window.
Features:
Implementation:
AddClipboardFormatListener Win32 APIUses AWT Toolkit clipboard with periodic polling.
Features:
Implementation:
Toolkit.getDefaultToolkit().systemClipboardScheduledExecutorService for pollingUses UIPasteboard with change count polling.
Features:
Permissions: iOS 14+ shows paste notification banner on first access. No explicit permission required.
Uses browser Clipboard API with periodic polling.
Features:
Requirements:
Limitations:
Browser Compatibility:
// All clipboard changes as Flow
fun ClipboardMonitor.asFlow(): Flow<ClipboardContent>
// Only text changes
fun ClipboardMonitor.textFlow(includeEmpty: Boolean = false): Flow<String?>
// Only file changes
fun ClipboardMonitor.filesFlow(): Flow<List<String>>
// Image availability changes
fun ClipboardMonitor.imageAvailableFlow(): Flow<Boolean>// Transform successful results
fun <T, R> ClipboardResult<T>.map(transform: (T) -> R): ClipboardResult<R>
// Chain result operations
fun <T, R> ClipboardResult<T>.flatMap(transform: (T) -> ClipboardResult<R>): ClipboardResult<R>
// Extract values
fun <T> ClipboardResult<T>.getOrNull(): T?
fun <T> ClipboardResult<T>.getOrDefault(default: T): T
fun <T> ClipboardResult<T>.getOrThrow(): T
// Callbacks for different result types
fun <T> ClipboardResult<T>.onSuccess(block: (T) -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onEmpty(block: () -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onPermissionDenied(block: (String?) -> Unit): ClipboardResult<T>
fun <T> ClipboardResult<T>.onError(block: (String, Throwable?) -> Unit): ClipboardResult<T>
// Status checks
fun <T> ClipboardResult<T>.isSuccess(): Boolean
fun <T> ClipboardResult<T>.isFailure(): Boolean
fun <T> ClipboardResult<T>.isEmpty(): BooleanAll monitor implementations are thread-safe and can be called from any thread.
Callback Threading:
UI updates are safe from callbacks on Android and iOS. Other platforms require proper thread handling.
Always call stop() when finished monitoring to release resources:
val monitor = ClipboardMonitor.Builder()
.setListener(listener)
.build()
try {
monitor.start()
// ... use monitor ...
} finally {
monitor.stop()
}Or use Kotlin coroutines with automatic cleanup:
coroutineScope {
ClipboardMonitor.Builder()
.setListener(listener)
.build()
.asFlow()
.collect { content ->
// Process content
}
// Automatically cleaned up when scope exits
}| Content Type | Android | Windows | macOS | Linux | iOS | JS/WasmJS |
|---|---|---|---|---|---|---|
| Plain Text | Yes | Yes | Yes | Yes | Yes | Yes |
| HTML | Yes | Yes | Yes | Yes | Yes | Limited |
| RTF | No | Yes | Yes | Yes | Yes | No |
| Files | Yes¹ | Yes | Yes | Yes | Yes² | Limited |
| Images | Yes³ | Yes³ | Yes³ | Yes³ | Yes³ | Limited |
¹ Android provides content URIs (content://...)
² iOS provides file paths via URLs
³ Detection only; actual image data requires platform-specific APIs
The library handles errors gracefully with configurable error handling:
val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.setErrorHandler { error ->
when (error) {
is SecurityException -> Log.e("Clipboard", "Permission denied")
is IllegalStateException -> Log.e("Clipboard", "Invalid state")
else -> Log.e("Clipboard", "Error: ${error.message}", error)
}
}
.build()Listener exceptions are caught internally to prevent monitoring interruption.
class ClipboardHistory {
private val history = mutableListOf<ClipboardContent>()
private val monitor = ClipboardMonitor.Builder()
.setListener(object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
if (content.isNotEmpty()) {
history.add(content)
if (history.size > 100) {
history.removeAt(0)
}
}
}
})
.useHighFrequencyPreset() // Immediate updates
.build()
fun start() = monitor.start()
fun stop() = monitor.stop()
fun getHistory(): List<ClipboardContent> = history.toList()
}class ClipboardSync(private val remoteSync: suspend (String) -> Unit) {
private val monitor = ClipboardMonitor.Builder()
.setListener(object : ClipboardListener {
override fun onClipboardChange(content: ClipboardContent) {
content.text?.let { text ->
coroutineScope.launch {
remoteSync(text)
}
}
}
})
.setDebounceDelay(100) // Debounce before syncing
.setErrorHandler { error ->
logger.error("Sync failed", error)
}
.build()
fun start() = monitor.start()
fun stop() = monitor.stop()
}// Android or iOS
val monitor = ClipboardMonitor.Builder()
.setListener(myListener)
.useLowPowerPreset() // Battery-conscious settings
.setErrorHandler { error ->
Timber.e(error, "Clipboard error")
}
.build()single<ClipboardMonitor> {
ClipboardMonitor.Builder()
.setListener(get())
.setDebounceDelay(100)
.build()
}@Module
@InstallIn(SingletonComponent::class)
object ClipboardModule {
@Provides
@Singleton
fun provideClipboardMonitor(
listener: ClipboardListener,
@ApplicationContext context: Context
): ClipboardMonitor {
ClipboardMonitorFactory.init(context)
return ClipboardMonitor.Builder()
.setListener(listener)
.setDebounceDelay(100)
.build()
}
}Copyright 2025 Sk Niyaj Ali
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome. Please ensure:
Sk Niyaj Ali - https://github.com/niyajali