cream

Simplifies class state transitions by automatically generating copy functions using annotations. Facilitates seamless inheritance of previous state data, supporting cross-class state transitions and reducing boilerplate code.

Android JVMJVMKotlin/NativeWasmJS
GitHub stars4
AuthorsTBSten
Dependents0
LicenseApache License 2.0
Creation dateabout 1 year ago

Last activity5 days ago
Latest release0.9.0-alpha02 (5 days ago)

cream.kt

Maven Central Version GitHub License Ask DeepWiki

English | 日本語 | DeepWiki

Contents: Why cream.kt? · Setup · Quick Start · Annotations · Customization · Use Cases


cream.kt is a KSP plugin that enables declarative data copy and makes it easy to copy across classes. Annotate a class, and cream automatically generates a copy function to another, similar class — properties with matching names are carried over for you.

// Without cream.kt
// ❌ Hard to see which data was actually added or changed
MyUiState.Success(
    userName = prevState.userName,    // manual copy
    password = prevState.password,    // manual copy
    data = newData,
)

// With cream.kt — copyToMyUiStateSuccess is generated automatically
// ✅ Only the data that changed stands out
prevState.copyToMyUiStateSuccess(data = newData)

Function names are customizable (e.g. shorten it to toSuccess) — see Function name.

Why cream.kt?

  • Declarative data copy — one annotation generates the copy function; properties with matching names become default arguments, so you only pass what changed.
  • State transitions across classes — like data class copy(), but across classes (e.g. LoadingSuccess). Designed for sealed class/interface state management.
  • Kotlin Multiplatform ready — runtime annotations are published for all Kotlin platforms.

See also the comparison with other mapping libraries (MapStruct, KOMM).

Setup

<cream-version> GitHub Release
<ksp-version> GitHub Release
// module/build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "<ksp-version>"
}

dependencies {
    implementation("me.tbsten.cream:cream-runtime:<cream-version>")
    ksp("me.tbsten.cream:cream-ksp:<cream-version>")
}

Kotlin Multiplatform (commonMain) requires additional setup due to a KSP limitation → Kotlin Multiplatform support

Quick Start

Annotate the source class with @CopyTo, and cream generates a copy function to the target class:

import me.tbsten.cream.CopyTo

@CopyTo(UiState.Success::class)
class UiState {
    data class Success(
        val data: String,
    )
}

// Auto-generated
fun UiState.copyToUiStateSuccess(
    data: String,
): UiState.Success = /* ... */

// Usage
val uiState: UiState = /* ... */
val nextUiState: UiState.Success = uiState.copyToUiStateSuccess(
    data = /* ... */,
)

Constructor parameters that match property names of the source class get default values, so you only pass what changed. Details: Copy

Use Cases

UI state transitions

In GUI apps (such as Android apps) where you need to manage screen state, modeling the state as a sealed interface is convenient — but the constructor calls at each state transition tend to become hard to read.

With cream.kt you can keep the state transitions simple while sticking with sealed interfaces.

sealed interface HomeState {
    @CopyTo(Success::class, Error::class)
    data object Loading : HomeState

    data class Success(
        val data: HomeScreenData,
    ) : HomeState

    data class Error(
        val message: String,
    ) : HomeState
}

class HomeViewModel : ViewModel() {
    private val _state = MutableStateFlow<HomeState>(HomeState.Loading)

    fun initialLoad() = viewModelScope.launch {
        val loadingState = HomeState.Loading
        _state.update { loadingState }

        runCatching {
            fetchHomeScreenDataFromServer()
        }.fold(
            onSuccess = { _state.update { loadingState.copyToHomeStateSuccess(data = it) } },
            onFailure = { _state.update { loadingState.copyToHomeStateError(message = it.message ?: "Unknown error") } },
        )
    }
}

See UI state management with sealed classes for details.

Layers

Defining separate models for the data layer and the domain layer keeps data-layer changes from affecting the rest of the app (such as the UI layer).

