飴屋

Kotlin/状態を持つ

Composableなレイアウトを書き始めて、いくつかの部品をつなげて、画面の切り替えぐらいはできるようになりました。切り替わった先で、表示するものを変えれば何かしらのビューワー的なアプリは作れそうです。ただ、表示したいものが常に固定されてしまうので、フレキシブルに展示物を変えられないですね。個展だって会期中に展示物が入れ替わったり、作品の販売状況みたいな刻々と変わる情報を脇に表示したくなるかもしれません。しかし、今のところ、表示内容を変更するにはアプリのコードを変更してビルドしなおす必要があるんですん。静的にしかコンテンツが決められないので、頻繁に内容を変えられず、お客は飽きて寄り付かなるわけです。やはり、プログラムのソースに表示内容を依存させず、プログラムとデータソースはある程度分離させておけると嬉しいものです。ただ、データを読み込むように改修を入れると、

  • データがまだ読み込まれていない
  • データが読み込み中にいろんな理由でエラーが出て表示できない
  • データが読み込めたので表示できる

といった状態(State)を持つようになるわけです。こういうとき、Jetpack Composeはどうするんでしょう?

状態は思い出される

Text("A: これ、ええかな?")
Text("B: ええで!")
Text("B: あかん!")
Text("B: しらんけど")

Aさんの質問に対して、Bさんが何らかの応答をする画面を作りたいのですが、静的にBさんの反応を並べただけでは、応答が3つ並ぶだけです。

Text("A: これ、ええかな?")
when (isGranted) {
  true-> {
    Text("B: ええで!")
  }
  false-> {
    Text("B: あかん!")
  }
  null -> {
    Text("B: しらんけど")
  }
}

変数を使って表示を分けたいわけです。ここでがisGranted変数がBさんの態度を決めているわけです。ここでBさんの変化しうる態度が状態を持つっていうことなんでしょうね。でも、画面レイアウトがスクリーンに描画される時、その時点でのisGrantedが「あかん!」と評価されるとしても、あとでBさんが態度を「やっぱええかも」と軟化させても、そのことに画面は気づいてくれません。Bさんはずっとふてくされた顔で「あかん」って言い続ける感じの悪い子になってしまいそうです。

var isGranted by remember { mutableStateOf<Boolean?>(null) }

そこでisGranted をremember しておくのでした。「思い出せ、あの状態を」「isGranted を覚えてろよ」と指定しておくと、isGranted に変更が加わったことを検知して、画面レイアウトを更新してくれるのでした。mutableStateOfは、二値かnullの範囲で状態が書き換わり得ることを示していますね。初期値 nullも合わせて指定できます。なんかVue.jsとか思い出しました。javascriptの変数を拡張して、値を変更したら画面に適切なタイミングで更新を反映してくれるところが似てますね。

var todayStepCount by remember { mutableStateOf(0L) }

整数値をrememberすれば歩数計の画面なんかも作れそうです。

LaunchedEffect

じゃあ、rememberした変数はどのタイミングで書き換えればいいんですかね?その場で変数の内容を変更できるのであれば、そもそもrememberする必要はなく、静的に画面は決まるのでした。しかし、世の中にはsuspendedな関数だとか、コルーチンだとか、返り値がその場で定まらないときがあるのでした。(ちなみにコルーチン超便利、という噂だけは聞いていますが、まだ真価をわかってない私です。)そういうわけのわからない状態を定められないことをComposableな関数は嫌うので、好き勝手に書けないのだそうです。静的に決めたがるComposableちゃん。

そこでそういうわけのわからないものを詰め込めるのがLaunchedEffectです。名前から「作用するぜ!」という意思がビシビシ伝わってきます。

var isGranted by remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(Unit) {
  isGranted = (何かしらのその場で決まらない処理)
}

LaunchedEffectの中なら、Composable関数の中でもsuspendedな関数を呼び出せました。実際に呼び出されたらrememberな関数に値を入れてやるとComposable関数はそれを察知して画面を書き換えてくれるのです。

作用を嫌うComposable関数ですがLaunchedEffectのように作用を認めるAPIが他にもあるようです。

  • LaunchedEffect: コンポーザブルのスコープ内で suspend 関数を実行する
  • rememberCoroutineScope: コンポーザブルの外部でコルーチンを起動するために Composition 対応スコープを取得します
  • rememberUpdatedState: 値が変化しても再起動すべきでない作用の値を参照する
  • DisposableEffect: クリーンアップが必要な作用
  • produceState: Compose 外の状態を Compose の状態に変換する
  • derivedStateOf: 1 つ以上の状態オブジェクトを別の状態に変換する
  • snapshotFlow: Compose の State を Flow に変換する

コピペしてみましたが、内容は必要になってからおいおい調べましょう。

別のActivityを呼んで結果を得たい

何かを取得するActivityってありますよね。昔、たくさんActivityを作ってた頃はそれこそActivityの返す結果を元に画面を書き換えたり制御に反映させたりしたものでした。今でも許諾(Permission)を得るのにActivityを呼び出す部分があったので、registerForActivityResultで結果を受け取ろうとしたのですが、これはsuspendedな関数の中で呼ばないとならないっぽくて、Comosableな関数にはそのまま書くわけにはいきませんでした。

val launcher = rememberLauncherForActivityResult(
  contract = requestPermissionActivityContract
) { granted ->
  if (granted.containsAll(permissions)) {
    isGranted = true
  }
}
LaunchedEffect(Unit) {
  launcher.launch(permissions)
}

でもrememberLauncherForActivityResultなんて代替物が用意されていました。(代替物の探し方が知りたい)ここでもrememberの文字が!remember、effect、composableの三者が揃って画面が更新できるって感じでしょうか?

一覧へ