Android アプリ開発で sealed class を使いたい!

id:nobuoka / @nobuoka
Kansai.kt #1

こんにちは! Nobuoka です

  • 株式会社はてな所属
  • ソフトウェア開発者
    • モバイルアプリ開発 (Android、UWP)
    • Web 開発 (Scala、Java、TypeScript、Perl)
  • 仕事は Android アプリ 「はてなブックマーク」 開発

Android アプリ開発で sealed class を使いたい!

  • Java で開発 (Not Kotlin)
  • 最近 「Kotlin の sealed class を使いたいなー」 って場面がちょこちょこ
  • Sealed class は便利! って話

Sealed class

sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type.

See: Classes and Inheritance - Kotlin Programming Language (Sealed Classes)

Sealed class の特徴

  • in a sense, an extension of enum classes
    • enum : 値はインスタンスでしかない
    • Sealed class : サブクラスを扱える
  • Sealed class の型の実際のクラスが制限される
    • 例) 前頁 Expr 型の変数には : Const 型 / Sum 型 / NotANumber
  • whenで効果を発揮
  • enum の values() メソッドみたいなのはない
    • 当然といえば当然

when 式での使用

val message = when (expr) {
  is Expr.Const -> "const : " + expr.number
  is Expr.Sum -> "sum : " + expr.e1 + " + " + expr.e2
  is Expr.NotANumber -> "not a number"
}
  • スマートキャスト
  • when を式として扱う場合に網羅できてないとコンパイルエラー
    • 文として扱われるときは異なる

どういうところで便利なのか?

階層構造を持った enum

  • enum っぽいものが欲しいけど型が違う場合
  • 例) 「はてブ」 のエントリ一覧画面の種類
    • 「総合」、カテゴリごと、「マイホットエントリー」 など
    • それぞれ持つべき情報が違う (カテゴリにはカテゴリ ID を持たせるとか)

宣言

sealed class EntryListScreenType {
  sealed class Category(val categoryId: String) : EntryListScreenType() {
    object It : Category("xxxx")
    object Fun : Category("yyyy")
    // ...
  }
  object MyHotEntry : EntryListScreenType()
  // ...
}

when 式での使用

fun createIntentFromEntryScreenType(t: EntryListScreenType) = when (t) {
  is EntryListScreenType.Category ->
    EntryListCategoryActivity.createIntent(this, t.categoryId)
  is EntryListScreenType.MyHotEntry ->
    EntryListMyHotEntryActivity.createIntent(this)
}

RxJava で非同期処理する際のエラーも型に含める

// 普通にやるなら起こりうる例外を検査例外にしたいところ
SuccessValue fooBar() throws Error1, Error2 { /* ... */ }

// RxJava を使う場合は例外を検査例外にできない
Observable<SucessValue /* ??? */> fooBar() { /* ... */ }

Either みたいなものを定義すると良さそう? (下の例がベストプラクティスだというつもりはないけど一つの例として)

宣言

// 汎用的なクラス。
sealed class Result<V, E> {
    class Success<V, E>(val value: V) : Result<V, E>()
    class Error<V, E>(val value: E) : Result<V, E>()
}

// あるメソッド (今回の例だと `fooBar` メソッド) が
// 返しうる例外一覧の sealed class。
sealed class FooBarException {
    class E1(val value: Error1) : FooBarException()
    class E2(val value: Error2) : FooBarException()
}

使用

val o: Observable<Result<SuccessValue, FooBarException>> = fooBar()
o.subscribe({ res ->
    // この `when` は文なので branch が網羅されていなくても
    // コンパイルエラーにならない。
    when (res) {
        is Result.Success -> procOnSuccess(res.value)
        is Result.Error -> {
            when (res.value) {
                is FooBarException.E1 -> procOnError1(res.value.value)
                is FooBarException.E2 -> procOnError2(res.value.value)
            }
        }
    }
})

悩み事

when が文でも網羅しているかチェックしてほしい

  • 式の場合は網羅チェック有り / 文の場合はない
  • 文と式はどうやって区別されるのか???
  • when 式の戻り値を使うかどうかっぽい
  • キャストすることで無理やり式として解釈させる
    • 助けてくれ……
when (res.value) {
    is FooBarException.E1 -> procOnError1(res.value.value)
    is FooBarException.E2 -> procOnError2(res.value.value)
} as Unit

おわり