
Decomposes Compose presenters, eliminating callback and event plumbing by generating compile-time routing between parent and child events/effects, enabling clean, callback-free fused sub-presenters.
⚠️ Experimental — research project building on DroidKaigi 2024 / 2025-style Compose architectures.
Fusio — Latin, "to pour together, to fuse" — is a Kotlin compiler plugin that decomposes Compose presenters without the usual I/O boilerplate at the seams.
A @Composable presenter that grows past a few features wants to be
split into smaller pieces. But splitting forces I/O boilerplate at
every seam: callback lambdas the parent threads down to each child,
event invocations the children fire back up to the parent. That
boilerplate scales with every new feature you add.
Fusio resolves those seams at compile time — fusion in the literal sense. Two sealed-type annotations declare which parent event routes into which child and which child effect lifts back up as which parent effect; the compiler synthesises the callback-and-invocation plumbing between them. The decomposed presenters end up reading as plain functions — no callback parameters threaded through, no manual event forwarding.
The library's core data type, Presentation<State, Event, Effect>,
is a state value, the effect stream the presenter produced alongside
it, and a send: (Event) -> Unit entry point the UI uses to push
input back in.
Apply the plugin:
plugins {
id("com.kitakkun.fusio") version "0.1.0"
}Declare event/effect routing on the parent's sealed types and fuse
sub-presenters inside buildPresenter:
sealed interface MyEvent {
@MapTo(ChildEvent.Add::class) data class AddItem(val title: String) : MyEvent
}
sealed interface MyEffect {
@MapFrom(ChildEffect.Added::class) data class ShowAdded(val title: String) : MyEffect
}
@Composable
fun PresenterScope<ChildEvent, ChildEffect>.childPresenter(): ChildState { /* … */ }
@Composable
fun myScreenPresenter(): Presentation<MyState, MyEvent, MyEffect> = buildPresenter {
val list = fuse { childPresenter() }
MyState(list.items)
}UI calls presentation.send(MyEvent.AddItem("milk")); the event is
routed into the child's on<ChildEvent.Add> handler, the child emits
a ChildEffect.Added, and the parent surfaces it as MyEffect.ShowAdded
on presentation.effectFlow. All routing plumbing is generated by the
compiler plugin — no reflection.
The runnable demo lives in demo/:
cd demo
../gradlew runJvm
fusio-test headless harness,
scenario API, sub-presenter testing, integration with Kotest /
Turbine.fusio-runtime internals.docs/ — additional design notes (event-handler
exhaustiveness, multi-compiler-version layer, etc.).| Your Kotlin | Supported |
|---|---|
| 2.3.0 – 2.3.x | ✅ |
| 2.4.0-Beta2+ | ✅ |
A single fusio-compiler-plugin jar ships with a per-Kotlin-version
compatibility layer inside it (a shaded fusio-compiler-compat +
kXXX submodule pattern inspired by
ZacSweers/Metro). The plugin
inspects the running Kotlin compiler at startup and ServiceLoader-
resolves the matching impl, so picking up a new patch needs no Fusio
release.
fusio-annotations, fusio-runtime, and fusio-test publish to:
| Target | Status |
|---|---|
| JVM | ✅ |
Android (com.android.kotlin.multiplatform.library, minSdk 21) |
✅ |
iOS (iosArm64, iosSimulatorArm64) |
✅ |
macOS (macosArm64) |
✅ |
JS (js(IR) — browser & node) |
✅ |
Wasm (wasmJs — browser & node) |
✅ |
commonTest runs on all of the above. watchOS, tvOS, Linux, and
Windows aren't configured yet but pose no fundamental obstacle.
Apache License, Version 2.0 — see LICENSE.
The design of the compiler-compat layer and the plugin's apply-time Kotlin-version warning are inspired by ZacSweers/Metro (also Apache 2.0). See NOTICE.
⚠️ Experimental — research project building on DroidKaigi 2024 / 2025-style Compose architectures.
Fusio — Latin, "to pour together, to fuse" — is a Kotlin compiler plugin that decomposes Compose presenters without the usual I/O boilerplate at the seams.
A @Composable presenter that grows past a few features wants to be
split into smaller pieces. But splitting forces I/O boilerplate at
every seam: callback lambdas the parent threads down to each child,
event invocations the children fire back up to the parent. That
boilerplate scales with every new feature you add.
Fusio resolves those seams at compile time — fusion in the literal sense. Two sealed-type annotations declare which parent event routes into which child and which child effect lifts back up as which parent effect; the compiler synthesises the callback-and-invocation plumbing between them. The decomposed presenters end up reading as plain functions — no callback parameters threaded through, no manual event forwarding.
The library's core data type, Presentation<State, Event, Effect>,
is a state value, the effect stream the presenter produced alongside
it, and a send: (Event) -> Unit entry point the UI uses to push
input back in.
Apply the plugin:
plugins {
id("com.kitakkun.fusio") version "0.1.0"
}Declare event/effect routing on the parent's sealed types and fuse
sub-presenters inside buildPresenter:
sealed interface MyEvent {
@MapTo(ChildEvent.Add::class) data class AddItem(val title: String) : MyEvent
}
sealed interface MyEffect {
@MapFrom(ChildEffect.Added::class) data class ShowAdded(val title: String) : MyEffect
}
@Composable
fun PresenterScope<ChildEvent, ChildEffect>.childPresenter(): ChildState { /* … */ }
@Composable
fun myScreenPresenter(): Presentation<MyState, MyEvent, MyEffect> = buildPresenter {
val list = fuse { childPresenter() }
MyState(list.items)
}UI calls presentation.send(MyEvent.AddItem("milk")); the event is
routed into the child's on<ChildEvent.Add> handler, the child emits
a ChildEffect.Added, and the parent surfaces it as MyEffect.ShowAdded
on presentation.effectFlow. All routing plumbing is generated by the
compiler plugin — no reflection.
The runnable demo lives in demo/:
cd demo
../gradlew runJvm
fusio-test headless harness,
scenario API, sub-presenter testing, integration with Kotest /
Turbine.fusio-runtime internals.docs/ — additional design notes (event-handler
exhaustiveness, multi-compiler-version layer, etc.).| Your Kotlin | Supported |
|---|---|
| 2.3.0 – 2.3.x | ✅ |
| 2.4.0-Beta2+ | ✅ |
A single fusio-compiler-plugin jar ships with a per-Kotlin-version
compatibility layer inside it (a shaded fusio-compiler-compat +
kXXX submodule pattern inspired by
ZacSweers/Metro). The plugin
inspects the running Kotlin compiler at startup and ServiceLoader-
resolves the matching impl, so picking up a new patch needs no Fusio
release.
fusio-annotations, fusio-runtime, and fusio-test publish to:
| Target | Status |
|---|---|
| JVM | ✅ |
Android (com.android.kotlin.multiplatform.library, minSdk 21) |
✅ |
iOS (iosArm64, iosSimulatorArm64) |
✅ |
macOS (macosArm64) |
✅ |
JS (js(IR) — browser & node) |
✅ |
Wasm (wasmJs — browser & node) |
✅ |
commonTest runs on all of the above. watchOS, tvOS, Linux, and
Windows aren't configured yet but pose no fundamental obstacle.
Apache License, Version 2.0 — see LICENSE.
The design of the compiler-compat layer and the plugin's apply-time Kotlin-version warning are inspired by ZacSweers/Metro (also Apache 2.0). See NOTICE.