【Android】sealed classについて

sealed classを理解する

sealed classとは

普通のclassだと無限に拡張される可能性がある型となりますが、sealed classは取りうる形をここで全部決め切る型のためSwiftでいうenumのようなものとなります。

sealed classの定義

sealed classは継承が利用されることを前提として作成されます。

sealed class内ではobjectとしてLoadStateを継承したものを定義します。

Kotlin
sealed class LoadState {
  // シングルトンとしてIdleオブジェクトを定義(LoadStateを継承している) 
    object Idle : LoadState()
    object Loading : LoadState()
    
    data class Success(val data: Data) : LoadState()
    data class Error(val error: Throwable) : LoadState()
}

継承に括弧が必要な理由

上記のLoadStateになぜ括弧が必要かと言うとsealed class LoadStateと定義しても実際にはsealed class LoadState()として定義されます。Kotlinの言語仕様として親クラスにコンストラクタがある場合に必ず呼ばなければならないためです。

objectで定義する

Kotlinにおけるobjectはインスタンスは1つで状態を保持しないSwiftでいうenumそのものだからです。

Swift
// これと同じ
LoadState.Idle

data classで定義する

値を保持する場合はdata classを利用します。

なぜ普通のclassではなくdata classを利用するのか、普通のクラスではれば同じ内容でも等価性で判断するため別物として扱われます。しかしdata classの場合は中身が同じなら同じ(value object)として扱われます。

data classはequals()、hashcode()、toString()、copy()メソッドを利用することができるため、 状態と非常に相性がいいという性質があります。

Composeでは「前回と今回の値が等しいか」で再コンポーズするかを判断することになるのでここがclassになっていると別物として扱われるので同じ状態でも変更時には再コンポーズされてしまいます。

こうすることで下記のSwiftのように合計4つの状態を表すことが可能となります。

Swift
enum LoadState {
    case idle
    case loading
    case success(Data)
    case error(Error)
}

objectはdata classは相互に参照できる?

    LoadState
    /       \
 Idle     Success

技術的に可能ですが横方向の参照関係はないく、親を通じて同じ名前空間にいるだけです。

同じLoadStateとして扱うにはIdleもSuccessもLoadStateを継承する必要がありますのでseal classの中で隠蔽しているという形ですね。

使い方

Kotlin
var state: LoadState = .idle
state = .success(data)
state = .idle

上記では値を保持する必要がない初期値では.idleとなっていて、値を保持する場合は.success(data)を利用しています。

Composeで利用する場合

Kotlin
@Composable
fun Screen(viewModel: SampleViewModel) {
    val state = viewModel.uiState

    when (state) {
        UiState.Idle -> IdleView()
        UiState.Loading -> LoadingView()
        is UiState.Success -> ContentView(state.data)
        is UiState.Error -> ErrorView(state.message)
    }
}

class SampleViewModel : ViewModel() {

    private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
    val uiState: StateFlow<UiState> = _uiState

    fun load() {
        _uiState.value = UiState.Loading
    }
}

これの何がいいのかというと、isによってスマートキャストされると自動的にその型に変換され、Successであればdataを利用することが可能になるという点です。

Composeとも相性がいいという事がわかりました!