👻

Kotlin sealed classを使った例

2024/03/28に公開

📕Overview

https://kotlinlang.org/docs/sealed-classes.html

シールされたクラスとインターフェースは、クラス階層の制御された継承を提供します。シールされたクラスの直接のサブクラスはコンパイル時にすべてわかります。シールドされたクラスが定義されているモジュールやパッケージの外では、 他のサブクラスが出現することはありません。同じ理屈がシールドされたインターフェースとその実装にも当てはまります。一度シールドされたインターフェースを持つモジュールがコンパイルされると、新しい実装を作成することはできません。

そのまま翻訳すると分かりづらいので別の解説を作った

  • sealed classは、そのクラスの継承を制限することができるクラスです。
  • sealed classを継承できるサブクラスは、sealed classが定義されているモジュール/ファイル内に限定されます。
  • つまり、sealed classの外部からサブクラスを継承することはできません。
  • このようにサブクラスが限定されることで、sealed classのインスタンスがどのサブクラスのインスタンスであるかを網羅的に扱うことができます。
  • そのため、when式を使ってパターンマッチングをする際に、すべてのパターンを網羅的に扱う必要があります。
  • これにより、不完全なパターンマッチングのエラーを防ぐことができます。

sealed classは、代数的データ型のようにデータの種類を列挙し、それぞれのデータに応じた処理を関数内で記述できるようになります。状態マシンやエラーハンドリングなど、ある種類のデータに対してケースバイケースで処理を分ける必要がある場合に、sealed classは非常に役立ちます。

🧷summary

今回は、sealed classを使ってColorBoxを切り替えるロジックを作ってみました。

分岐処理に必要な定数のような役割を持っているsealed classを定義します。Enumみたいなもんですね。

// Boxの状態を表すsealed class
// Red, Green, Blueの3つの状態を持つ
sealed class BoxState {
    data object Red : BoxState()
    data object Green : BoxState()
    data object Blue : BoxState()
}

View側のComposable関数は、ColorBoxをTapするとStateが更新されて、色が変化するロジックになっております。他の言語だとwhenは、switchに似たようなものです。

// BoxStateを受け取り、それに応じて色を変えるBoxを描画する
@Composable
fun ToggleBox() {
    // Boxの状態を保持するmutableState
    // Genericsの型にはBoxStateを指定。初期値はBoxState.Red
    // rememberを使って再構築時に状態を保持する
    var boxState by remember { mutableStateOf<BoxState>(BoxState.Red) }
    // Columnを使って縦方向に配置
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.LightGray),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Color Boxを描画
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(
                    when (boxState) {
                        BoxState.Red -> Color.Red
                        BoxState.Green -> Color.Green
                        BoxState.Blue -> Color.Blue
                    }
                )
                .clickable {
                    boxState = when (boxState) {
                        BoxState.Red -> BoxState.Green
                        BoxState.Green -> BoxState.Blue
                        BoxState.Blue -> BoxState.Red
                    }
                }
        )
        // Tapを支持するTextを描画
        Text(text = "タップしてね", color = Color.White, fontSize = 20.sp)
    }
}

🤖全体のコード:

package com.example.sealedapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.sealedapp.ui.theme.SealedAppTheme

// Boxの状態を表すsealed class
// Red, Green, Blueの3つの状態を持つ
sealed class BoxState {
    data object Red : BoxState()
    data object Green : BoxState()
    data object Blue : BoxState()
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SealedAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ToggleBox()
                }
            }
        }
    }
}

// BoxStateを受け取り、それに応じて色を変えるBoxを描画する
@Composable
fun ToggleBox() {
    // Boxの状態を保持するmutableState
    // Genericsの型にはBoxStateを指定。初期値はBoxState.Red
    // rememberを使って再構築時に状態を保持する
    var boxState by remember { mutableStateOf<BoxState>(BoxState.Red) }
    // Columnを使って縦方向に配置
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.LightGray),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Color Boxを描画
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(
                    when (boxState) {
                        BoxState.Red -> Color.Red
                        BoxState.Green -> Color.Green
                        BoxState.Blue -> Color.Blue
                    }
                )
                .clickable {
                    boxState = when (boxState) {
                        BoxState.Red -> BoxState.Green
                        BoxState.Green -> BoxState.Blue
                        BoxState.Blue -> BoxState.Red
                    }
                }
        )
        // Tapを支持するTextを描画
        Text(text = "タップしてね", color = Color.White, fontSize = 20.sp)
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    SealedAppTheme {
        ToggleBox()
    }
}

ColorBoxをTapするとこんな感じで色が変わります!


ああでもなんか違うな...
公式は、interfaceがあったらから組みわせて継承したクラスに修正したコードにしました。こちらもTapすると先ほどと同じように、ColorBoxが切り替わります。

package com.example.sealedapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.sealedapp.ui.theme.SealedAppTheme

// Boxの状態を表すsealed class
// Red, Green, Blueの3つの状態を持つ
//sealed class BoxState {
//    data object Red : BoxState()
//    data object Green : BoxState()
//    data object Blue : BoxState()
//}
// こっちの方が公式ぽい!
sealed interface BoxState

sealed class ColorBox() : BoxState

data object Red : ColorBox()
data object Green : ColorBox()
data object Blue : ColorBox()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SealedAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ToggleBox()
                }
            }
        }
    }
}

// BoxStateを受け取り、それに応じて色を変えるBoxを描画する
@Composable
fun ToggleBox() {
    // Boxの状態を保持するmutableState
    // Genericsの型にはBoxStateを指定。初期値はBoxState.Red
    // rememberを使って再構築時に状態を保持する
    var boxState by remember { mutableStateOf<BoxState>(Red) }
    // Columnを使って縦方向に配置
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.LightGray),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Color Boxを描画
        Box(
            modifier = Modifier
                .size(150.dp)
                .background(
                    when (boxState) {
                        Red -> Color.Red
                        Green -> Color.Green
                        Blue -> Color.Blue
                    }
                )
                .clickable {
                    boxState = when (boxState) {
                        Red -> Green
                        Green -> Blue
                        Blue -> Red
                    }
                }
        )
        // Tapを支持するTextを描画
        Text(text = "BOXをタップしてね!", color = Color.White, fontSize = 20.sp)
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    SealedAppTheme {
        ToggleBox()
    }
}

🧑‍🎓thoughts

今回は、sealed classを使って色の箱を切り替えるロジックを作りました。使う例としては、分岐処理でObjectを切り替えたり、エラー処理をするとかかな。
whenと組み合わせて渡された値に応じて分岐処理をしています。

Discussion