Sealed Class — specjalny typ klasy w Kotlinie, który ogranicza hierarchię dziedziczenia do ustalonego zestawu podtypów. Wszyscy dziedziczący są deklarowani w tym samym pliku i są znane kompilatorowi, co pozwala na użycie wyczerpującego bloku when bez obowiązkowej gałęzi else. Według Kotlin Docs, 2026, zapieczętowane klasy — kluczowy mechanizm do reprezentacji ograniczonych hierarchii, takich jak stany, typy błędów i zdarzenia UI.
Najważniejsze
Sealed Class (zapieczętowana klasa) — to klasa w Kotlinie oznaczona modyfikatorem sealed. Definiuje ograniczoną hierarchię typów: wszyscy możliwi dziedziczący są wymienieni w tym samym pliku, a kompilator wie o każdym z nich. To odróżnia sealed class od zwykłej otwartej klasy, której dziedziczący mogą być zadeklarowani w dowolnym miejscu.
Głównym celem sealed class jest typbezpieczna reprezentacja skończonego zestawu wariantów. Każdy dziedziczący może mieć własną strukturę danych, co czyni sealed class bardziej elastycznym niż enum. W czasie wykonania sealed class to zwykła klasa abstrakcyjna, kompilator nakłada ograniczenia tylko na etapie kompilacji.
Sealed class jest szczególnie przydatny w architekturze Android aplikacji: stany UI, wyniki zapytań sieciowych, zdarzenia nawigacyjne typu Intent i, oczywiście, hierarchie błędów — to typowe scenariusze zastosowania.
Podczas kompilacji sealed class jest optymalizowany do tablicy przejść dla wyrażeń when, co czyni go wydajniejszym niż łańcuchy if-else. W połączeniu z data class każdy dziedziczący może zawierać nie tylko stan, ale także metody, co pozwala budować samodokumentujące się modele domenowe bez kodu boilerplate.
Sealed class jest również skuteczny do reprezentacji automatów stanowych (state machine) w aplikacjach mobilnych. Każdy stan — osobny dziedziczący z unikalnymi parametrami, a przejścia między stanami są kontrolowane przez wyrażenie when. Kompilator gwarantuje, że wszystkie możliwe stany zostały obsłużone, co eliminuje błędy wykonawcze przy zmianie stanu UI lub logiki biznesowej.
Początkujący programiści Kotlina często mylą sealed class z enum, ponieważ oba ograniczają zestaw wartości. Jednak istnieje między nimi zasadnicza różnica: enum — to zestaw stałych jednego typu, sealed class — hierarchia różnych typów.
Enum jest optymalny, gdy wszystkie warianty są stałymi bez dodatkowej struktury. Na przykład dni tygodnia, statusy zamówienia czy typy akcji bez parametrów. Każda wartość enum to singleton o ustalonej nazwie.
Sealed class jest potrzebny, gdy każdy wariant ma własne dane. Na przykład błąd sieci zawiera kod odpowiedzi, błąd parsowania — szczegóły, a błąd autoryzacji — komunikat. Każdy dziedziczący sealed class to osobny typ z unikalnymi polami.
// Enum — wszystkie warianty jednego typu
enum class Status { LOADING, SUCCESS, ERROR }
// Sealed class — każdy wariant z własnymi danymi
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
Od Kotlin 1.5 istnieje możliwość deklarowania sealed interface. Rozszerza to koncepcję sealed na interfejsy: sealed interface również ma ustalony zestaw implementacji, ale obsługuje wielodziedziczenie.
Sealed interface jest wygodny, gdy dziedziczący muszą implementować wiele kontraktów jednocześnie. Na przykład zdarzenie UI może być jednocześnie klikalne i śledzone. Przy sealed class trzeba by wybrać jedną klasę bazową, przy sealed interface dziedziczący implementuje oba.
Sealed class — to klasa, więc każdy dziedziczący może mieć tylko jednego rodzica. Sealed interface rozwiązuje ten problem, ale nie może zawierać stanu. Wybór między nimi zależy od zadania: potrzebna jest wspólna logika z polami — sealed class, potrzebna jest elastyczność kontraktów — sealed interface.
sealed interface ScreenEvent {
data class Refresh(val force: Boolean) : ScreenEvent
data class Navigate(val route: String) : ScreenEvent
data class ShowError(val toast: String) : ScreenEvent
}
sealed interface AnalyticsEvent {
val name: String
val params: Map<String, Any>
}
// Dziedziczący implementuje oba interfejsy
data class LoginClicked(
override val name: String = "login_click",
override val params: Map<String, Any> = emptyMap()
) : ScreenEvent, AnalyticsEvent
Jedno z głównych zastosowań sealed class w tworzeniu aplikacji mobilnych — typbezpieczna hierarchia błędów. Zamiast rzucać wyjątki różnych typów lub używać ogólnego Exception, sealed class zbiera wszystkie możliwe błędy dziedziny w jeden typ.
Utwórz sealed class DomainError i wypisz wszystkie rodzaje awarii jako dziedziczących. Każdy dziedziczący zawiera tylko te dane, które mają znaczenie dla danego typu błędu. Kompilator gwarantuje, że przy obsłudze błędu nie zapomnisz żadnego wariantu.
Rozważmy aplikację z autoryzacją, gdzie możliwe są różne scenariusze awarii: nieprawidłowe hasło, blokada konta, problem z serwerem. Sealed class łączy je w jeden typ z wyczerpującą obsługą.
sealed class AuthError {
data class InvalidCredentials(
val attempts: Int
) : AuthError()
data class AccountBlocked(
val until: Long
) : AuthError()
data class NetworkFailure(
val cause: Throwable
) : AuthError()
object ServerError : AuthError()
}
fun handleError(error: AuthError): String = when (error) {
is AuthError.InvalidCredentials ->
"Pozostało prób: ${3 - error.attempts}"
is AuthError.AccountBlocked ->
"Dostęp zablokowany do ${Date(error.until)}"
is AuthError.NetworkFailure ->
"Sprawdź połączenie: ${error.cause.localizedMessage}"
AuthError.ServerError ->
"Serwer tymczasowo niedostępny"
}
Sealed class stał się standardowym narzędziem w architekturze aplikacji Android. Rozważmy trzy kluczowe wzorce, w których sealed class jest niezastąpiony w tworzeniu aplikacji mobilnych.
Osobno warto wspomnieć o zastosowaniu sealed class w Clean Architecture. Każda warstwa (data, domain, presentation) używa sealed class dla swoich typów błędów, a mappery przekształcają jeden sealed class w inny. Na przykład DataError z warstwy danych jest mapowany na DomainError dla logiki biznesowej, a następnie na UiState dla warstwy prezentacji. Zachowuje to typbezpieczeństwo na wszystkich poziomach aplikacji i gwarantuje, że żaden błąd nie pozostanie nieobsłużony.
Testowanie sealed class wymaga szczególnego podejścia, ponieważ każdy dziedziczący to osobny typ z własnym stanem. Zaleca się pisanie testów parametryzowanych, które przechodzą przez wszystkich dziedziczących sealed class. Gwarantuje to, że wyrażenia when pokrywają wszystkie warianty, włącznie z nowymi dodanymi przy rozszerzaniu hierarchii.
Dla testów UI sealed class jako UiState pozwala sprawdzić wyświetlanie każdego stanu: Loading pokazuje spinner, Content — dane, Error — komunikat o błędzie. Ponieważ sealed class jest skończony, pokrycie testowe wszystkich stanów daje pełną pewność poprawności logiki UI.
Mimo prostoty koncepcji, programiści regularnie popełniają błędy przy projektowaniu hierarchii sealed class. Rozważmy główne problemy i sposoby ich unikania.
Często zadawane pytania
Tak, sealed class może zawierać metody abstrakcyjne, a każdy dziedziczący musi je zaimplementować. Jest to wygodne, gdy wszystkie warianty powinny udostępniać wspólny interfejs, ale z różną logiką wykonania.
W Java 17+ pojawiły się zapieczętowane klasy i interfejsy z modyfikatorem sealed. Android na razie obsługuje Java 17 częściowo, ale w projektach Kotlin sealed class jest dostępny od Kotlin 1.0 bez ograniczeń.
Tak, jeden sealed class może być dziedziczącym drugiego. Hierarchia sealed class pozostaje skończona: kompilator zna wszystkich dziedziczących na każdym poziomie. Pozwala to budować szczegółowe klasyfikacje błędów.
Sealed class nie tworzy narzutów w czasie wykonania. Kompilator optymalizuje wyrażenia when z sealed class w tablice przejść (tableswitch), co jest szybsze niż łańcuchy if-else. Wydajność jest identyczna z enum.
Każdy dziedziczący sealed class jest testowany osobno. Ponieważ sealed class jest skończony, można napisać test parametryzowany, który przechodzi przez wszystkie warianty. Daje to pełne pokrycie gałęzi bloków when.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również