When should you choose `expect`/`actual` functions versus a common interface with per-platform implementations? What are the trade-offs, including testability and inlining?
answer
- expect/actual = one fixed binding, no dispatch, not mockable
- interface = swappable, injectable, testable, vtable cost
- Test an expect fun via a test-target actual
- Layer: expect leaf under an injectable interface
- expect can be inline; interface cannot avoid dispatch
basics
~10 sUse expect/actual for small, fixed platform calls where there's exactly one implementation per platform. Use a common interface when you want to swap or mock implementations, inject dependencies, or have more than one variant.
solid answer
~50 s`expect`/`actual` gives a single, compiler-enforced implementation per target with zero runtime indirection — ideal for thin leaf calls (UUID, clock, platform name). But it's **not abstraction you can swap or mock**: there's exactly one actual per target, resolved at compile time, so unit tests in `commonTest` can't substitute a fake easily (you'd add a test-target actual or use the JVM test set). A **common interface + per-platform implementation** (provided via a factory or DI) trades a vtable lookup for flexibility: you can inject mocks in tests, supply multiple implementations, and evolve the API behind the interface. Interfaces also avoid expect/actual's restriction that you can't mark expected functions arbitrary ways. Rule of thumb: expect/actual for *one fixed platform binding*; interface for *anything you want to vary, fake, or inject*. Many teams keep expect/actual only at the very bottom (a `expect fun currentTimeMillis()`) and wrap it in injectable interfaces above.
go deeper
Knows both approaches exist but not when to pick which.
Picks expect/actual for leaf calls and interfaces for swappable behavior.
Articulates the testability/inlining/dispatch trade-offs and the layered leaf-under-interface pattern.
Sets module-wide conventions for where platform seams live and how they stay testable across a big target matrix.
## Two ways to reach platform code KMP common code needs platform behavior in two flavors: 1. **expect/actual** — declare `expect fun`, supply one `actual fun` per target. 2. **Interface + impl** — declare an `interface Clock` in common, implement it per platform, and obtain the instance via a factory function (often itself an `expect fun createClock(): Clock`) or a DI container (Koin, Kodein, manual). ## What expect/actual is good at - **Zero indirection**: the call binds directly to the actual at compile time — no virtual dispatch. With `expect`/`actual` you can even make functions `inline` for hot paths. - **Compiler guarantee**: exactly one actual per target; you can't forget one. - **Minimal ceremony** for leaf platform calls. ## What it's bad at - **Not mockable**: there's one actual per target, fixed at compile time. To test common code that calls an `expect fun`, you must provide an actual in the **test target's** source set (e.g., a JVM test actual) — you can't inject a fake at runtime. - **No polymorphism/variants**: you can't have two implementations selectable at runtime. - **Refactoring friction**: changing the signature ripples to every target's actual. ## What interfaces give you - **Testability**: pass a fake `Clock` into the class under test in `commonTest`. - **Dependency injection**: register implementations, swap them per environment. - **Multiple implementations / runtime selection.** - **Stable seam** for evolving behavior behind the boundary. The cost is a small runtime dispatch and a bit more wiring (a factory or DI module). ## A pragmatic layered pattern ```kotlin // commonMain — tiny leaf binding expect fun systemNanoTime(): Long // commonMain — injectable seam built on top interface Clock { fun nowNanos(): Long } class SystemClock : Clock { override fun nowNanos() = systemNanoTime() } // tests inject a FakeClock implementing Clock ``` Here expect/actual sits at the very bottom (one fixed binding) while the **interface** above it stays mockable and injectable. ## Decision checklist - One fixed impl per platform, no need to mock it directly → **expect/actual**. - Need to fake/inject/swap, or multiple variants → **interface (+ optional expect factory)**. - Hot path needing inlining and no dispatch → **expect/actual (inline)**. - Large evolving subsystem → **interface** for a stable boundary.
- How do you unit-test common code that calls an `expect fun` without a real platform?Provide an `actual` in the test target's source set (e.g., jvmTest) returning deterministic values, or wrap the expect in an injectable interface and pass a fake in commonTest.
- Can an expect function be `inline`? Why might that matter?Yes; an inline expect/actual function avoids any dispatch and lets the body be inlined per target, useful on hot paths where an interface's virtual call is too costly.
saying these in an interview costs you the question
- Claiming expect/actual functions are easily mockable like interfaces
- Using expect/actual for large, evolving subsystems that need a stable seam
- Never wrapping platform leaves in injectable abstractions for testability
- Assuming interfaces have no runtime cost compared to expect/actual