13 KiB
Sealed Class Catalog
Comprehensive list of sealed types in AmethystMultiplatform with usage patterns.
Table of Contents
State Management
AccountState (Sealed Class)
File: commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-46
sealed class AccountState {
data object LoggedOut : AccountState()
data class LoggedIn(
val signer: NostrSigner,
val pubKeyHex: String,
val npub: String,
val nsec: String?,
val isReadOnly: Boolean
) : AccountState()
}
Why sealed class:
- Two distinct states with different data
LoggedInholds data,LoggedOutdoesn't- No need for generics or multiple inheritance
Usage:
fun handleAccountState(state: AccountState) {
when (state) {
is AccountState.LoggedOut -> showLogin()
is AccountState.LoggedIn -> {
showFeed(
pubkey = state.pubKeyHex,
canSign = !state.isReadOnly
)
}
} // Exhaustive - compiler enforces
}
VerificationState (Sealed Class)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationState.kt
sealed class VerificationState {
data object NotStarted : VerificationState()
data object Started : VerificationState()
data class Failed(val reason: String) : VerificationState()
data object Verified : VerificationState()
}
Pattern:
- State machine (NotStarted → Started → Failed/Verified)
- Only
Failedcarries data (reason) - Rest are singletons (
data object)
Result Types
SignerResult (Sealed Interface with Generics)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt:25-46
sealed interface SignerResult<T : IResult> {
sealed interface RequestAddressed<T : IResult> : SignerResult<T> {
class Successful<T : IResult>(val result: T) : RequestAddressed<T>
class Rejected<T : IResult> : RequestAddressed<T>
class TimedOut<T : IResult> : RequestAddressed<T>
class ReceivedButCouldNotPerform<T : IResult>(
val message: String? = null
) : RequestAddressed<T>
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
val eventJson: String
) : RequestAddressed<T>
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
val invalidEvent: Event
) : RequestAddressed<T>
}
}
interface IResult
data class SignResult(val event: Event) : IResult
data class EncryptionResult(val ciphertext: String) : IResult
data class DecryptionResult(val plaintext: String) : IResult
Why sealed interface:
- Generic result type
<T : IResult> - Nested sealed hierarchy (RequestAddressed)
- Need covariance for flexible result types
Usage:
suspend fun signEvent(event: Event): SignerResult<SignResult> {
return when (val result = remoteSigner.sign(event)) {
is SignerResult.RequestAddressed.Successful -> result
is SignerResult.RequestAddressed.Rejected -> {
logger.warn("Signing rejected")
result
}
is SignerResult.RequestAddressed.TimedOut -> {
logger.error("Signing timed out")
result
}
is SignerResult.RequestAddressed.ReceivedButCouldNotPerform -> {
logger.error("Signer error: ${result.message}")
result
}
}
}
CacheResults (Sealed Class with Generics)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt
sealed class CacheResults<T> {
data class Found<T>(val value: T) : CacheResults<T>()
class NotFound<T> : CacheResults<T>()
}
Pattern:
- Simple binary result (found/not found)
Foundcarries data,NotFounddoesn't- Generic for reusability
Tag Variants
MuteTag (Sealed Class)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt
sealed class MuteTag(
val nameOrNull: String?,
val valueOrNull: String?
) {
class Event(eventId: String) : MuteTag("e", eventId)
class Profile(pubkey: String) : MuteTag("p", pubkey)
class Word(word: String) : MuteTag("word", word)
class Thread(threadId: String) : MuteTag("thread", threadId)
companion object {
fun parse(tag: Array<String>): MuteTag? {
return when (tag.getOrNull(0)) {
"e" -> tag.getOrNull(1)?.let { Event(it) }
"p" -> tag.getOrNull(1)?.let { Profile(it) }
"word" -> tag.getOrNull(1)?.let { Word(it) }
"thread" -> tag.getOrNull(1)?.let { Thread(it) }
else -> null
}
}
}
fun toArray(): Array<String> {
return arrayOf(nameOrNull ?: "", valueOrNull ?: "")
}
}
Pattern:
- Common base class with shared properties
- Each variant represents different tag type
- Factory method
parse()for parsing toArray()for serialization
BookmarkIdTag (Sealed Class)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt
sealed class BookmarkIdTag {
abstract val id: String
abstract val marker: String?
data class Event(override val id: String, override val marker: String?) : BookmarkIdTag()
data class Profile(override val id: String, override val marker: String?) : BookmarkIdTag()
data class Address(override val id: String, override val marker: String?) : BookmarkIdTag()
companion object {
fun parse(tag: Array<String>): BookmarkIdTag? {
val marker = tag.getOrNull(3)
return when (tag.getOrNull(0)) {
"e" -> tag.getOrNull(1)?.let { Event(it, marker) }
"p" -> tag.getOrNull(1)?.let { Profile(it, marker) }
"a" -> tag.getOrNull(1)?.let { Address(it, marker) }
else -> null
}
}
}
}
Pattern:
- Abstract properties in sealed class
- Data classes implement abstract properties
- Parse factory returns sealed variant
Exception Hierarchies
SignerExceptions (Sealed Class)
File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt
sealed class SignerExceptions(message: String) : Exception(message) {
class UnableToSign(message: String) : SignerExceptions(message)
class UnableToDecrypt(message: String) : SignerExceptions(message)
class UnableToEncrypt(message: String) : SignerExceptions(message)
class UnableToGetPublicKey(message: String) : SignerExceptions(message)
}
Pattern:
- Sealed exception hierarchy
- Extends
Exceptionbase class - Type-safe error handling
Usage:
try {
signer.sign(event)
} catch (e: SignerExceptions) {
when (e) {
is SignerExceptions.UnableToSign -> logger.error("Signing failed: ${e.message}")
is SignerExceptions.UnableToDecrypt -> logger.error("Decryption failed: ${e.message}")
is SignerExceptions.UnableToEncrypt -> logger.error("Encryption failed: ${e.message}")
is SignerExceptions.UnableToGetPublicKey -> logger.error("No public key: ${e.message}")
}
}
Sealed Class vs Sealed Interface
When to Use Sealed Class
Examples from codebase:
- AccountState - State variants with different data
- VerificationState - State machine
- MuteTag - Tag variants with common base properties
- SignerExceptions - Exception hierarchy
Characteristics:
- Need common constructor parameters
- Single inheritance only
- State variants
- Exception hierarchies
When to Use Sealed Interface
Examples from codebase:
- SignerResult - Generic result types needing variance
- RelayUrlNormalizer.Result - Binary result with no shared state
Characteristics:
- Need generics with variance (
out,in) - No common state needed
- Multiple inheritance possible
- Contract/capability representation
Patterns
Pattern: State Machine
sealed class ConnectionState {
data object Disconnected : ConnectionState()
data object Connecting : ConnectionState()
data class Connected(val relay: String) : ConnectionState()
data class Failed(val error: String) : ConnectionState()
}
// Allowed transitions
fun transition(from: ConnectionState, event: Event): ConnectionState {
return when (from) {
is ConnectionState.Disconnected -> {
when (event) {
is Event.Connect -> ConnectionState.Connecting
else -> from
}
}
is ConnectionState.Connecting -> {
when (event) {
is Event.Success -> ConnectionState.Connected(event.relay)
is Event.Error -> ConnectionState.Failed(event.message)
is Event.Cancel -> ConnectionState.Disconnected
else -> from
}
}
is ConnectionState.Connected -> {
when (event) {
is Event.Disconnect -> ConnectionState.Disconnected
is Event.Error -> ConnectionState.Failed(event.message)
else -> from
}
}
is ConnectionState.Failed -> {
when (event) {
is Event.Retry -> ConnectionState.Connecting
is Event.Cancel -> ConnectionState.Disconnected
else -> from
}
}
}
}
Pattern: Result Type
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Exception) : Result<Nothing>
data object Loading : Result<Nothing>
}
// Extension functions
fun <T> Result<T>.getOrNull(): T? = when (this) {
is Result.Success -> data
else -> null
}
fun <T> Result<T>.getOrThrow(): T = when (this) {
is Result.Success -> data
is Result.Error -> throw exception
is Result.Loading -> error("Still loading")
}
fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
is Result.Success -> Result.Success(transform(data))
is Result.Error -> this
is Result.Loading -> Result.Loading
}
Pattern: Tagged Union (Discriminated Union)
sealed class Command {
data class SendEvent(val event: Event) : Command()
data class Subscribe(val filters: List<Filter>) : Command()
data class Unsubscribe(val subId: String) : Command()
data object Close : Command()
fun toJson(): String = when (this) {
is SendEvent -> """["EVENT",${event.toJson()}]"""
is Subscribe -> """["REQ","sub",${filters.joinToString { it.toJson() }}]"""
is Unsubscribe -> """["CLOSE","$subId"]"""
is Close -> """["CLOSE"]"""
}
}
Pattern: Nested Sealed Hierarchies
sealed interface UiState {
sealed interface Loading : UiState {
data object Initial : Loading
data class Refreshing(val currentData: List<Item>) : Loading
}
sealed interface Content : UiState {
data class Success(val data: List<Item>) : Content
data object Empty : Content
}
sealed interface Error : UiState {
data class Network(val message: String) : Error
data class Server(val code: Int, val message: String) : Error
}
}
// Usage
fun renderUi(state: UiState) {
when (state) {
is UiState.Loading.Initial -> showFullScreenLoader()
is UiState.Loading.Refreshing -> showRefreshIndicator(state.currentData)
is UiState.Content.Success -> showList(state.data)
is UiState.Content.Empty -> showEmptyState()
is UiState.Error.Network -> showNetworkError(state.message)
is UiState.Error.Server -> showServerError(state.code, state.message)
}
}
All Sealed Types in Quartz
Complete list of sealed types found in codebase:
Commons
- AccountState (class)
Quartz
- BaseZapSplitSetup (class)
- MuteTag (class)
- BookmarkIdTag (class)
- SignerResult (interface)
- VerificationState (class)
- CacheResults (class)
- SignerExceptions (class)
- RelayUrlNormalizer.Result (interface)
Total: 8 sealed types (7 classes, 1 interface)
Decision Tree
Need to represent variants of a concept?
YES → Use sealed type
NO → Regular class/interface
Variants have different data?
YES → sealed class or sealed interface
NO → enum (if simple constants)
Need generics with variance (out/in)?
YES → sealed interface
NO → sealed class (simpler)
Need common constructor/properties?
YES → sealed class
NO → sealed interface
Need multiple inheritance?
YES → sealed interface
NO → Either works
Representing state machine?
→ sealed class (state transitions)
Representing result/error types?
→ sealed interface (if generic, else class)
Representing tag/command variants?
→ sealed class (common structure)