Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,88 @@ runBlocking {
scheduler.cancelAndJoin(jobHandle2)
}
```

# Migrating from remo

[remo](https://github.com/appKODE/remo) (`ru.kode:remo`, `ru.kode.android:remo`, `ReactiveModel`) is deprecated,
reactive-tasks replaces it. Both run suspend "jobs" and let you observe their state, results and errors;
reactive-tasks drops the `ReactiveModel` base class and `kotlin-result`, and adds `startLatest`, `startSuspended`,
`restart` and iOS.

```kotlin
// before
implementation("ru.kode:remo:1.2.0") // or ru.kode:remo:9.0.1, ru.kode.android:remo:9.1.0
// after
implementation("ru.kode:reactive-tasks:1.0.0")
```

## API map

| remo | reactive-tasks |
|---|---|
| `class UserModel : ReactiveModel()` | `class UserModel(scheduler: Scheduler)`, no base class |
| `model.start(parentScope)` (1.x) / constructor scope (9.x), `Job.cancel()` to stop | `Scheduler(scope)`, cancel `scope` to stop |
| `scope.launch { }` inside the model | `scheduler.scope.launch { }` |
| `uncaughtExceptions: Flow<Throwable>` | `Scheduler(onUncaughtException = { taskId, name, e -> })` |
| `val fetch = task { id: String -> api.fetch(id) }` | `val fetch = scheduler.registerTask("fetch") { id: String -> api.fetch(id) }` |
| `task { -> api.fetch() }` | `registerTask("fetch") { _: Unit -> api.fetch() }`, `start(fetch, Unit)` |
| `task { a: A, b: B -> }` (2–3 params) | one argument: `registerTask("t") { (a, b): Pair<A, B> -> }` or a data class |
| `fetch.start(id)`: `Job` | `scheduler.start(fetch, id)`: `JobHandle` |
| `job.cancel()` | `scheduler.cancel(jobHandle)` / `scheduler.cancel(fetch)` (all jobs of the task) |
| `QueueingStrategy.Disallow` (default: fails if a job runs) | no equivalent, `start` runs jobs in parallel |
| `QueueingStrategy.SkipNew` | check `isRunning(fetch)` (below) before `start` |
| — | `startLatest(fetch, id)`: cancels running jobs of the task (search-as-you-type) |
| — | `startSuspended(fetch, id)`: suspends until done, returns `JobState` |
| `WatchContext()` + `executeInModelScope { }` | `registerTask` + `start` |
| one `WatchContext` shared by several jobs (one loader) | several tasks, `merge(jobStates(a), jobStates(b))`, or one task with a sealed argument |
| `fetch.jobFlow.state`: `Idle` / `Running` | `jobStates(fetch).map { it.runState == RunState.Running }` |
| `fetch.jobFlow.successResults()` | `jobStates(fetch).filter { it.runState == RunState.FinishedSuccess }.map { it.result as R }` |
| `fetch.jobFlow.errors()` | `jobStates(fetch).filter { it.runState == RunState.FinishedError }.map { it.error!! }` |
| `fetch.jobFlow.results()`: `Result<R, Throwable>` | `JobState` with `runState`, `result`, `error` |
| `results(replayLast = true)` | no replay: `shareIn(scope, started, replay = 1)` or `stateIn`; last states are in `scheduler.taskJobState` |
| `StartScheduled.Lazily(...)` | subscribe first: `shareIn(...).onSubscription { scheduler.start(fetch, id) }` |
| `mapErrors {}` / `WatchContext(errorMapper = …)` | map in the flow, or catch inside the task body |

## Observing one task

`Scheduler.observeStateChanges(handle)` (1.0.0) emits states of **all** tasks of the scheduler and closes on the
first cancelled job of any task — `startLatest` cancels jobs, so the flow would end. Until it is fixed, observe with
this helper, which behaves like remo's `jobFlow`:

```kotlin
@OptIn(ExperimentalUuidApi::class)
fun Scheduler.jobStates(task: TaskHandle<*, *>): Flow<JobState> = callbackFlow {
val listener = object : TaskStateChangeListener {
override suspend fun onJobStateChanged(state: JobState) {
if (state.taskId == task.id) send(state)
}

override fun onJobCancelled(taskId: Uuid, jobId: Uuid) = Unit
}
addTaskStateChangeListener(listener)
awaitClose { removeTaskStateChangeListener(listener) }
}

fun Scheduler.isRunning(task: TaskHandle<*, *>): Boolean =
taskJobState.values.any { it.taskId == task.id && it.runState == RunState.Running }
```

## Example

```kotlin
// remo
class UserListModel : ReactiveModel() {
val fetch = task { sort: Sort -> api.fetch(sort) }
}
model.fetch.jobFlow.successResults().collect { render(it) }
model.fetch.start(Sort.Ascending)

// reactive-tasks
class UserListModel(private val scheduler: Scheduler) {
val fetch = scheduler.registerTask("fetch") { sort: Sort -> api.fetch(sort) }
val users: Flow<List<User>> = scheduler.jobStates(fetch)
.filter { it.runState == RunState.FinishedSuccess }
.map { it.result as List<User> }
fun fetch(sort: Sort) = scheduler.start(fetch, sort)
}
```
Loading