Part 1 claimed that reducers are the most portable code you can write. Here is what that actually cashes out to: every line of platform-specific code the Redux layer of a ReduxKotlin app needs. Not a summary. The actual, complete set. It’s from the TaskFlow sample, not the library: you write this expect/actual yourself, once, and the library supplies the NotificationContext type and the coalescingNotificationContext helper you build it with.
// commonMainpublic expect fun mainNotificationContext(): NotificationContext// androidMainpublic actual fun mainNotificationContext(): NotificationContext { val handler = Handler(Looper.getMainLooper()) return coalescingNotificationContext( isOnTargetThread = { Looper.myLooper() == Looper.getMainLooper() }, post = { block -> handler.post(block) }, )}// iosMainpublic actual fun mainNotificationContext(): NotificationContext = coalescingNotificationContext( isOnTargetThread = { NSThread.isMainThread() }, post = { block -> dispatch_async(dispatch_get_main_queue()) { block() } }, )// jvmMain (desktop)public actual fun mainNotificationContext(): NotificationContext = coalescingNotificationContext( isOnTargetThread = { SwingUtilities.isEventDispatchThread() }, post = { block -> SwingUtilities.invokeLater(block) }, )That’s it. One function, deciding which thread your subscriber callbacks land on. Your state, your actions, your reducers, your selectors, your subscriptions, and your Compose bindings are all in commonMain with no expect/actual at all.
I want to be careful here, because this is the kind of claim libraries oversell. A real app has other shims. TaskFlow declares eight expects in total, for things like a SQLDelight driver factory, a UUID generator, and a back handler. Those are real and you will write them. But only one of the eight belongs to Redux. Within the state layer, the platform surface is a single function, and that is unusual enough to be worth showing.
The target matrix
There are three tiers, and they exist for reasons I can defend.
Core (redux-kotlin, redux-kotlin-devtools-core) publishes ten:
android · jvm · js (browser + node) · wasmJs (browser + node)iosArm64 · iosSimulatorArm64 · macosArm64linuxX64 · linuxArm64 · mingwX64Standard is everything else: both bundles, concurrent, granular, multimodel, routing, and the Compose bindings. Same list minus linuxArm64, so nine targets. That’s the number in the title, and it’s the one that matters, because it’s what you actually get when you take the bundle.
Compose-UI is redux-kotlin-devtools-ui and redux-kotlin-devtools-inapp: standard minus linuxX64 and mingwX64. The build file says why, and I’ll just quote it:
compose.foundation/material3publish no linux/mingw klibs in Compose Multiplatform 1.11, so modules consuming them can’t target the desktop-native platforms.
That’s a constraint I inherited, not a decision I made. When Compose ships those klibs, the tier disappears.
JVM-only: redux-kotlin-snapshot (a Skiko renderer, see Part 8) and the rk CLI.
What got dropped
iosX64, macosX64, watchOS, tvOS, and androidNative are gone as of 1.0.
The x64 Apple targets went with Compose Multiplatform 1.11 and the end of Intel Macs. The simulator target that matters now is iosSimulatorArm64. I hit the consequence of this myself, in an app of mine that adopted an early alpha:
iosX64dropped:redux-kotlin-granularhas noiosX64publication, and Intel Macs are EOL.
That comment is in a real build.gradle.kts I wrote.
watchOS and tvOS were never carrying their weight. If someone shows up with a real use case, the tiers are cheap to extend: the core is target-agnostic.
Interop
A detail that says a lot about how much of this was actually exercised on non-Kotlin platforms.
The granular subscription API has a property-reference form that is lovely in Kotlin:
store.subscribeTo(AppState::user) { _, new -> render(new) }It’s marked @HiddenFromObjC, because a KProperty1 isn’t constructible from Swift. So there’s a second pathway that takes a KClass explicitly:
store.subscribeToModel(CartModel::class, { it.itemCount }) { _, n -> badge.count = n }Same for the Compose bindings: fieldState(Model::field) for Kotlin, fieldStateOf(Model::class) { ... } for everyone else. Nobody writes that second overload unless they have actually tried to call the library from Swift and watched it not be there.
What an agent reads instead of your source
One more cross-platform artifact, and it’s aimed at machines.
Every module commits an .api dump, a machine-readable declaration of its entire public surface, per target, regenerated by ./gradlew apiDump and enforced in CI. It exists to catch accidental API breakage, which is its day job.
Its second job is that it’s the cheapest possible way for a coding agent to learn what a module can do. Instead of grepping through source files and inferring the API from usage, it reads one flat file that is the API. And because CI fails when the dump drifts from the code, that file can’t lie. Part 9 goes into this.
Toolchain
Kotlin 2.3.20, Compose Multiplatform 1.11.1, library bytecode pinned at JVM 17, Android minSdk 21.
The TaskFlow sample builds Android, iOS, desktop, and a wasm browser target off one commonMain. Here it is running as a desktop app, which is the same Kanban board, the same reducers, and the same store you’d get on a phone:

Nothing in that screen is desktop-specific except the window around it. That’s the whole pitch, and you can go read it: examples/taskflow.
Happy coding, or agent chatting, on whichever of the nine you ship to.
Sources of truth: reduxkotlin.org · github.com/reduxkotlin/redux-kotlin · the TaskFlow sample
Part 2 of ReduxKotlin 1.0.