Skip to content

Commit 70a108e

Browse files
docs: Enhance README with Android and iOS implementation examples
This commit significantly updates the README.md to provide more detailed and practical usage examples for integrating the PollingEngine into Android and iOS applications. **Key Changes to README.md:** * **Android Implementation Section:** * Added a new "Android Implementation" section. * Provides a complete `ViewModel` example demonstrating how to: * Use `Polling.startPolling` within a `viewModelScope`. * Define `fetch`, `isTerminalSuccess`, and `backoff` policies. * Collect the resulting `Flow<PollingOutcome>` and update a `MutableStateFlow` for UI observation. * Retrieve the polling session ID using `Polling.listActiveIds()`. * Implement `cancelPolling()` using `Polling.cancel(id)`. * Properly cancel polling in `onCleared()` to prevent leaks. * Includes a `PollingUiState` sealed class example for managing UI states (Idle, Loading, Success, Error). * **iOS (Swift) Implementation Section:** * Added a new "iOS (Swift) Implementation" section. * Demonstrates creating a helper object (`IosPollingHelper`) in the shared Kotlin module to bridge to Swift. * The helper exposes a `startStatusPolling` function that takes Swift closures for updates (`onUpdate`) and completion (`onComplete`). * It uses `CoroutineScope(Dispatchers.Main)` and `launchIn` for flow collection. * Provides a SwiftUI `PollingViewModel` example: * Uses `@Published` to expose status to the UI. * Calls the shared Kotlin helper's `startStatusPolling` function. * Handles different `PollingOutcome` types in the `onComplete` closure to update the UI. * Shows how to cancel the `Kotlinx_coroutines_coreJob`. * Includes a basic SwiftUI `ContentView` example with buttons to start and cancel polling. * **Removed Sections:** * Removed the generic "Usage" section with basic shared code, as the new platform-specific examples are more comprehensive. * Removed the "Control APIs and Runtime Updates" section as its content is implicitly covered or better suited for more advanced documentation. * Removed the "RetryPredicates examples" section, as common predicates are often shown within the configuration examples. * Removed the "More documentation" section that linked to internal docs, simplifying the main README. * **Minor Adjustments:** * Platform-specific notes regarding `expect/actual` and coroutines were removed as they are general KMP concepts and the examples implicitly demonstrate their usage. * API reference generation instructions were removed. **Overall Improvement:** The README now offers clearer, step-by-step guidance for developers looking to integrate the PollingEngine into their Android (Jetpack Compose with ViewModel) and iOS (SwiftUI with ObservableObject) projects, focusing on common patterns and best practices for each platform.
1 parent c3cdaa6 commit 70a108e

1 file changed

Lines changed: 140 additions & 103 deletions

File tree

README.md

Lines changed: 140 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -111,76 +111,169 @@ cd iosApp && pod install
111111
- Swift Package Manager: If you publish an XCFramework, add the package URL and version in Xcode. (
112112
SPM publication is not configured in this repo out‑of‑the‑box.)
113113

114-
## Usage
114+
## Android Implementation
115115

116-
Basic shared usage:
116+
On Android, you'll typically use the polling engine within a ViewModel and expose the results to
117+
your UI using StateFlow.
117118

