# Reaktive: Reactive Extensions that compile to Kotlin Multiplatform targets

> Reaktive is a Kotlin Multiplatform implementation of Reactive Extensions, publishing Observable, Maybe, Single and Completable with subjects, schedulers and interop modules for coroutines and RxJava. It suits teams with a shared reactive layer across JVM, Android, iOS, JS and native, and since 2.x it requires the new Kotlin/Native memory model.

**badoo/Reaktive** — Kotlin multi-platform implementation of Reactive Extensions

- Repository: https://github.com/badoo/Reaktive
- Stars: 1,214 · Forks: 63
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/badoo-reaktive

## One reactive vocabulary across seven target families

Reaktive is a Kotlin Multiplatform implementation of Reactive Extensions, published to Maven Central under the `com.badoo.reaktive` group. The core claim is that the same reactive code compiles for JVM, Android, iOS, macOS, watchOS, tvOS, JavaScript and Linux X64, and the source layout backs that up: the repository carries separate sample applications for Android, iOS, a JS browser, a Linux x64 app and a multiplatform module, alongside the library directories themselves. The licensed surfaces are the four familiar ones, `Observable`, `Maybe`, `Single` and `Completable`, plus the four subject types, `PublishSubject`, `BehaviorSubject`, `ReplaySubject` and `UnicastSubject`. For a team whose shared domain logic has to live in a KMP module and feed an Android app, an iOS app and a web build, that is the point of the library.

## Seven modules, each published separately to Maven Central

The setup is modular rather than a single dependency, and the choice is the first decision an adopter makes. The modules are `reaktive`, the main multiplatform library, `reaktive-annotations`, a collection of annotations, `reaktive-testing`, multiplatform testing utilities, `utils` with utilities such as `Clock`, `AtomicReference` and `Lock`, `coroutines-interop` for Kotlin coroutines interoperability, and `rxjava2-interop` and `rxjava3-interop` for JVM and Android interop with those two RxJava versions. The Gradle configuration puts the interop modules in `commonMain`, so a common source set can depend on them even though their own implementation is JVM and Android specific:

```groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation 'com.badoo.reaktive:reaktive:<version>'
                implementation 'com.badoo.reaktive:reaktive-annotations:<version>'
                implementation 'com.badoo.reaktive:coroutines-interop:<version>'
                implementation 'com.badoo.reaktive:rxjava2-interop:<version>'
                implementation 'com.badoo.reaktive:rxjava3-interop:<version>'
            }
        }
    }
}
```

The test utilities go in `commonTest` under `com.badoo.reaktive:reaktive-testing`. The `rxjava2-interop` and `rxjava3-interop` modules are the awkward part of this layout, because they are declared for JVM and Android while being referenced from a common source set, and a team that only wants one of the two RxJava versions still has to think about the other.

## Six schedulers with clearly different threading contracts

The scheduler set is where the library's concurrency behaviour is decided, and each one is documented with its own rule. `computationScheduler` is a fixed thread pool equal to a number of cores, `ioScheduler` is an unbound thread pool with a caching policy, `newThreadScheduler` creates a new thread for each unit of work, `singleScheduler` executes tasks on a single shared background thread, `trampolineScheduler` queues tasks and executes them on one of the participating threads, and `mainScheduler` executes tasks on the main thread. On Kotlin/Native the library supports true multithreading, and the scheduling semantics are the same across targets rather than being a JVM-only story. The `trampolineScheduler` is the one worth understanding precisely, because it is the scheduler that makes synchronous-looking chains possible without blocking a thread, and it is also the one whose behaviour surprises people who assume every scheduler hops threads.

## Coroutines interop covers flows, suspend functions and dispatchers

The `coroutines-interop` module converts between the two worlds in both directions. `Flow` converts to and from `Observable`, `suspend` functions convert to and from `Single`, `Maybe` and `Completable`, a `CoroutineContext` converts to a `Scheduler`, and a `Scheduler` converts to a `CoroutineDispatcher`. The examples are short enough to read in full:

```kotlin
val flow: Flow<Int> = observableOf(1, 2, 3).asFlow()
val observable: Observable<Int> = flowOf(1, 2, 3).asObservable()
```

```kotlin
val defaultScheduler = Dispatchers.Default.asScheduler()
```

The documentation also points at known problems with multi-threaded coroutines on Kotlin/Native in the kotlinx.coroutines documentation, which is the kind of caveat that decides whether a team attempts a bridge between the two models on that target. The interop surface is deliberately narrow, which is the right instinct: every conversion is a place where a scheduler boundary can be crossed without the author noticing.

## RxJava interop is a conversion layer, not a dependency

The `rxjava2-interop` and `rxjava3-interop` modules exist so that an existing RxJava codebase can adopt Reaktive gradually rather than in one rewrite. The documented capability is conversion of sources and schedulers between Reaktive and RxJava, and because the modules cover JVM and Android only, they exist mainly to ease a migration inside a mobile backend that already speaks RxJava 2 or 3. The reason this matters is that the two libraries do not have identical operator sets, and an interop module lets a team move one call site at a time. The cost is that you now have two reactive vocabularies in the same codebase, and the boundary between them has to be deliberate rather than accidental, since a conversion that crosses a thread is easy to miss in review.