In small-to-mid-sized apps, however, this mapping often produces tedious boilerplate. With cream.kt you can replace the hand-written mapping code with generated functions:

// domain layer
data class Item(
    val itemId: String,
    val name: String,
    val price: Int,
)

// data layer
@CopyTo(Item::class)
data class GetItemApiResponse(
    val itemId: String,
    val name: String,
    val price: Int,
)

class ItemRepositoryImpl : ItemRepository {
    override suspend fun getItem(itemId: String): Item {
        val apiResponse = itemApi.getItem(itemId)
        return apiResponse.copyToItem()
    }
}

See cross-layer model mapping for details.

Annotations

See the docs below for the details of each feature.

Annotation Put it on Generates Docs
@CopyTo(Target::class) Source class Copy function from source to target docs
@CopyFrom(Source::class) Target class Same as @CopyTo, annotation placed on the target side docs
@CopyMapping(Source::class, Target::class) A declaration in your module Copy function between two classes you cannot modify (e.g. library classes) docs
@CopyToChildren Sealed class/interface Copy functions from the sealed parent to all concrete leaves docs
@SealedCopy Sealed class/interface copy() on the sealed parent that preserves the subtype docs
@CombineTo(Target::class) Each source class Combine function from multiple sources to one target docs
@CombineFrom(SourceA::class, SourceB::class, ...) Target class Same as @CombineTo, annotation placed on the target side docs
@CombineMapping(...) A declaration in your module Combine function between classes you cannot modify docs

Customization

When you need finer-grained customization, see the following.

I want to... API Docs
Map properties whose names differ .Map (e.g. @CopyTo.Map) Property mapping
Drop the auto-copy default and make callers pass a value .Exclude (e.g. @CopyTo.Exclude), excludes for mapping annotations Exclude
Add my own notes/examples to the generated KDoc kdoc = KDoc(...) KDoc
Control the visibility of generated functions visibility / CopyVisibility Visibility
Rename generated functions (per-declaration / module-wide) funName / cream.copyFunNamePrefix / … Function name
See all module-wide KSP options cream.* KSP options KSP Options
Android JVMJVMKotlin/NativeWasmJS
GitHub stars4
AuthorsTBSten
Dependents0
LicenseApache License 2.0
Creation dateabout 1 year ago

Last activity5 days ago
Latest release0.9.0-alpha02 (5 days ago)

cream.kt

Maven Central Version GitHub License Ask DeepWiki

English | 日本語 | DeepWiki

Contents: Why cream.kt? · Setup · Quick Start · Annotations · Customization · Use Cases


cream.kt is a KSP plugin that enables declarative data copy and makes it easy to copy across classes. Annotate a class, and cream automatically generates a copy function to another, similar class — properties with matching names are carried over for you.

// Without cream.kt
// ❌ Hard to see which data was actually added or changed
MyUiState.Success(
    userName = prevState.userName,    // manual copy
    password = prevState.password,    // manual copy
    data = newData,
)

// With cream.kt — copyToMyUiStateSuccess is generated automatically
// ✅ Only the data that changed stands out
prevState.copyToMyUiStateSuccess(data = newData)

Function names are customizable (e.g. shorten it to toSuccess) — see Function name.

Why cream.kt?

  • Declarative data copy — one annotation generates the copy function; properties with matching names become default arguments, so you only pass what changed.
  • State transitions across classes — like data class copy(), but across classes (e.g. LoadingSuccess). Designed for sealed class/interface state management.
  • Kotlin Multiplatform ready — runtime annotations are published for all Kotlin platforms.

See also the comparison with other mapping libraries (MapStruct, KOMM).

Setup

<cream-version> GitHub Release
<ksp-version> GitHub Release
// module/build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "<ksp-version>"
}

dependencies {
    implementation("me.tbsten.cream:cream-runtime:<cream-version>")
    ksp("me.tbsten.cream:cream-ksp:<cream-version>")
}

Kotlin Multiplatform (commonMain) requires additional setup due to a KSP limitation → Kotlin Multiplatform support

