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.notifyObjectsInitializedSnapshot.sendApplyNotifications を使用する必要があります。

notifyObjectsInitializedsendApplyNotifications の違いは状態に変更があるときのみに advanceGlobalSnapshot を実行するかどうかが違います。 notifyObjectsInitialized は変更がなくても実行します。

advanceGlobalSnapshot は実行することによって現在の状態を適用してObserverに通知し、そこから新しくまたSnapshotを開始するような感じです。(あんまりちゃんと理解してないですが…)

なんかよく分からないので、コードで見てもらったほうが早いかなと思います。

Snapshot.registerApplyObserversendApplyNotifications されたときに変更された状態を通知で受け取れるようになります。

このとき複数回変更されたとしても最新の状態だけが通知されることになります。

Global Snapshotの書き込みを監視する

先程の例では明示的に sendApplyNotifications 等を実行しないと通知が来ませんでしたが、すべての書き込みの通知を受けることも可能です。

Snapshot.registerGlobalWriteObserver を使うことで、Global Snapshotでの書き込みがすべて通知されるようになります。

書き込みのたびに registerApplyObserver の通知を行いたい場合は以下のようにすることで可能になります。

Composeの処理でも似たようなことをやっています。 (ソースコード

おまけ

このSnapshotを使って、ViewシステムでリアクティブUIを実現してみます。あくまで遊んだ程度なので色々雑です。

ボタン押すたびにカウンターが変更され、変更されるたびTextViewを更新しています。

snapshotFlow を使って値を反映していますが、 snapshotFlow は名前のとおり内部でSnapshotを使用しています。

snapshotFlowregisterApplyObserver で変更を監視し、ブロック内で読み取られてる状態(この例だとcounter変数)が変更されたときに、動くようになっています。

registerApplyObserver を使ってるので、 snapshotFlow 単体では動かず、registerGlobalWriteObserver を使って変更通知を行っています。Compose UIではこの処理を自動でやっていくれています。

--

--

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store