たくさんの自由帳
Androidのお話
投稿日 : | 0 日前
文字数(だいたい) : 11107
どうもこんばんわ
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クラスが共通化できちゃうわけ
ジェネリックなのかジェネリクスなのか、どっちなんだろう
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
NothingNothingにすることで、アップキャストが働きNothing以外の型を探そうとし、結果成功時の型が使われるようになる
Successful<T>の型になる!<out T>
Loading/Errorの型をどうすればいいの?
Nothing を使うとアップキャストが効いていい感じ<T>にoutをつけることで、親クラスを指定した際に子クラスも型パラメータに入れることを許容するように
Nothingという特殊な型により、自動的に成功時の型が使われるようになる・・・ってことで合ってる?
間違ってたらすいません
難しい
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