Quick Start

Annotate the source class with @CopyTo, and cream generates a copy function to the target class:

import me.tbsten.cream.CopyTo

@CopyTo(UiState.Success::class)
class UiState {
    data class Success(
        val data: String,
    )
}

// Auto-generated
fun UiState.copyToUiStateSuccess(
    data: String,
): UiState.Success = /* ... */

// Usage
val uiState: UiState = /* ... */
val nextUiState: UiState.Success = uiState.copyToUiStateSuccess(
    data = /* ... */,
)

Constructor parameters that match property names of the source class get default values, so you only pass what changed. Details: Copy

Use Cases

UI state transitions

In GUI apps (such as Android apps) where you need to manage screen state, modeling the state as a sealed interface is convenient — but the constructor calls at each state transition tend to become hard to read.

With cream.kt you can keep the state transitions simple while sticking with sealed interfaces.

sealed interface HomeState {
    @CopyTo(Success::class, Error::class)
    data object Loading : HomeState

    data class Success(
        val data: HomeScreenData,
    ) : HomeState

    data class Error(
        val message: String,
    ) : HomeState
}

class HomeViewModel : ViewModel() {
    private val _state = MutableStateFlow<HomeState>(HomeState.Loading)

    fun initialLoad() = viewModelScope.launch {
        val loadingState = HomeState.Loading
        _state.update { loadingState }

        runCatching {
            fetchHomeScreenDataFromServer()
        }.fold(
            onSuccess = { _state.update { loadingState.copyToHomeStateSuccess(data = it) } },
            onFailure = { _state.update { loadingState.copyToHomeStateError(message = it.message ?: "Unknown error") } },
        )
    }
}

See UI state management with sealed classes for details.

Layers

Defining separate models for the data layer and the domain layer keeps data-layer changes from affecting the rest of the app (such as the UI layer).

In small-to-mid-sized apps, however, this mapping often produces tedious boilerplate. With cream.kt you can replace the hand-written mapping code with generated functions:

// domain layer
data class Item(
    val itemId: String,
    val name: String,
    val price: Int,
)

// data layer
@CopyTo(Item::class)
data class GetItemApiResponse(
    val itemId: String,
    val name: String,
    val price: Int,
)

class ItemRepositoryImpl : ItemRepository {
    override suspend fun getItem(itemId: String): Item {
        val apiResponse = itemApi.getItem(itemId)
        return apiResponse.copyToItem()
    }
}

See cross-layer model mapping for details.

Annotations

See the docs below for the details of each feature.

Annotation Put it on Generates Docs
@CopyTo(Target::class) Source class Copy function from source to target docs
@CopyFrom(Source::class) Target class Same as @CopyTo, annotation placed on the target side docs
@CopyMapping(Source::class, Target::class) A declaration in your module Copy function between two classes you cannot modify (e.g. library classes) docs
@CopyToChildren Sealed class/interface Copy functions from the sealed parent to all concrete leaves docs
@SealedCopy Sealed class/interface copy() on the sealed parent that preserves the subtype docs
@CombineTo(Target::class) Each source class Combine function from multiple sources to one target docs
@CombineFrom(SourceA::class, SourceB::class, ...) Target class Same as @CombineTo, annotation placed on the target side docs
@CombineMapping(...) A declaration in your module Combine function between classes you cannot modify docs

Customization

When you need finer-grained customization, see the following.

I want to... API Docs
Map properties whose names differ .Map (e.g. @CopyTo.Map) Property mapping
Drop the auto-copy default and make callers pass a value .Exclude (e.g. @CopyTo.Exclude), excludes for mapping annotations Exclude
Add my own notes/examples to the generated KDoc kdoc = KDoc(...) KDoc
Control the visibility of generated functions visibility / CopyVisibility Visibility
Rename generated functions (per-declaration / module-wide) funName / cream.copyFunNamePrefix / … Function name
See all module-wide KSP options cream.* KSP options KSP Options