# Static, Finally: Moving Android Code from Companion Objects to Companion Blocks

For a decade, every Android codebase written in Kotlin has carried the same quiet tax. You want a factory function, a constant, a `TAG`, a `newInstance()` for a Fragment, and the language answers with a nested singleton called `Companion`. It works, it is idiomatic, and it leaks into places you never planned for: Java call sites, reflection, ProGuard rules, bytecode size.

Kotlin 2.5.0-Beta1 introduces an experimental alternative: **companion blocks** and **companion extensions**. JetBrains describes the feature in [The Companions to Come](https://blog.jetbrains.com/kotlin/2026/09/the-companions-to-come/), and the full design lives in [KEEP-0449](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0449-companions-block-extension.md). This article is the Android engineer’s view: what changes, what to try first, and, more importantly, how an adoption goes wrong.

> ***Status note.*** *The feature is experimental. Snippets below follow the syntax shown in the announcement, so pin your toolchain and re-check against the KEEP before copying anything into production. Where I am reasoning from the design rather than quoting it, I say so.*

## 1\. The problem a companion object solves, and the one it creates

A companion object is a real object. That is its strength (it can implement interfaces, be passed around as a value) and its cost (the compiler must materialize a class and a holder for it).

Here is the pattern most Android modules contain today:

```kotlin
data class Money(val minorUnits: Long, val currency: Currency) {

    operator fun plus(other: Money): Money {
        require(currency == other.currency) { "Currency mismatch: $currency vs ${other.currency}" }
        return copy(minorUnits = minorUnits + other.minorUnits)
    }

    companion object {
        fun zero(currency: Currency) = Money(0L, currency)

        fun fromMajor(major: BigDecimal, currency: Currency): Money =
            Money(major.movePointRight(currency.defaultFractionDigits).longValueExact(), currency)
    }
}
```

On the JVM this produces a `Money` class, a nested `Money$Companion` class, and a static field on `Money` that holds the singleton. Kotlin callers never notice. Java callers, reflection-based libraries, and shrinker configurations do.

## 2\. What the new syntax looks like

The block form is deliberately a near-copy of the old one. The keyword `object` disappears:

```kotlin
data class Money(val minorUnits: Long, val currency: Currency) {

    companion {
        fun zero(currency: Currency) = Money(0L, currency)

        fun fromMajor(major: BigDecimal, currency: Currency): Money =
            Money(major.movePointRight(currency.defaultFractionDigits).longValueExact(), currency)
    }
}
```

Call sites in Kotlin are unchanged: `Money.zero(usd)`. What changed is the compilation strategy. According to the announcement, companion blocks compile to **static members** on platforms like the JVM, instead of members of a hidden singleton class.

![companion-bytecode-shapes](https://cdn.hashnode.com/uploads/posts/6ae6fd909af3dc0afb7a2ccf/4be41049-a676-4526-bd46-f86378b7658c.png align="center")

## Extensions: adding members to types you do not own

The second feature is the one Android developers will notice first. A companion extension attaches a type-level member to *any* class or interface, including Java classes and types without a companion:

```kotlin
import android.net.Uri

companion fun Uri.forRoute(route: String, query: Map<String, String> = emptyMap()): Uri =
    Uri.Builder()
        .scheme("shopapp")
        .authority("app")
        .appendPath(route)
        .apply { query.forEach { (k, v) -> appendQueryParameter(k, v) } }
        .build()

// call site
val link = Uri.forRoute("cart", mapOf("promo" to "DIWALI"))
```

`Uri.Builder` percent-encodes path segments and query parameters for you, so callers never hand-escape values. A `Map` parameter also avoids the array allocation a `vararg` of pairs creates on every call, which matters if routing code runs on a hot path.

Until now the same idea meant a top-level function with an awkward name (`uriForRoute(...)`) that no one could discover through autocomplete on `Uri.`. Here the function reads as if the platform class had always shipped it.

## 3\. Opting in, and who you lock out

Two compiler flags control the feature:

- `-Xcompanion-blocks` enables blocks.
- `-Xcompanion-blocks-and-extensions` enables both blocks and extensions.

In a Gradle build, apply them narrowly, through a convention plugin rather than ad hoc per module:

```kotlin
// build-logic: AndroidAppConventionPlugin.kt (sketch)
class AndroidAppConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        extensions.configure<KotlinAndroidProjectExtension> {
            compilerOptions {
                freeCompilerArgs.addAll("-Xcompanion-blocks-and-extensions")
            }
        }
    }
}
```

The reach of each flag is the part that bites. Per the announcement, turning on extensions makes the compiler emit **pre-release binaries**, so a library that uses companion extensions cannot be consumed as a dependency until the feature stabilizes. Exposing plain blocks from a library is allowed, but anyone consuming those blocks must also enable the blocks feature.

![companion-flags-and-reach](https://cdn.hashnode.com/uploads/posts/6ae6fd909af3dc0afb7a2ccf/6dfc1989-f613-4d29-977e-a318530a0470.png align="center")

The practical reading: extensions belong in leaf application modules for now. Blocks can reach library code, but only if you control, or can coordinate with, every consumer.

## 4\. Keep, migrate, or extend: a decision path

JetBrains is explicit that nobody has to migrate. Companion objects remain a guaranteed part of the language, and a handful of use cases only they serve. The clearest one: a companion that must implement an interface, because it compiles to a full class.

```kotlin
fun interface ScreenFactory<S : Screen> {
    fun create(args: Bundle): S
}

class CheckoutScreen(private val cartId: String) : Screen {

    // Must stay an object: it is passed around as a ScreenFactory value.
    companion object : ScreenFactory<CheckoutScreen> {
        override fun create(args: Bundle) =
            CheckoutScreen(requireNotNull(args.getString("cartId")) { "cartId missing" })
    }
}

// somewhere in navigation setup
registry.register("checkout", CheckoutScreen)   // the companion as a value
```

A `companion { }` block has no instance to hand over, so this registration style cannot move. Everything else (constants, factories, `newInstance` helpers) is a candidate.

![companion-decision-path](https://cdn.hashnode.com/uploads/posts/6ae6fd909af3dc0afb7a2ccf/ee14a51c-8da8-41f0-bd34-b23ba1763e51.png align="center")

The announcement describes mechanical migration as deleting the `object` keyword in most cases, followed by a recompile of the module and everything that depends on it.

## 5\. Failure modes: how this goes wrong in a real app

The syntax migration is trivial. The consequences are not. These are the places I would expect trouble, grouped along the build-to-runtime pipeline. Items marked *inferred* come from reasoning about the design, not from the announcement, so verify them against your own build.

![companion-failure-pipeline](https://cdn.hashnode.com/uploads/posts/6ae6fd909af3dc0afb7a2ccf/a6c5b942-ca8d-4f21-8268-7e358348a9b5.png align="center")

## Failure 1: the silent binary break in a published library

The announcement states that the migrated project and everything depending on it must be recompiled. For an app module that is a non-event. For an AAR you publish, it means existing consumers compiled against `Money.Companion.zero(...)` will fail when they pick up the new artifact. Treat a migration in public API as a binary-breaking change: bump the major version, or leave that library on companion objects.

## Failure 2: Java callers that stop compiling (inferred)

If any Java in your codebase, or in a downstream app, calls the member through the singleton accessor, that route is tied to the old shape:

```java
// Before: works because Companion is a real field holding a real object
Money zero = Money.Companion.zero(usd);
```

Since blocks compile to static members, I expect the Java form to become a plain static call, `Money.zero(usd)`. Search the repository for `.Companion.` before flipping anything. It also means that `@JvmStatic` on these members likely becomes redundant. Confirm in the KEEP rather than assuming.

## Failure 3: reflection that expects a companion instance

Anything that discovers type-level functionality by asking Kotlin reflection for the companion will find nothing after migration:

```kotlin
import kotlin.reflect.full.companionObjectInstance

fun <T : Any> KClass<T>.findFactory(): ScreenFactory<T>? =
    @Suppress("UNCHECKED_CAST")
    companionObjectInstance as? ScreenFactory<T>
```

For classes converted to blocks, `companionObjectInstance` returns `null`, and the code above quietly falls back to whatever default you wrote. Plugin systems, custom DI glue, and serialization helpers are the usual suspects. Grep for `companionObject` and `companionObjectInstance`, and write a unit test that asserts the lookup still succeeds for each registered type.

## Failure 4: R8 and ProGuard rules written for a class that no longer exists (inferred)

Keep rules aimed at `**$Companion` classes silently match nothing once the nested class is gone. The build stays green; the failure arrives in release builds as a stripped or renamed member that reflection or JNI still expects. After migrating, diff the R8 `usage.txt` and `seeds.txt` outputs for the affected classes, and rewrite the rule against the outer class's static members.

## Failure 5: tooling that has not caught up

The announcement is candid here: other ecosystem tools may not support blocks yet, and JetBrains is working with the major players. For an Android project that list is long: annotation processors, KSP-based code generators, lint rules, IDE inspections. Do not discover this on your main branch. Spend a day on a throwaway branch with the flag on and the full CI matrix running.

## Failure 6: a flag that leaks into a library

The cheapest way to reproduce the pre-release problem is to enable the extensions flag in a convention plugin that every module shares. A guard in the build prevents it:

```kotlin
// root build.gradle.kts (sketch)
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

subprojects {
    plugins.withId("maven-publish") {
        // afterEvaluate so flags added by convention plugins are already visible
        afterEvaluate {
            tasks.withType<KotlinCompilationTask<*>>().configureEach {
                val flags = compilerOptions.freeCompilerArgs.get()
                check("-Xcompanion-blocks-and-extensions" !in flags) {
                    "$path publishes artifacts; companion extensions would produce pre-release binaries."
                }
            }
        }
    }
}
```

Two details matter here. First, the check runs when Gradle configures the compile tasks, so a bad flag fails the build before any compilation starts, rather than midway through execution as a `doFirst` block would. Second, matching on `KotlinCompilationTask<*>` instead of the narrower `KotlinCompile` also covers Android variant and multiplatform compile tasks, which a `KotlinCompile`\-only match can miss depending on your Kotlin Gradle plugin version. Treat the snippet as a starting point and test it against your own AGP and KGP combination, including with the configuration cache on.

Failing at configuration time is far cheaper than failing in a consumer’s sync a week later.

## 6\. A rollout plan I would actually run

1. **Canary branch.** Enable `-Xcompanion-blocks` in one app module, convert a single class, run unit tests, instrumentation tests, and a release build with R8.
2. **Grep before you edit.** Search for `.Companion.`, `companionObject`, and `$Companion` across Kotlin, Java, and keep-rule files.
3. **Convert leaf utilities first.** Constants and pure factories, with no interface implementations and no reflection consumers.
4. **Leave interface-bearing companions alone.** They are first-class citizens, not legacy.
5. **Add extensions last, and only in apps.** Start with platform types where the readability win is obvious, such as `Uri`, `Bundle`, and `Intent` helpers.
6. **Gate libraries.** Add the publishing guard above before any teammate touches a shared convention plugin.

## 7\. Where this leaves Android and Multiplatform

For pure Android work, the benefit is cleaner bytecode and a more natural Java story. For Kotlin Multiplatform, the announcement points to a larger payoff: because blocks compile like platform statics, an expected companion block member can be actualized by a Java class with static members. That removes a long-standing wrinkle at the Android boundary. The KEEP has the exact rules, and I would read it in full before designing a shared API around it.

My position: adopt blocks for new application code once your toolchain passes the canary branch; defer extensions in anything you publish; and never migrate a companion just because you can. The old `companion object` is not deprecated and not going away. The new form is a sharper tool, not a replacement.

**Sources**

- [The Companions to Come, JetBrains Kotlin Blog](https://blog.jetbrains.com/kotlin/2026/09/the-companions-to-come/)
- [KEEP-0449: Companion blocks and extensions](https://github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0449-companions-block-extension.md)

## 📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend **“Mastering Data Structures & Algorithms in Java”**. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.

- **E-book:** [**Available on Google Play**](https://play.google.com/store/books/details?id=biiSEQAAQBAJ)
- **Kindle Edition:** [**Available on Amazon**](https://www.amazon.com/dp/B0FXWL8XMB)
- **Topmate:** [**Available on Topmate**](https://topmate.io/sivavishnu_r/2248718)
- Also available in **Paperback & Hardcover.**

---

*Originally published on* [*Medium*](https://medium.com/@sivavishnu0705/static-finally-moving-android-code-from-companion-objects-to-companion-blocks-ca0cee1bdf67)*.*
