Kapture - Kotlin/Native State Capture
Tags: kotlin, kotlinmultiplatform, android, ios
Published: 2020-01-21 19:27:33

---
title: Kapture - Kotlin/Native State Capture
published: true
description: Intellij plugin to help avoid capturing and freezing state unintentionally
tags: kotlin, kotlinmultiplatform, android, ios
---

Kotlin/Native state is different than what most Kotlin developers would be used to. To be shared between threads, state must be "frozen", which makes it immutable in the runtime.

For background on Kotlin/Native concurrency and state, see [Practical Kotlin Native Concurrency](https://dev.to/touchlab/practical-kotlin-native-concurrency-ac7).

Once the developer gets the general idea of how this works, it's not too complicated. However, capturing and freezing state unintentionally is a problem. Especially when combined with concurrency libraries.

## Library Model

If you use the KN primitive [`Worker`](https://github.com/JetBrains/kotlin-native/blob/master/runtime/src/main/kotlin/kotlin/native/concurrent/Worker.kt) to implement concurrency, you'll need to be very explicit about state transfer or freezing.

In practice, few developers will use `Worker` directly. All current concurrency libraries opt to take a lambda argument that will be frozen, then run on another thread.

That makes concurrency in KN very simple. New developers often complain about the restrictions of the KN model, but KN forces you to rethink your architecture and be intentional about your mutable state. If you learn to work with the model, it's actually a good thing.

However, because lambdas can capture state, and freezing aggressively freezes everything it touches, you can easily wind up freezing state unintentionally. This can be very frustrating and lead to a lot of the beginner trouble with KN's model.

Take the following example:

```kotlin
class Product(val id:Long){
    fun saveOrder(order:Order){
        background {
            saveToDb(id, order)
        }
    }
}
```

I've been bitten by this problem on multiple occasions. The method `background` is going to take the lambda argument and freeze it, and run `saveToDb(id, order)` on another thread. That means `id` and `order` will be frozen.

Visually it looks like you're just freezing those two values. `order` might be a problem, because we don't know if the `Order` type should be frozen, but the real problem here is `id` is actually attached to `Product`. We wind up freezing the whole `Product` class.

Seeing what state is captured is not always obvious. That is relatively easy to determine with static analysis, and could be surfaced to the developer in the IDE.

However, not all state is problematic to freeze. In the simple example above, although `Product` is frozen, as presented, that's not a problem. There's nothing about `Product` that would be mutable anyway.

However, maybe it looked more like this:

```kotlin
class Product(val id:Long){

    var quantity:Int = 0

    fun saveOrder(order:Order){
        background {
            saveToDb(id, order)
        }
    }
}
```

Although perhaps not an architecturally good decision, the code above now has a real issue.

## Kapture

Kapture is a code inspection Intellij plugin that finds "hot" functions, collects what state will be captured in the lambda arg(s), and evaluates if that state is problematic to freeze. If so, it'll create an error.

![Basic id capture](https://thepracticaldev.s3.amazonaws.com/i/3vglq32slscymh81zy4r.gif)

The inspection will add an issue to `id`, and hovering over it will pop up a summary of what's wrong and offer some fixes.

## Education

The goal of Kapture is not simply to warn you of captured state. The options in the fix list will include links to documentation explaining the issue and potential best-practice approaches to refactoring your state architecture.

For example, in the scenario above, it's better to cross thread boundaries with a named function and use function parameters to avoid capturing the parent class.

```kotlin
class Product(val id:Long){

    var quantity:Int = 0

    fun saveOrder(order:Order){
        save(id, order)
    }
    
    private fun save(id: Long, order: Order) = background { saveToDb(id, order) }
}
```

## Hot Functions

Hot functions are functions that take function arguments and may cross thread boundaries. There are some built-in functions that Kapture recognizes. These include `withContext` from corountines, and methods from `Flow`. You can also annotate your own with `@FreezingBlockCall`. This tells capture to treat `block` as special and look at it's captured state.

```kotlin
@FreezingBlockCall
fun <R> background(block: () -> R): R = block()
```

Sometimes a function won't actually cross a thread boundary at runtime, but it can be difficult to know for sure at compile time. Kapture, in general, leans conservative and will warn you of potential issues in ambiguous cases.

If you're in the main thread and call the following, you won't actually switch threads (and coroutines shouldn't freeze the lambda), but Kapture treats it as a potentially thread-crossing method anyway.

```kotlin

withContext(Dispatchers.Main){
    //What if you were already in the main thread?
}
```
## Mutability Analysis

How do we determine if something is immutable? In summary, Kapture analyses the captured data. Everything has to be final and immutable.

For example, the following is OK:

```kotlin
data class SomeData(val s:String)
```

However, this is not:

```kotlin
data class SomeData(val s:Any)
```

The val `s` is of type `Any`, which is not final.

There are many other rules, including special classes. For example, anything from Sqldelight is considered safe to freeze, so those classes are ignored. On the other end, it's relatively easy to capture test classes, so even if your test class has nothing mutable, it'll be flagged as an issue.

```kotlin
class ProductTest{
    
    val db : DbHelper = inject()
    
    @Test
    fun heyTest() = background {
        db.whatever() // <- issue here
    }
}

```

You can override that, or anything, with @Freezeable.

```kotlin
@Freezeable
class ProductTest{

    val db : DbHelper = inject()

    @Test
    fun heyTest() = background {
        db.whatever()
    }
}
```

Kapture can also see some calls in the init block, so this would also work:

```kotlin
class ProductTest{
    init {
        freeze()
    }

    val db : DbHelper = inject()

    @Test
    fun heyTest() = background {
        db.whatever()
    }
}
```

There are a number of detailed inspections which you can read about in the docs, and more to be added to increase the accuracy. However, the important point to remember is if we can't reasonably verify that you can freeze some state, you'll get an error. It is intentionally conservative.

## 80/20

KN verifies frozen at runtime, which has been a criticism of the model. Doing this at compile-time, like Rust, would be great, but would require a significantly different language. There's only so much the compiler can know about what you're doing without those language changes.

Kapture operates under the "pretty good" model. It'll make pretty good guesses, and you can override it when it's wrong. "Pretty good" isn't a virtue for compilers, but for developer tools, it's a different story.

## Evaluation

We're currently looking for private testers to give feedback. We'd like to know if this is actually useful in practice and if it helps explain how the KN state model works. Please sign up here for access.

[http://go.touchlab.co/Kapture-Devto](http://go.touchlab.co/Kapture-Devto)

## Special Thanks

This idea was kind of a combination of some slides in my concurrency talk, and chats with Eugenio Marletti and Scott Pierce in and around the Kotlin Slack.