Documentation
@when
Pattern match on sealed classes and enums with exhaustive checking.
Basic Usage
sealed class Status {
object Loading : Status()
data class Success(val data: String) : Status()
data class Error(val msg: String) : Status()
}
var status: Status = Status.Loading
@when (status) {
is Loading -> {
<CircularProgressIndicator />
}
is Success -> {
<Text>{status.data}</Text>
}
is Error -> {
<Text color="#ef4444">{status.msg}</Text>
}
} Braces are required around each branch body, even for single expressions.
Network State Example
sealed class ApiResult<T> {
object Idle : ApiResult<T>()
object Loading : ApiResult<T>()
data class Success<T>(val data: T) : ApiResult<T>()
data class Error<T>(val message: String) : ApiResult<T>()
}
var result: ApiResult<List<Show>> = ApiResult.Idle
@when (result) {
is Idle -> {
<Button onClick={loadData} text="Load Data" />
}
is Loading -> {
<Column align="center">
<CircularProgressIndicator />
<Text>Loading...</Text>
</Column>
}
is Success -> {
<LazyColumn>
@for (show in result.data) {
<ShowCard show={show} />
}
</LazyColumn>
}
is Error -> {
<Column align="center">
<Text color="#ef4444">Error: {result.message}</Text>
<Button onClick={loadData} text="Retry" />
</Column>
}
} Enum Matching
enum class PaymentMethod {
CREDIT_CARD, PAYPAL, CRYPTO, BANK_TRANSFER
}
var method: PaymentMethod = PaymentMethod.CREDIT_CARD
@when (method) {
PaymentMethod.CREDIT_CARD -> {
<CreditCardForm />
}
PaymentMethod.PAYPAL -> {
<PayPalButton />
}
PaymentMethod.CRYPTO -> {
<CryptoWalletSelector />
}
PaymentMethod.BANK_TRANSFER -> {
<BankTransferForm />
}
} Nested Data
sealed class LoginState {
object NotStarted : LoginState()
object CheckingCredentials : LoginState()
object FetchingProfile : LoginState()
data class Complete(val user: User) : LoginState()
data class Failed(val reason: String, val canRetry: Boolean) : LoginState()
}
@when (loginState) {
is NotStarted -> {
<LoginForm onSubmit={handleLogin} />
}
is CheckingCredentials -> {
<Text>Verifying credentials...</Text>
}
is FetchingProfile -> {
<Text>Loading profile...</Text>
}
is Complete -> {
<Text>Welcome, {loginState.user.name}!</Text>
}
is Failed -> {
<Column>
<Text color="#ef4444">{loginState.reason}</Text>
@if (loginState.canRetry) {
<Button onClick={retry} text="Try Again" />
}
</Column>
}
} Multiple Properties
sealed class AsyncData<T> {
data class Loading<T>(val progress: Int) : AsyncData<T>()
data class Success<T>(val data: T, val cached: Boolean) : AsyncData<T>()
data class Error<T>(val message: String, val code: Int) : AsyncData<T>()
}
@when (asyncData) {
is Loading -> {
<Column>
<CircularProgressIndicator />
<Text>{asyncData.progress}%</Text>
</Column>
}
is Success -> {
<Column>
<DataView data={asyncData.data} />
@if (asyncData.cached) {
<Text fontSize={12} color="#999">Cached</Text>
}
</Column>
}
is Error -> {
<Text color="#ef4444">
Error {asyncData.code}: {asyncData.message}
</Text>
}
} With Smart Casts
Kotlin smart casts work inside branches:
sealed class Result {
data class Success(val value: Int) : Result()
data class Error(val error: String) : Result()
}
@when (result) {
is Success -> {
// result is smart cast to Success here
<Text>Value: {result.value * 2}</Text>
}
is Error -> {
// result is smart cast to Error here
<Text>Error: {result.error.uppercase()}</Text>
}
} Exhaustive Checking
Compiler ensures all cases are handled:
sealed class Mode {
object Light : Mode()
object Dark : Mode()
object Auto : Mode()
}
// ✅ All cases handled
@when (mode) {
is Light -> { <LightTheme /> }
is Dark -> { <DarkTheme /> }
is Auto -> { <SystemTheme /> }
}
// ❌ Compiler error - missing Auto case
@when (mode) {
is Light -> { <LightTheme /> }
is Dark -> { <DarkTheme /> }
} Behind the Scenes
Whitehall transpiles @when to Compose when expressions:
@when (status) {
is Success -> { <Text>{status.data}</Text> }
is Error -> { <Text>{status.msg}</Text> }
}
// Becomes:
when (status) {
is Success -> Text(text = status.data)
is Error -> Text(text = status.msg)
} Use sealed classes with @when for type-safe state machines. The compiler will ensure you handle all possible states.
Complete Example
sealed class UploadState {
object Idle : UploadState()
data class Uploading(val progress: Int, val filename: String) : UploadState()
data class Success(val url: String) : UploadState()
data class Failed(val error: String) : UploadState()
}
var uploadState: UploadState = UploadState.Idle
suspend fun uploadFile(file: File) {
uploadState = UploadState.Uploading(0, file.name)
try {
for (progress in 0..100 step 10) {
uploadState = UploadState.Uploading(progress, file.name)
delay(100)
}
val url = "https://cdn.example.com/${file.name}"
uploadState = UploadState.Success(url)
} catch (e: Exception) {
uploadState = UploadState.Failed(e.message ?: "Unknown error")
}
}
<Column p={16} gap={16}>
@when (uploadState) {
is Idle -> {
<Button onClick={() => selectAndUpload()} text="Choose File" />
}
is Uploading -> {
<Column class="gap-8">
<Text>Uploading: {uploadState.filename}</Text>
<LinearProgressIndicator progress={uploadState.progress / 100f} />
<Text>{uploadState.progress}%</Text>
</Column>
}
is Success -> {
<Column class="gap-8">
<Text color="#10b981">Upload complete!</Text>
<Text fontSize={12} color="#999">{uploadState.url}</Text>
<Button onClick={() => uploadState = UploadState.Idle} text="Upload Another" />
</Column>
}
is Failed -> {
<Column class="gap-8">
<Text color="#ef4444">Upload failed: {uploadState.error}</Text>
<Button onClick={() => uploadState = UploadState.Idle} text="Try Again" />
</Column>
}
}
</Column> See Also
- @if / @else - Simple conditionals
- @for - List rendering