119+
**ViewModel Example:**
118120
```kotlin
121+
class PollingViewModel : ViewModel() {
122+
123+
private val _uiState = MutableStateFlow<PollingUiState>(PollingUiState.Idle)
124+
val uiState: StateFlow<PollingUiState> = _uiState.asStateFlow()
125+
126+
private var pollingSession: PollingSession? = null
127+
128+
fun startPolling() {
129+
viewModelScope.launch {
130+
_uiState.value = PollingUiState.Loading
131+
132+
val pollingFlow = Polling.startPolling<String> {
133+
fetch = {
134+
// Your network call or other async operation
135+
// e.g., api.checkJobStatus()
136+
// Return PollingResult.Success, PollingResult.Failure, or PollingResult.Waiting
137+
}
138+
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
139+
backoff = BackoffPolicy(
140+
initialDelayMs = 1000,
141+
maxDelayMs = 10000,
142+
multiplier = 1.5,
143+
maxAttempts = 10
144+
)
145+
shouldRetryOnError = RetryPredicates.networkOrServerOrTimeout
146+
}
147+
148+
// Get the session ID
149+
pollingSession = Polling.listActiveIds().firstOrNull()?.let { PollingSession(it) }
150+
151+
pollingFlow.collect { outcome ->
152+
when (outcome) {
153+
is PollingOutcome.Success -> _uiState.value = PollingUiState.Success(outcome.value)
154+
is PollingOutcome.Exhausted -> _uiState.value =
155+
PollingUiState.Error("Polling exhausted after ${outcome.attempts} attempts.")
156+
is PollingOutcome.Timeout -> _uiState.value = PollingUiState.Error("Polling timed out.")
157+
is PollingOutcome.Cancelled -> _uiState.value = PollingUiState.Idle
158+
}
159+
}
160+
}
161+
}
162+
163+
fun cancelPolling() {
164+
pollingSession?.let {
165+
viewModelScope.launch {
166+
Polling.cancel(it.id)
167+
}
168+
}
169+
}
119170

120-
val config = pollingConfig<String> {
121-
fetch { /* return PollingResult<String> */ TODO() }
122-
success { it == "READY" }
123-
// Retry for common transient errors (network/server/timeout/unknown)
124-
retry(RetryPredicates.networkOrServerOrTimeout)
125-
backoff(BackoffPolicies.quick20s)
171+
override fun onCleared() {
172+
super.onCleared()
173+
cancelPolling()
174+
}
126175
}
127176

128-
suspend fun run(): PollingOutcome<String> = Polling.run(config)
177+
sealed class PollingUiState {
178+
object Idle : PollingUiState()
179+
object Loading : PollingUiState()
180+
data class Success(val data: String) : PollingUiState()
181+
data class Error(val message: String) : PollingUiState()
182+
}
129183
```
130184

131-
Android example (ViewModel + Compose):
185+
**Lifecycle Management:**
186+
It's crucial to cancel the polling operation when the ViewModel is cleared to avoid memory leaks.
187+
The `onCleared()` method is the perfect place for this.
132188

