たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 5581
どうもこんばんわ
11 月ってこんな暑かったっけ?
Jetpack Composeを使うときによく、以下のような画面の値を持っておくステートクラスみたいなのを作ると思うんですけどnowinandroid/feature/foryou/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/OnboardingUiState.kt at main · android/nowinandroid
A fully functional Android app built entirely with Kotlin and Jetpack Compose - android/nowinandroid
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>
}<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が一番近いかな?
このクラスの特徴は継承する際に制限があり(同じパッケージ内で継承して宣言する必要がある?)、その代わり継承しているクラスが分かるという特徴があります。
基本的には同じファイル内にすべて継承するクラスを定義するはず。
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
ようやく本題、はい。
全てのクラスのスーパークラス
全てのクラスはこのAnyを継承しているってことらしい。
toStringとかはいつ見てもあると思いますがこれは根っこの部分Anyで用意されているからなんですねえ
試しにさっき作ったクラスをインスタンス化しis Anyしてみますがもちろんtrueになります
これは逆に全てのクラスを継承したクラスです。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()ジェネリックなのかジェネリクスなのか、どっちなんだろう
そこらの言語と同じ奴。
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 ジェネリクス 共変とかで調べてみてください)
Generics: in, out, where | Kotlin
https://kotlinlang.org/docs/generics.html
(なので以下のコードは動きません)
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 interfaceLoading/Successful/Error の3つに制限whenやinでクラスを比較する際はこの3つを見ればおっけーobject Loadingobjectでいいらしいsealed interfaceを継承してねdata class Successful<T>data class ErrorNothingNothingNothingにすることで、アップキャストが働き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にいれることが出来ているわけです。
間違ってたらすいません
難しい
How to make sealed classes generic in kotlin?
Is it possible to use the AsyncResult class below to prevent redefining InFlight, Error and InFlight in UserDataAppResult and CreateUserResult? //TODO: use this to make the below classes generic? ...
https://stackoverflow.com/questions/44243763/how-to-make-sealed-classes-generic-in-kotlin
Kotlin 紹介 - Nothing 型の使い道 - PHONE APPLI Engineer blog
弊社プロダクト「PHONE APPLI PEOPLE (旧:連絡とれるくん)」では、サーバサイドに Kotlin を採用しています。 今回は、初見だとたぶん使い道が分かり辛い Nothing 型について紹介します。 Nothing 型とは Kotlin language specification 言語仕様によると、ユーザが定義した型も含む、すべての型のサブタイプ (i.e. ボトム型) です。 すべての型のスーパータイプである Any のちょうど反対にある型です。 「すべての型のサブタイプ」という訳の分からないものなので、インスタンスが存在しません。 (コード上で Nothing のインスタ…
https://phoneappli.hatenablog.com/entry/2021/03/30/180749
Kotlin - Void vs. Unit vs. Nothing
Kotlin has three types that are very similar in nature: Void Unit Nothing It almost seems like they're making the JavaScript mistake: null undefined void(0) Assuming that they haven't fallen into the
https://stackoverflow.com/questions/55953052/kotlin-void-vs-unit-vs-nothing