Every mobile project starts the same way: a week arguing about state before a line of product gets written. After years of cycling through ViewModels, Bloc, Redux, MVVM, MVI, I built Anchor — a minimal, opinionated, type-safe state management library for Kotlin Multiplatform.
The problem
You start a new project. There's an idea you're excited about, maybe a tight deadline, and you want to focus on shipping it. But before any business logic gets written, the same questions surface. Where does the state live? Which pattern fits the views? What happens to it across the screen's lifecycle? How will any of this be tested?
State management libraries rarely answer these questions outright. Most are foundations — flexible primitives that assume you'll wire them up your own way. The freedom is real, but so is the cost: every team rediscovers the same patterns, every project reinvents its own conventions, and the idea you came here to build waits while you read about testing strategies.
The friction compounds across platforms. Kotlin Multiplatform lets you share business logic between Android and iOS, but the state story stops at the platform boundary. Android reaches for ViewModel. iOS picks whichever architecture happens to be in fashion that quarter. The shared module ends up as a thin layer of helpers, while the interesting decisions — state shape, side effects, navigation events — get duplicated in two languages and two mental models.
No library in the Kotlin ecosystem takes a clear, opinionated stance without the boilerplate, and that's the gap Anchor is trying to fill.
Design philosophy
The idea is simple: business logic from the first line. No ViewModel setup, no scope wiring, no testing scaffolding. Define an anchor, write functions, and ship.
The trade-off is honest. Anchor is opinionated by design. There is no escape hatch into the underlying machinery, no second way to structure your state. If you've used Elm, the feel will be familiar — the runtime is Anchor's problem. Yours is describing what the program does, not how it's wired.
Two pillars hold the model up:
- •State — an immutable
ViewStatedata class that gets replaced, never mutated - •Effects — external dependencies (network, database, analytics) scoped to the anchor
Everything else — actions, signals, cancellation, testing — falls out of these two ideas.
A minimal anchor fits on the back of a napkin:
data class CounterState(val count: Int = 0) : ViewState
typealias CounterAnchor = Anchor<EmptyEffect, CounterState, Nothing>
fun RememberAnchorScope.counterAnchor(): CounterAnchor =
create(initialState = ::CounterState, effectScope = { EmptyEffect })
fun CounterAnchor.increment() {
reduce { copy(count = count + 1) }
}
State, anchor, action. No base class, no event hierarchy, no view-model glue — increment is a regular Kotlin function that happens to be defined on CounterAnchor.
Who is it for?
Anchor is built for Kotlin Multiplatform teams who'd rather ship product than wire up plumbing. If you share business logic between Android and iOS, prefer compile-time guarantees to runtime surprises, and need a testing story that doesn't change shape with the platform — it's aimed at you.
In practice, that looks like:
- •Teams standing up Android and iOS from a single shared module, who can't afford to keep two architectures in their head
- •A mature codebase migrating off duplicated ViewModel-side and Swift-side state holders, tired of writing the same feature twice
- •Apps where correctness matters more than maximum flexibility
Anchor trades flexibility for clarity. That's the deal!
Testing
The test story is the part most state management libraries hand-wave. Anchor ships anchor-test: a recording runtime and a BDD-style DSL that runs in commonTest.
@Test
fun `incrementing updates the count`() {
runAnchorTest(RememberAnchorScope::counterAnchor) {
given("the screen started at zero") {
initialState { CounterState(count = 0) }
}
on("incrementing the counter", CounterAnchor::increment)
verify("the count moves to one") {
assertState { copy(count = 1) }
}
}
}
given seeds the initial state and lets you swap in fake effects.
on calls the exact extension function you wrote for production.
verify asserts against the recorded sequence of reduce, post, and emit calls, in the order they happened.
The verify block is exhaustive: every reduce, post, and emit must be matched, and assertions compare the full produced value, not its shape. No accidentally passing because you checked list.size instead of the list, no forgotten signal alongside a state change.
Current state and roadmap
Anchor is at version 0.1.3 and is published to Maven Central and GitHub Packages. The core concept is stable, but the API is actively evolving.
If you are building a Kotlin Multiplatform app and feel the pain of bolting together separate state management solutions per platform, give Anchor a look.
Add the artefacts to your shared module's build.gradle.kts:
dependencies {
implementation("dev.kioba.anchor:anchor:0.1.3")
implementation("dev.kioba.anchor:anchor-compose:0.1.0")
testImplementation("dev.kioba.anchor:anchor-test:0.1.0")
}
Docs at kioba.github.io/anchor, source at github.com/kioba/anchor.
Version 0.1.3. Opinions included. Pull requests welcome.