## Kotlin/Native since 2.x means the new memory model only

This is the sharpest constraint in the whole library and it is easy to miss. Since version 2.x, Reaktive only works with the new memory model, and true multithreading on Kotlin/Native depends on it. The older strict memory model is gone from the supported path. The documentation keeps the 1.x behaviour in a collapsed section, and that material is worth reading even if you are on 2.x, because it explains the shape of the problem that the new model removes. Under the old model, shared mutable state between threads was not allowed, and because Reaktive supports multithreading there, the library preferred frozen state, with callbacks and captured objects submitted to a scheduler being frozen. `subscribeOn` froze both its upstream source and its downstream observer along with all the Disposables, while the values themselves were not frozen by the operator, and `observeOn` froze only its downstream observer and the values passed through it, leaving the upstream source unfrozen. Operators that use a scheduler, such as `debounce`, `timer` and `delay`, behaved like `observeOn` in most cases.

## threadLocal is the escape hatch when frozen state is wrong

Freezing is occasionally the wrong outcome, and the usual example given is loading data in the background and then updating the UI, since UI state cannot be frozen. The documentation names two ways to get that behaviour with Reaktive. The first is the `threadLocal` operator, which is placed after `observeOn`:

```kotlin
observable<Any> { emitter ->
    // Background job
}
    .subscribeOn(ioScheduler)
    .observeOn(mainScheduler)
    .threadLocal()
    .doOnBeforeNext { values += it }
    .subscribe()
```

The second is passing `isThreadLocal = true` to the `subscribe` operator, with the callbacks then running unfrozen. Both routes carry the same constraint, stated in bold in the documentation: the subscription call must be performed on the Main thread. This is a sharp edge. It is the kind of rule that is easy to violate through a refactor that moves a subscription into a background component, and the failure appears at runtime rather than at compile time.

## The upgrade path runs through the interop modules

For a team already on RxJava 2 or 3, Reaktive offers a staged route: keep the RxJava codebase, add the matching interop module, and convert sources and schedulers at the edges while new shared code is written against Reaktive. The alternative is the direct approach, taking the RxJava dependency out of the multiplatform module and rewriting the operators in Reaktive terms, which is a larger change but ends with a single vocabulary. The trade-off is dependency surface. Reaktive itself is multiplatform and lightweight, but each interop module you keep is a bridge between two models, and a bridge you keep is a bridge you have to maintain. On the version history, the last push was on 2026-09-30 and the published releases are 2.4.0 on 2025-06-01, 2.3.0 on 2024-08-31 and 2.2.0 on 2024-06-04, so the 2.x line has been stable across the new memory model transition. The licence is Apache 2.0, and the repository also carries a `benchmarks/` directory, a `detekt.yml` configuration and a `docs/` folder alongside the sample applications.

## Conclusion

Adopt Reaktive when one reactive layer has to run unchanged on JVM, Android, iOS, JavaScript and native targets, and you want the RxJava operator vocabulary without dragging RxJava itself into a multiplatform build. Skip it if a single-target Android app is all you have, because the interop modules and the multiplatform source set layout add ceremony a plain RxJava dependency does not. Before adopting, confirm every target you ship actually needs the library, read the new memory model requirement for Kotlin/Native since version 2.x dropped the old strict model, and pick the module set deliberately, since `reaktive`, `coroutines-interop` and the two `rxjava-interop` modules are separate Maven artefacts and each one you add is a dependency you carry.

## FAQ

### Which Kotlin/Native memory model does Reaktive require?

Since version 2.x, Reaktive only works with the new memory model, and true multithreading on Kotlin/Native depends on it. The old strict memory model from the 1.x line is not supported.

### What modules does Reaktive publish to Maven Central?

The modules are reaktive, reaktive-annotations, reaktive-testing, utils, coroutines-interop, rxjava2-interop and rxjava3-interop, all published under the com.badoo.reaktive group.

### How do I convert a Kotlin Flow to a Reaktive Observable?

Use the asFlow and asObservable conversions from the coroutines-interop module, for example observableOf(1, 2, 3).asFlow() and flowOf(1, 2, 3).asObservable(). The module also converts suspend functions to and from Single, Maybe and Completable, and schedulers to and from coroutine dispatchers.

### What is the threadLocal operator in Reaktive for?

It lets callbacks and captured objects avoid being frozen, which matters when the callback updates mutable state such as a UI list. The alternative is passing isThreadLocal = true to the subscribe operator. In both cases the subscription must be performed on the Main thread.

### Which platforms does Reaktive support?

The documented targets are JVM, Android, iOS, macOS, watchOS, tvOS, JavaScript and Linux X64.

## Sources

- [badoo/Reaktive on GitHub](https://github.com/badoo/Reaktive)
- [Issues](https://github.com/badoo/Reaktive/issues)
- [License: Apache-2.0](https://github.com/badoo/Reaktive/blob/master/LICENSE)
- [README](https://github.com/badoo/Reaktive/blob/master/README.md)
- [Releases](https://github.com/badoo/Reaktive/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/badoo-reaktive
