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