133-
```kotlin
134-
class StatusViewModel : ViewModel() {
135-
private val _status = MutableStateFlow("Idle")
136-
val status: StateFlow<String> = _status
137-
138-
private val config = pollingConfig<String> {
139-
fetch { TODO("Return PollingResult<String>") }
140-
success { it == "READY" }
141-
backoff(BackoffPolicies.quick20s)
142-
}
189+
## iOS (Swift) Implementation
190+
191+
For iOS, you can use a helper class in your shared Kotlin module to expose the polling functionality
192+
to Swift.
143193

144-
fun runOnce() = viewModelScope.launch {
145-
_status.value = Polling.run(config).toString()
194+
**Shared Kotlin Helper:**
195+
```kotlin
196+
// In your shared module (e.g., in a file named IosPollingHelper.kt)
197+
object IosPollingHelper {
198+
fun startStatusPolling(
199+
onUpdate: (String) -> Unit,
200+
onComplete: (PollingOutcome<String>) -> Unit
201+
): Job {
202+
val scope = CoroutineScope(Dispatchers.Main)
203+
return Polling.startPolling<String> {
204+
fetch = {
205+
// Your fetch logic here
206+
}
207+
isTerminalSuccess = { it.equals("COMPLETED", ignoreCase = true) }
208+
backoff = BackoffPolicies.quick20s
209+
onAttempt = { attempt, _ ->
210+
onUpdate("Polling attempt: $attempt")
211+
}
212+
}.onEach { outcome ->
213+
onComplete(outcome)
214+
}.launchIn(scope)
146215
}
147216
}
148217
```
149218

150-
iOS example (Swift calling Kotlin helper):
219+
**SwiftUI ViewModel:**
151220

152-
```kotlin
153-
// shared Kotlin
154-
object IosAdapters {
155-
fun provideStatusConfig(): PollingConfig<String> = pollingConfig {
156-
fetch { TODO() }
157-
success { it == "READY" }
158-
backoff(BackoffPolicies.quick20s)
221+
```swift
222+
import SwiftUI
223+
import pollingengine // Your KMP module name
224+
225+
@MainActor
226+
class PollingViewModel: ObservableObject {
227+
@Published var status: String = "Idle"
228+
private var pollingJob: Kotlinx_coroutines_coreJob?
229+
230+
func startPolling() {
231+
status = "Polling started..."
232+
pollingJob = IosPollingHelper.shared.startStatusPolling(
233+
onUpdate: { [weak self] updateMessage in
234+
self?.status = updateMessage
235+
},
236+
onComplete: { [weak self] outcome in
237+
if let success = outcome as? PollingOutcome.Success<NSString> {
238+
self?.status = "Success: \(success.value)"
239+
} else if let exhausted = outcome as? PollingOutcome.Exhausted {
240+
self?.status = "Polling exhausted after \(exhausted.attempts) attempts."
241+
} else if outcome is PollingOutcome.Timeout {
242+
self?.status = "Polling timed out."
243+
} else if outcome is PollingOutcome.Cancelled {
244+
self?.status = "Polling cancelled."
245+
}
246+
}
247+
)
248+
}
249+
250+
func cancelPolling() {
251+
pollingJob?.cancel(cause: nil)
252+
status = "Idle"
159253
}
160254
}
161255
```
162256

257+
**SwiftUI View:**
163258
```swift
164-
// Swift
165-
166-
import PollingEngine
167-
168-
let handle = InAndroidplayPollingengineAdaptersIosAdapters().startStatusPolling { outcome in
169-
print("Outcome: \(outcome)")
259+
struct ContentView: View {
260+
@StateObject private var viewModel = PollingViewModel()
261+
262+
var body: some View {
263+
VStack {
264+
Text(viewModel.status)
265+
.padding()
266+
Button("Start Polling") {
267+
viewModel.startPolling()
268+
}
269+
Button("Cancel Polling") {
270+
viewModel.cancelPolling()
271+
}
272+
}
273+
}
170274
}
171275
```
172276

173-
API Reference:
174-
175-
- Generate locally with Dokka: `./gradlew :pollingengine:dokkaHtml`
176-
- Output is in `pollingengine/build/dokka/html/index.html`
177-
178-
Platform‑specific notes:
179-
180-
- expect/actual: Core engine lives in commonMain. If you introduce platform APIs, add expect
181-
declarations in common and provide actual implementations in androidMain/iosMain.
182-
- Coroutines: library uses kotlinx.coroutines; ensure proper dispatchers on each platform.
183-
184277
## Setup/Build Instructions
185278

186279
Clone and build:
@@ -258,59 +351,3 @@ Copyright (c) 2025 AndroidPlay
258351
- Maintainer: @bosankus
259352
- Issues: use [GitHub Issues](https://github.com/bosankus/PollingEngine/issues)
260353
- Security: see [SECURITY.md](SECURITY.md)
261-
262-
## Control APIs and Runtime Updates
263-
264-
Start polling by collecting the returned Flow, and control active sessions by ID:
265-
266-
```kotlin
267-
// Start and collect in your scope
268-
val flow = Polling.startPolling(config)
269-
val job = flow.onEach { outcome ->
270-
println("Outcome: $outcome")
271-
}.launchIn(scope)
272-
273-
// Introspection
274-
val ids = Polling.listActiveIds() // suspend; returns List<String>
275-
println("Active: $ids (count=${Polling.activePollsCount()})")
276-
277-
// Pause/resume first active session (example)
278-
if (ids.isNotEmpty()) {
279-
val id = ids.first()
280-
Polling.pause(id)
281-
// ... later
282-
Polling.resume(id)
283-
284-
// Update backoff at runtime
285-
Polling.updateBackoff(id, BackoffPolicies.quick20s)
286-
287-
// Cancel
288-
Polling.cancel(id)
289-
}
290-
291-
// Or cancel all
292-
Polling.cancelAll()
293-
294-
// Stop collecting if needed
295-
job.cancel()
296-
```
297-
298-
## RetryPredicates examples
299-
300-
Built-ins to reduce boilerplate:
301-
302-
```kotlin
303-
// Retry for network/server/timeout/unknown errors (recommended)
304-
retry(RetryPredicates.networkOrServerOrTimeout)
305-
306-
// Always retry on failures
307-
retry(RetryPredicates.always)
308-
309-
// Never retry on failures
310-
retry(RetryPredicates.never)
311-
```
312-
313-
## More documentation
314-
315-
- docs/pollingengine.md — Web Guide (overview, install, Android/iOS usage)
316-
- docs/DeveloperGuide.md — Developer Guide (API overview, DSL, migration, reference)

0 commit comments

Comments
 (0)