ComposeのSnapshot
Composeでは Snapshot
という状態を管理する仕組みがあります。この Snapshot
はCompose UIとは独立してるので、 Snapshot
単体で使用することができます。この Snapshot
について色々試してみたので、簡単に紹介します。
Compose UIでSnapshotがどのように使用されてるかは、なんとなくイメージはできるのですが、そこまで深く調べていないので、今回は特に触れていません。
参考
先にぼくが参考にした記事を載せておきます。
基本的にこの記事が元になっています。
また、この記事はシリーズになっていて、全部興味深い内容になっているので、ぜひ読んでみてください。
Snapshotとは
ゲームなどであるセーブポイントみたいなもので、ある時点の状態を保存することができます。また、変更の監視もできるようになっています。
Snapshot
ではComposeでよく見かけると思いますが、 State<T>
で状態を管理します。 mutableStateOf
で作成するやつですね。
準備
androidx.compose.runtime:runtime
だけがあれば Snapshot
は使用することができますので、 build.gradle
に追加します。
implementation "androidx.compose.runtime:runtime:1.1.1"
あとは普通に使用できます。手っ取り早く試すにはテストコードで試すと早いです。
Snapshotを取って状態を復元する
状態を保存して、復元する簡単な例です。状態を保存することを take a snaphot という表現にあわせて、Snapshotを取ると表現しています。
Snapshotの対象になるのは State<T>
なので、まずは mutableStateOf
で作成しています。(3行目)
実際にSnapshotを取るには、 Snapshot.takeSnapshot
を使います。そして、その時点のSnapshot状態を取得します。(6行目)
Snapshotを取ったので、値を変更します。(9行目)
その後、 Snapshot.enter
でSnapshotを取った状態に復元しています。このブロックの中ではSnapshotを取った状態に戻っています。(13行目)
なので、ブロック内で状態を確認すると9行目の変更する前の状態になっています。(15行目)
enter
のブロックを抜けると、最新の状態に戻るので、9行目の変更が反映された状態になっています。(18行目)
最後に不要になったSnapshotを破棄しています。(21行目)
処理の流れとしては以上のようになっています。
Snapshot内で状態を変更する
先程はSnapshotの状態を確認しただけでしたが、今度はSnapshotの中で状態を変更してみます。
先程は Snapshot.takeSnapshot
を使用しましたが、これはread-onlyのため enter
内で状態を変更することができません。
代わりに、MutableなSnapshotを取る Snapshot.takeMutableSnapshot
を使います。
これで良いように見えますが、 enter
ブロックを抜けると変更された状態が反映されていません。
Snapshot内での変更を反映するには、 MutableSnapshot.apply
を実行する必要があります。
Snapshotを取って変更の一連の処理は Snapshot.withMutableSnapshot
で以下のように簡単に書くこともできます。
Snapshotの状態を監視する
Snapshot内での状態の読み取りや、書き込みを監視することも可能になっています。
少し長いですが、以下のような感じになります。横のコメントの数字は実行順です。
Snapshot.takeMutableSnapshot
実行時に引数で読み込みと書き込みのObserverを渡すことができるので、それを使って状態を監視することができます。
Global Snapshot
これまで説明したSnapshotとは異なり、Rootにある特別なGlobalなSnapshotがあります。
Global Snapshotは状態を変更したあともapply等はする必要はなく、そのまま使用できますが、Observerに通知したい場合などでは、Snapshot.notifyObjectsInitialized
か Snapshot.sendApplyNotifications
を使用する必要があります。
notifyObjectsInitialized
と sendApplyNotifications
の違いは状態に変更があるときのみに advanceGlobalSnapshot
を実行するかどうかが違います。 notifyObjectsInitialized
は変更がなくても実行します。
advanceGlobalSnapshot
は実行することによって現在の状態を適用してObserverに通知し、そこから新しくまたSnapshotを開始するような感じです。(あんまりちゃんと理解してないですが…)
なんかよく分からないので、コードで見てもらったほうが早いかなと思います。
Snapshot.registerApplyObserver
で sendApplyNotifications
されたときに変更された状態を通知で受け取れるようになります。
このとき複数回変更されたとしても最新の状態だけが通知されることになります。
Global Snapshotの書き込みを監視する
先程の例では明示的に sendApplyNotifications
等を実行しないと通知が来ませんでしたが、すべての書き込みの通知を受けることも可能です。
Snapshot.registerGlobalWriteObserver
を使うことで、Global Snapshotでの書き込みがすべて通知されるようになります。
書き込みのたびに registerApplyObserver
の通知を行いたい場合は以下のようにすることで可能になります。
Composeの処理でも似たようなことをやっています。 (ソースコード)
おまけ
このSnapshotを使って、ViewシステムでリアクティブUIを実現してみます。あくまで遊んだ程度なので色々雑です。
ボタン押すたびにカウンターが変更され、変更されるたびTextViewを更新しています。
snapshotFlow
を使って値を反映していますが、 snapshotFlow
は名前のとおり内部でSnapshotを使用しています。
snapshotFlow
は registerApplyObserver
で変更を監視し、ブロック内で読み取られてる状態(この例だとcounter変数)が変更されたときに、動くようになっています。
registerApplyObserver
を使ってるので、 snapshotFlow
単体では動かず、registerGlobalWriteObserver
を使って変更通知を行っています。Compose UIではこの処理を自動でやっていくれています。