
Deploys a robust polling engine with features like exponential backoff, cancellation, and observability, assisting in function calls until conditions or limits are satisfied. Ideal for long-polling workflows.
Last updated: 2025-09-08 17:36
A Kotlin Multiplatform library for Android and iOS that provides a production‑ready polling engine with:
Mermaid flow diagram (GitHub renders this):
flowchart TD
A[Start] --> B{Attempt fetch}
B -->|Success & meets success predicate| C[Outcome: Success]
B -->|Failure & retryable| D[Backoff delay]
B -->|Failure & not retryable| E[Outcome: Exhausted]
B -->|Timeout reached| F[Outcome: Timeout]
D --> G{More attempts left?}
G -->|Yes| B
G -->|No| E
%% External control
B -. pause/resume/cancel .-> H[Control APIs]Note: Public API rename — PollingEngineApi has been renamed to PollingApi, and apps should use the
facade object Polling instead of referencing PollingEngine directly.
PollingEngine helps you repeatedly call a function until a condition is met or limits are reached. It is designed for long‑polling workflows like waiting for a server job to complete, checking payment status, etc.
Platforms: Kotlin Multiplatform (common code) with Android and iOS targets.
Highlights:
Coordinates on Maven Central:
Gradle Kotlin DSL (Android/shared):
repositories { mavenCentral() }
dependencies { implementation("in.androidplay:pollingengine:0.1.1") }Gradle Groovy DSL:
repositories { mavenCentral() }
dependencies { implementation "in.androidplay:pollingengine:0.1.1" }Maven:
<dependency>
<groupId>in.androidplay</groupId>
<artifactId>pollingengine</artifactId>
<version>0.1.1</version>
</dependency>iOS integration options:
# Podfile (example)
platform :ios, '14.0'
use_frameworks!
target 'YourApp' do
pod 'pollingengine', :path => '../pollingengine'
endThen:
./gradlew :pollingengine:generateDummyFramework
cd iosApp && pod installOn Android, you'll typically use the polling engine within a ViewModel and expose the results to your UI using StateFlow.
ViewModel Example:
class PollingViewModel : ViewModel() {
private val _uiState = MutableStateFlow<PollingUiState>(PollingUiState.Idle)
val uiState: StateFlow<PollingUiState> = _uiState.asStateFlow()
private var pollingSession: PollingSession? = null
fun startPolling() {
viewModelScope.launch {
_uiState.value = PollingUiState.Loading
val pollingFlow = Polling.startPolling<String> {
fetch = {
// Your network call or other async operation
// e.g., api.checkJobStatus()
// Return PollingResult.Success, PollingResult.Failure, or PollingResult.Waiting
}
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
backoff = BackoffPolicy(
initialDelayMs = 1000,
maxDelayMs = 10000,
multiplier = 1.5,
maxAttempts = 10
)
shouldRetryOnError = RetryPredicates.networkOrServerOrTimeout
}
// Get the session ID
pollingSession = Polling.listActiveIds().firstOrNull()?.let { PollingSession(it) }
pollingFlow.collect { outcome ->
when (outcome) {
is PollingOutcome.Success -> _uiState.value = PollingUiState.Success(outcome.value)
is PollingOutcome.Exhausted -> _uiState.value =
PollingUiState.Error("Polling exhausted after ${outcome.attempts} attempts.")
is PollingOutcome.Timeout -> _uiState.value = PollingUiState.Error("Polling timed out.")
is PollingOutcome.Cancelled -> _uiState.value = PollingUiState.Idle
}
}
}
}
fun cancelPolling() {
pollingSession?.let {
viewModelScope.launch {
Polling.cancel(it.id)
}
}
}
override fun onCleared() {
super.onCleared()
cancelPolling()
}
}
sealed class PollingUiState {
object Idle : PollingUiState()
object Loading : PollingUiState()
data class Success(val data: String) : PollingUiState()
data class Error(val message: String) : PollingUiState()
}Lifecycle Management:
It's crucial to cancel the polling operation when the ViewModel is cleared to avoid memory leaks.
The onCleared() method is the perfect place for this.
For iOS, you can use a helper class in your shared Kotlin module to expose the polling functionality to Swift.
Shared Kotlin Helper:
// In your shared module (e.g., in a file named IosPollingHelper.kt)
object IosPollingHelper {
fun startStatusPolling(
onUpdate: (String) -> Unit,
onComplete: (PollingOutcome<String>) -> Unit
): Job {
val scope = CoroutineScope(Dispatchers.Main)
return Polling.startPolling<String> {
fetch = {
// Your fetch logic here
}
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
backoff = BackoffPolicies.quick20s
onAttempt = { attempt, _ ->
onUpdate("Polling attempt: $attempt")
}
}.onEach { outcome ->
onComplete(outcome)
}.launchIn(scope)
}
}SwiftUI ViewModel:
import SwiftUI
import pollingengine // Your KMP module name
@MainActor
class PollingViewModel: ObservableObject {
@Published var status: String = "Idle"
private var pollingJob: Kotlinx_coroutines_coreJob?
func startPolling() {
status = "Polling started..."
pollingJob = IosPollingHelper.shared.startStatusPolling(
onUpdate: { [weak self] updateMessage in
self?.status = updateMessage
},
onComplete: { [weak self] outcome in
if let success = outcome as? PollingOutcome.Success<NSString> {
self?.status = "Success: \(success.value)"
} else if let exhausted = outcome as? PollingOutcome.Exhausted {
self?.status = "Polling exhausted after \(exhausted.attempts) attempts."
} else if outcome is PollingOutcome.Timeout {
self?.status = "Polling timed out."
} else if outcome is PollingOutcome.Cancelled {
self?.status = "Polling cancelled."
}
}
)
}
func cancelPolling() {
pollingJob?.cancel(cause: nil)
status = "Idle"
}
}SwiftUI View:
struct ContentView: View {
@StateObject private var viewModel = PollingViewModel()
var body: some View {
VStack {
Text(viewModel.status)
.padding()
Button("Start Polling") {
viewModel.startPolling()
}
Button("Cancel Polling") {
viewModel.cancelPolling()
}
}
}
}Clone and build:
git clone https://github.com/bosankus/PollingEngine.git
cd PollingEngine
./gradlew buildRun tests (all targets where applicable):
./gradlew :pollingengine:allTestsAndroid app:
./gradlew :composeApp:installDebugiOS builds (macOS):
scripts/fix-ios-simulator.sh then retry.Publishing to Maven Central uses com.vanniktech.maven.publish.
./gradlew :pollingengine:publishToMavenLocal
./gradlew :pollingengine:publish --no-configuration-cacheVersioning policy: Semantic Versioning (MAJOR.MINOR.PATCH). Public API stability is guarded by Kotlin Binary Compatibility Validator.
Release notes: maintain CHANGELOG.md for each version. Tag releases on Git and reference them in release notes.
We welcome contributions!
./gradlew ktlintCheck detekt
./gradlew build
Guidelines and policies:
./gradlew apiCheck when modifying public APIs.Licensed under the Apache License, Version 2.0. See LICENSE.
Copyright (c) 2025 AndroidPlay
Last updated: 2025-09-08 17:36
A Kotlin Multiplatform library for Android and iOS that provides a production‑ready polling engine with:
Mermaid flow diagram (GitHub renders this):
flowchart TD
A[Start] --> B{Attempt fetch}
B -->|Success & meets success predicate| C[Outcome: Success]
B -->|Failure & retryable| D[Backoff delay]
B -->|Failure & not retryable| E[Outcome: Exhausted]
B -->|Timeout reached| F[Outcome: Timeout]
D --> G{More attempts left?}
G -->|Yes| B
G -->|No| E
%% External control
B -. pause/resume/cancel .-> H[Control APIs]Note: Public API rename — PollingEngineApi has been renamed to PollingApi, and apps should use the
facade object Polling instead of referencing PollingEngine directly.
PollingEngine helps you repeatedly call a function until a condition is met or limits are reached. It is designed for long‑polling workflows like waiting for a server job to complete, checking payment status, etc.
Platforms: Kotlin Multiplatform (common code) with Android and iOS targets.
Highlights:
Coordinates on Maven Central:
Gradle Kotlin DSL (Android/shared):
repositories { mavenCentral() }
dependencies { implementation("in.androidplay:pollingengine:0.1.1") }Gradle Groovy DSL:
repositories { mavenCentral() }
dependencies { implementation "in.androidplay:pollingengine:0.1.1" }Maven:
<dependency>
<groupId>in.androidplay</groupId>
<artifactId>pollingengine</artifactId>
<version>0.1.1</version>
</dependency>iOS integration options:
# Podfile (example)
platform :ios, '14.0'
use_frameworks!
target 'YourApp' do
pod 'pollingengine', :path => '../pollingengine'
endThen:
./gradlew :pollingengine:generateDummyFramework
cd iosApp && pod installOn Android, you'll typically use the polling engine within a ViewModel and expose the results to your UI using StateFlow.
ViewModel Example:
class PollingViewModel : ViewModel() {
private val _uiState = MutableStateFlow<PollingUiState>(PollingUiState.Idle)
val uiState: StateFlow<PollingUiState> = _uiState.asStateFlow()
private var pollingSession: PollingSession? = null
fun startPolling() {
viewModelScope.launch {
_uiState.value = PollingUiState.Loading
val pollingFlow = Polling.startPolling<String> {
fetch = {
// Your network call or other async operation
// e.g., api.checkJobStatus()
// Return PollingResult.Success, PollingResult.Failure, or PollingResult.Waiting
}
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
backoff = BackoffPolicy(
initialDelayMs = 1000,
maxDelayMs = 10000,
multiplier = 1.5,
maxAttempts = 10
)
shouldRetryOnError = RetryPredicates.networkOrServerOrTimeout
}
// Get the session ID
pollingSession = Polling.listActiveIds().firstOrNull()?.let { PollingSession(it) }
pollingFlow.collect { outcome ->
when (outcome) {
is PollingOutcome.Success -> _uiState.value = PollingUiState.Success(outcome.value)
is PollingOutcome.Exhausted -> _uiState.value =
PollingUiState.Error("Polling exhausted after ${outcome.attempts} attempts.")
is PollingOutcome.Timeout -> _uiState.value = PollingUiState.Error("Polling timed out.")
is PollingOutcome.Cancelled -> _uiState.value = PollingUiState.Idle
}
}
}
}
fun cancelPolling() {
pollingSession?.let {
viewModelScope.launch {
Polling.cancel(it.id)
}
}
}
override fun onCleared() {
super.onCleared()
cancelPolling()
}
}
sealed class PollingUiState {
object Idle : PollingUiState()
object Loading : PollingUiState()
data class Success(val data: String) : PollingUiState()
data class Error(val message: String) : PollingUiState()
}Lifecycle Management:
It's crucial to cancel the polling operation when the ViewModel is cleared to avoid memory leaks.
The onCleared() method is the perfect place for this.
For iOS, you can use a helper class in your shared Kotlin module to expose the polling functionality to Swift.
Shared Kotlin Helper:
// In your shared module (e.g., in a file named IosPollingHelper.kt)
object IosPollingHelper {
fun startStatusPolling(
onUpdate: (String) -> Unit,
onComplete: (PollingOutcome<String>) -> Unit
): Job {
val scope = CoroutineScope(Dispatchers.Main)
return Polling.startPolling<String> {
fetch = {
// Your fetch logic here
}
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
backoff = BackoffPolicies.quick20s
onAttempt = { attempt, _ ->
onUpdate("Polling attempt: $attempt")
}
}.onEach { outcome ->
onComplete(outcome)
}.launchIn(scope)
}
}SwiftUI ViewModel:
import SwiftUI
import pollingengine // Your KMP module name
@MainActor
class PollingViewModel: ObservableObject {
@Published var status: String = "Idle"
private var pollingJob: Kotlinx_coroutines_coreJob?
func startPolling() {
status = "Polling started..."
pollingJob = IosPollingHelper.shared.startStatusPolling(
onUpdate: { [weak self] updateMessage in
self?.status = updateMessage
},
onComplete: { [weak self] outcome in
if let success = outcome as? PollingOutcome.Success<NSString> {
self?.status = "Success: \(success.value)"
} else if let exhausted = outcome as? PollingOutcome.Exhausted {
self?.status = "Polling exhausted after \(exhausted.attempts) attempts."
} else if outcome is PollingOutcome.Timeout {
self?.status = "Polling timed out."
} else if outcome is PollingOutcome.Cancelled {
self?.status = "Polling cancelled."
}
}
)
}
func cancelPolling() {
pollingJob?.cancel(cause: nil)
status = "Idle"
}
}SwiftUI View:
struct ContentView: View {
@StateObject private var viewModel = PollingViewModel()
var body: some View {
VStack {
Text(viewModel.status)
.padding()
Button("Start Polling") {
viewModel.startPolling()
}
Button("Cancel Polling") {
viewModel.cancelPolling()
}
}
}
}Clone and build:
git clone https://github.com/bosankus/PollingEngine.git
cd PollingEngine
./gradlew buildRun tests (all targets where applicable):
./gradlew :pollingengine:allTestsAndroid app:
./gradlew :composeApp:installDebugiOS builds (macOS):
scripts/fix-ios-simulator.sh then retry.Publishing to Maven Central uses com.vanniktech.maven.publish.
./gradlew :pollingengine:publishToMavenLocal
./gradlew :pollingengine:publish --no-configuration-cacheVersioning policy: Semantic Versioning (MAJOR.MINOR.PATCH). Public API stability is guarded by Kotlin Binary Compatibility Validator.
Release notes: maintain CHANGELOG.md for each version. Tag releases on Git and reference them in release notes.
We welcome contributions!
./gradlew ktlintCheck detekt
./gradlew build
Guidelines and policies:
./gradlew apiCheck when modifying public APIs.Licensed under the Apache License, Version 2.0. See LICENSE.
Copyright (c) 2025 AndroidPlay