たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 5581
どうもこんばんわ
11 月ってこんな暑かったっけ?
Jetpack Compose
を使うときによく、以下のような画面の値を持っておくステートクラスみたいなのを作ると思うんですけど
多分、参考:https://github.com/android/nowinandroid/blob/main/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt
// HomeScreen の ステート
sealed interface HomeScreenUiState {
// ロード
object Loading : HomeScreenUiState
// 成功
data class Successful(val data: String) : HomeScreenUiState
// 失敗
data class Error(val throwable: Throwable) : HomeScreenUiState
}
private suspend fun getData(): String {
delay(500)
return "Hello World"
}
@Composable
fun HomeScreen() {
val uiState = remember { mutableStateOf<HomeScreenUiState>(HomeScreenUiState.Loading) }
LaunchedEffect(key1 = Unit) {
// 取得処理
val response = getData()
uiState.value = HomeScreenUiState.Successful(response)
}
Box {
when (val state = uiState.value) {
is HomeScreenUiState.Error -> Text(text = "失敗した!")
HomeScreenUiState.Loading -> Text(text = "ロードなう")
is HomeScreenUiState.Successful -> Text(text = "成功 = ${state.data}")
}
}
}
一画面だけなら問題ないと思いますが、似たような画面で使いたい場合に、HomeScreenUiState
の名前変えただけバージョンがコピーされてしまうのは避けたいなと思ったわけで、
解決するにはジェネリクス<T>
みたいなのを使えばいいですね!!
というわけでこれ。
sealed interface BaseUiState<out T> {
object Loading : BaseUiState<Nothing>
data class Successful<T>(val data: T) : BaseUiState<T>
data class Error(val throwable: Throwable) : BaseUiState<Nothing>
}
参考:https://stackoverflow.com/questions/44243763/how-to-make-sealed-classes-generic-in-kotlin
<out T>
← out
って何・・・
あと<Nothing>
って何なの?
確かに動くのですが、謎ばっかりだったので調べてみた
sealed interface BaseUiState<out T> {
object Loading : BaseUiState<Nothing>
data class Successful<T>(val data: T) : BaseUiState<T>
data class Error(val throwable: Throwable) : BaseUiState<Nothing>
}
private suspend fun getData(): String {
delay(1000)
return "Hello World"
}
@Composable
fun HomeScreen() {
val uiState = remember { mutableStateOf<BaseUiState<String>>(BaseUiState.Loading) }
LaunchedEffect(key1 = Unit) {
// 取得処理
val response = getData()
uiState.value = BaseUiState.Successful(response)
}
Box {
when (val state = uiState.value) {
is BaseUiState.Error -> Text(text = "失敗した!")
BaseUiState.Loading -> Text(text = "ロードなう")
is BaseUiState.Successful -> Text(text = "成功 = ${state.data}")
}
}
}
シールクラスって言うとかなんとか
TypeScript
だとUnion
が一番近いかな?
このクラスの特徴は継承する際に制限があり(同じパッケージ内で継承して宣言する必要がある?)、その代わり継承しているクラスが分かるという特徴があります。
https://kotlinlang.org/docs/sealed-classes.html#location-of-direct-subclasses
基本的には同じファイル内にすべて継承するクラスを定義するはず。
package io.github.takusan23.uistategenerics
sealed class Setting()
class WifiSetting : Setting()
class BluetoothSetting : Setting()
class DisplaySetting : Setting()
例えばパッケージが違うとエラー
package io.github.takusan23.uistategenerics.test
import io.github.takusan23.uistategenerics.Setting
class DeveloperSetting : Setting() // エラー
// Inheritor of sealed class or interface declared in package io.github.takusan23.uistategenerics.test but it must be in package io.github.takusan23.uistategenerics where base class is declared
制限がある代わりに、継承しているクラスが全て分かるため、when
等でinstanceof
する際に継承しているクラスが既にわかっているため、全て網羅すればelse
を追加する必要がないという点です。
これはenum
とかでも言えることですね
sealed class Setting()
class WifiSetting : Setting()
class BluetoothSetting : Setting()
class DisplaySetting : Setting()
fun example(setting: Setting) {
// Setting クラスは以下のクラス以外は継承していないので、else を書く必要がない
when (setting) {
is BluetoothSetting -> {
}
is DisplaySetting -> {
}
is WifiSetting -> {
}
}
}
例えばUI
になにかアラートか何かを出すため、データを入れておくクラスをつくろうと思います。
アラートはこの三種類、どれも Android 端末を触ったことがあれば出会ったことがあると思う。
汎用的にこんな感じかな
class NotifyData(
val type: Type,
// メッセージ
val message: String
) {
// Toast / Snackbar / ダイアログ のどれか
enum class Type {
TOAST,
SNACKBAR,
DIALOG
}
}
あ!Snackbar
とDialog
はボタンがあるので、ボタンのテキストも設定できるようにしたいですね。
うーん雲行きが怪しく
class NotifyData(
val type: Type,
val message: String,
// TOAST 以外
val actionText: String
) {
enum class Type {
TOAST,
SNACKBAR,
DIALOG
}
}
あとダイアログは外側を押したらキャンセル出来るかのフラグも欲しいかも
class NotifyData(
val type: Type,
val message: String,
// TOAST 以外
val actionText: String,
// DIALOG のみ
val isCancellable: Boolean
) {
enum class Type {
TOAST,
SNACKBAR,
DIALOG
}
}
きつくなってきた。
アラートの種類はenum
で表現できるのに、種類ごとに必要なデータが違うから使えない・・・!でも継承可だとなんか違う!
ってときに使うといいと思います。
sealed interface Notify
data class Toast(val message: String) : Notify
data class Snackbar(
val message: String,
val actionText: String? = null
) : Notify
data class Dialog(
val message: String,
val positiveText: String,
val isCancellable: Boolean
) : Notify
fun notifyToUi(notify: Notify) {
when (notify) {
is Toast -> {
// Toast を出す処理
}
is Snackbar -> {
// Snackbar を出す処理
}
is Dialog -> {
// Dialog を出す処理
}
}
}
これをうまく使ったのが、冒頭のUiState
ですね
when
の使いやすさと相まっていいと思う
sealed interface HomeScreenUiState {
object Loading : HomeScreenUiState
data class Successful(val data: String) : HomeScreenUiState
data class Error(val throwable: Throwable) : HomeScreenUiState
}
以上!sealed class
ようやく本題、はい。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/
全てのクラスのスーパークラス
全てのクラスはこのAny
を継承しているってことらしい。
toString
とかはいつ見てもあると思いますがこれは根っこの部分Any
で用意されているからなんですねえ
試しにさっき作ったクラスをインスタンス化しis Any
してみますがもちろんtrue
になります
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing.html
これは逆に全てのクラスを継承したクラスです。
Any
が根っこならNothing
はてっぺんです←?
全てのクラスを継承しているとかいう意味不明なクラスなので、インスタンス化することができません。
存在しない値を表現する際に使うらしい。例えばthrow
した場合Nothing
が返される。
もしNothing
を返す関数があればそれは例外を投げるか、無限ループになって呼び出し元に戻らない?になるらしいです。
val error: Nothing = throw RuntimeException("") // throw したら続行できないので、error に値が入ることはない...
イマイチ使い方が思いつかないですが、もう一個、多分こっちが本題!、
インスタンス化して使うことはできませんが、全てのクラスを継承した型として使うことができます。
全てのクラスを継承しているクラス(インスタンス化できないので建前でしか無いですが)はアップキャストと組み合わせることで効果を発揮します。
アップキャストはまぁ調べれば出ると思いますが、子クラスを親クラスにキャストするやつです。あの安全な方のキャスト
Android
だとImageView / TextView (子)
からView (子が継承している親のクラス)
にキャストするみたいな感じです。
話を戻して、アップキャストと組み合わせることでこんな事ができます。いや難しいなこれ
// むずかしい...
// 基本的にはアップキャストされるので、View と ImageView だと親クラスの View の配列になる
val list1: List<View> = listOf<ImageView>() + listOf<View>()
// Nothing 型は全てのクラスの子なので、アップキャストして安全を取ったとしても String になる
// Nothing 型のインスタンスなんて無いので、実質 String です
val list1: List<String> = listOf<Nothing>() + listOf<String>()
// Any 型は全てのクラスの親なので、アップキャストすると安全を取って Any 型になる
val list2: List<Any> = listOf<Any>() + listOf<String>()
てかこれ考えたん賢くない???頭めっちゃいいと思う
で、これを次の<out T>
と組み合わせることで、Jetpack Compose
のUiState
クラスが共通化できちゃうわけ
たとえばエルビス演算子(?:
)は、null 以外
なら左側の値、null
なら右側の値を返すという便利な演算子がありますが、
これthrow
を右側に置くことも出来ます。これはnull
の場合は例外を投げる。
val responseOrNull: String? = "hello world"
val responseOrThrow: String = responseOrNull ?: throw RuntimeException("null です!")
一見すると、Kotlin
のコンパイラが?:
の右側にthrow
が来た場合はNonNull
として返すという処理が特別に入っているのかと思ってしまいますが、
そうではなく、throw
はNothing
を返すため、先述の通りString
が選ばれるというわけです。
これはTODO()
とかいう初見殺し関数でも使われている技で、TODO()
を入れると、とりあえずコンパイルが通るようになるのは、
TODO()
はまだ未実装だよって例外を投げる。するとNothing
を返すため、すべてを継承しているNothing
で型が解決できているという話なだけです。
// これでコンパイルが通ってしまうのは
// TODO() は例外を投げる。
// Kotlin は例外を投げると Nothing を返す。
// Nothing 型はすべてのクラスを継承しているためとりあえずエラーが消える。
val text: String = TODO()
ジェネリックなのかジェネリクスなのか、どっちなんだろう
https://kotlinlang.org/docs/generics.html
そこらの言語と同じ奴。
val list: List<String>
val list2: List<Int>
ただ、アップキャストの話をちょっと↑でしましたが、ジェネリクスだとうまく行かないことがあるんですね。(てなことをドキュメントに書いてる)
たとえばこんなコード
data class CommonData<T>(val data: T)
fun main() {
var commonData1: CommonData<Any> = CommonData(Any())
var commonData2: CommonData<String> = CommonData("Hello World")
// CommonData<Any> に CommonData<String> を入れようとするとエラー
commonData1 = commonData2
}
String
はAny
を継承しているので、アップキャストにより入れられるはず・・・が入れようとするとエラーになる。継承してるのになぜ?
これは訳あってそうしているらしく、以下のようにダウンキャストの可能性が捨てきれないんですね。(詳しくは Java ジェネリクス 共変
とかで調べてみてください)
参考: https://kotlinlang.org/docs/generics.html#variance
(なので以下のコードは動きません)
data class CommonData<T>(var data: T) // 意図的に var にしました
fun main() {
// String の箱
var commonData1: CommonData<String> = CommonData("Hello World")
// Any の箱、String は Any を継承しているので入るはずだが、エラーになる
// もし仮にエラーにならないとする
var commonData2: CommonData<Any> = commonData1
// Any の箱に Any を入れる
commonData2.data = Any()
// さて、これは?
// Any を String にしようとしている...!
val string: String = commonData1.data
}
話を戻して、で、これを解決する方法があります。
Java
でも出来るらしいですが、Kotlin
だとout
を利用することでこの問題を解決できます。
data class CommonData<out T>(val data: T) // <out T> にする
fun main() {
var commonData1: CommonData<Any> = CommonData(Any())
var commonData2: CommonData<String> = CommonData("Hello World")
// CommonData<Any> に CommonData<String> が入るようになった
commonData1 = commonData2
}
これだとなんで通るようになるのかですが、<out T>
だとT
は返り値としてしか使うことができず、setter
やvar
等の値を変更する箇所でT
を使おうとすると怒られるわけです。
上記の問題はsetter
を使われてしまった場合に値が変わってしまう可能性があるために出来ないようにしていた、、、
が、変化しないと<out T>
で宣言することでこの問題が解決!ついでに変化しないので継承している子クラスも入れられるようになりました。
data class CommonData<out T>(val data: T)
fun main() {
// String の箱
var commonData1: CommonData<String> = CommonData("Hello World")
// out T なら出来る
var commonData2: CommonData<Any> = commonData1
// out T なので getter / val しか作ることが出来ず、結果的に後から値を変えることはできなくなるため、安全になる
commonData2.data = Any() // エラー!
// 書き換わらないので安全!
val string: String = commonData1.data
}
こういうステートが作れます。 一体それぞれが何の役割をしているか、わかった気がしませんか?
sealed interface BaseUiState<out T> {
object Loading : BaseUiState<Nothing>
data class Successful<T>(val data: T) : BaseUiState<T>
data class Error(val throwable: Throwable) : BaseUiState<Nothing>
}
sealed interface
Loading
/Successful
/Error
の3つに制限when
やin
でクラスを比較する際はこの3つを見ればおっけーobject Loading
object
でいいらしいsealed interface
を継承してねdata class Successful<T>
data class Error
Nothing
Nothing
Nothing
にすることで、アップキャストが働きNothing
以外の型を探そうとし、結果成功時の型が使われるようになる
Successful<T>
の型になる!<out T>
Loading
/Error
の型をどうすればいいの?
Nothing
を使うとアップキャストが効いていい感じ<T>
にout
をつけることで、親クラスを指定した際に子クラスも型パラメータに入れることを許容するように
Nothing
という特殊な型により、自動的に成功時の型が使われるようになる・・・ってことで合ってる?
emptyList()
という空の配列を返す関数があります。
こんな感じに、List
自体がnullable
の場合にエルビス演算子と合わせて使うとNonNull
になるので便利。
val responseListOrNull: List<String>? = listOf("Hello World")
val responseListOrEmpty = responseListOrNull ?: emptyList()
これは一見emptyList()
が中でnew List<T>
しているのかな?と思いきやここでもNothing
が活用されています。
EmptyList
と呼ばれるList<Nothing>
型の配列を返しています。
この関数はジェネリクスで<T>
を取ってはいますが、実際には使っておらず、シングルトンで作られた、中身のない配列、List<Nothing>
を返しています。
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
// 興味があれば Kotlin のコード見てきてください
}
public fun <T> emptyList(): List<T> = EmptyList
また、Kotlin
のList
はout T
(今見たらE
だった)といった感じでout
が付いています。
これにより、List
のジェネリクスの型<T>
にT
とT
を継承した型を入れられるようになっています。そしてNothing
は先述の通りすべての子なのでこのT
にいれることが出来ているわけです。
間違ってたらすいません
難しい
https://stackoverflow.com/questions/44243763/how-to-make-sealed-classes-generic-in-kotlin https://phoneappli.hatenablog.com/entry/2021/03/30/180749 https://stackoverflow.com/questions/55953052/kotlin-void-vs-unit-vs-nothing