diff --git a/README.md b/README.md index 75ff4d6..0005ea6 100644 --- a/README.md +++ b/README.md @@ -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` | `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 -> }` 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` | `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 = 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> = scheduler.jobStates(fetch) + .filter { it.runState == RunState.FinishedSuccess } + .map { it.result as List } + fun fetch(sort: Sort) = scheduler.start(fetch, sort) +} +```