cream
0.8.1indexedSimplifies 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.
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.
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.
copy(), but across classes
(e.g. Loading → Success). Designed for sealed class/interface state management.See also the comparison with other mapping libraries (MapStruct, KOMM).
<cream-version> | |
<ksp-version> |
// 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
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 (
: String,
)
}
: UiState.Success =
uiState: UiState =
nextUiState: UiState.Success = uiState.copyToUiStateSuccess(
= ,
)
Constructor parameters that match property names of the source class get default values, so you only pass what changed. Details: Copy
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.
See UI state management with sealed classes for details.
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:
See cross-layer model mapping for details.
See the docs below for the details of each feature.
When you need finer-grained customization, see the following.
sealed interface HomeState {
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") } },
)
}
}
// domain layer
data class Item(
val itemId: String,
val name: String,
val price: Int,
)
// data layer
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()
}
}
| 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 |
@ParentOptional | Property of a sealed child class | Nullable accessor exposing the property on the sealed parent (val Parent.prop: T?) | docs |
@ChildOptionals | Sealed class/interface | Nullable accessors on the sealed parent for all leaf-declared properties | 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 |
| 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 |
Wrap/unwrap value class properties automatically (always on) | automatic — opt out with cream.autoValueClassMapping=false | Value class mapping |
| 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 |
Surfaced from shared tags and platforms — no rankings paid for.