Jetpack Composeの公式サンプル JetNewsのFavoriteボタンを押した時の流れ

現在Jetpack Composeは絶賛開発中ですが、以下で新しいバージョンを使ったサンプルアプリがあります。そのサンプルの中で、いいねボタンが存在します。これがどうやって動くのかを見ていきます。
https://github.com/android/compose-samples/tree/master/JetNews
![toggle.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/f86c1333-68c1-5d62-522f-21dcee92e0db.gif)
まだbeta版でもないので、きっと変わっていくとは思います。
0.1.0-dev09での情報です。

## JetNewsのコードを読んでいく。

まずこの行はPostCardSimpleのようで、以下のようにプレビューされています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/37a34698-96dd-a02e-8010-ade5b50729a3.png)



```kotlin
@Composable
fun PostCardSimple(post: Post) {
    Clickable(
        modifier = Modifier.ripple(),
        onClick = { navigateTo(Screen.Article(post.id)) }
    ) {
        Row(modifier = Modifier.padding(16.dp)) {
            PostImage(post, Modifier.padding(end = 16.dp))
            Column(modifier = Modifier.weight(1f)) {
                PostTitle(post)
                AuthorAndReadTime(post)
            }
            // **↓Bookmarkボタン**
            BookmarkButton(
                isBookmarked = isFavorite(postId = post.id),
                onBookmark = { toggleBookmark(postId = post.id) }
            )
            // **↑Bookmarkボタン**
        }
    }
}
```

ちなみにBookmarkボタンは以下のようになっており、IconToggleButtonをラップして、一般的に使いやすくコンポーネント化しているという感じになります。(Composeでの開発では、こういうコンポーネントに分けていくのが大切になりそう)

```kotlin
@Composable
fun BookmarkButton(
    isBookmarked: Boolean,
    onBookmark: (Boolean) -> Unit
) {
    IconToggleButton(checked = isBookmarked, onCheckedChange = onBookmark) {
        if (isBookmarked) {
            Icon(vectorResource(R.drawable.ic_bookmarked), Modifier.fillMaxSize())
        } else {
            Icon(vectorResource(R.drawable.ic_bookmark), Modifier.fillMaxSize())
        }
    }
}
```

BookmarkButtonの呼び出しに戻りましょう。
このisFavorite(postId)とtoggleBookmark(postId)の実装が気になります。

```kotlin
BookmarkButton(
    isBookmarked = isFavorite(postId = post.id),
    onBookmark = { toggleBookmark(postId = post.id) }
)
```

isFavorite()の実装は以下のようになっています。普通にlist内にpostIdがあるかをチェックするだけでみたいです。

```kotlin
fun isFavorite(postId: String) = JetnewsStatus.favorites.contains(postId)
```

またJetnewsStatusは以下のようになっており、`ModelList`というものを使って、お気に入りした記事のid一覧を保持しているようです。`ModelList`は`androidx.compose.frames.ModelList`パッケージに存在するクラスです。

```kotlin
@Model
object JetnewsStatus {
    var currentScreen: Screen = Screen.Home
    val favorites = ModelList<String>()
    val selectedTopics = ModelList<String>()
}
```

toggleBookmark()の実装は以下のようになっています。
普通のリストの操作という感じです。

```kotlin
fun toggleBookmark(postId: String) {
    with(JetnewsStatus) {
        if (favorites.contains(postId)) {
            favorites.remove(postId)
        } else {
            favorites.add(postId)
        }
    }
}
```

さて、toggleBookmark()やisFavorite()には`@Composable`などがついていない普通の関数ですが、どのように変更を検知して、更新されるのでしょうか？

```kotlin
@Model
object JetnewsStatus {
    var currentScreen: Screen = Screen.Home
    val favorites = ModelList<String>()
    val selectedTopics = ModelList<String>()
}
```

JetnewsStatusは`@Model`がついています。`@Model`についてはcodelabにいかが書かれています。観測可能にしてくれるということでした。そのため、変更したときにちゃんと変更が反映されているようです。
`The @Model annotation will cause the Compose compiler to rewrite the class to make it observable and thread-safe.`
https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/?hl=ja#4

## 実際内部で何が起きるのか？
簡易的に以下のように変更したときの挙動を追ってみました。

```kotlin
fun toggleBookmark(postId: String) {
    with(JetnewsStatus) {
        AllFavoriteModeled.isFavorite = !AllFavorite.isFavorite
    }
}

fun isFavorite(postId: String) = AllFavorite.isFavorite

@Model
object AllFavorite {
    var isFavorite: Boolean = false
}
```

この場合、全体で一つのisFavoriteという変数をみるため、両方変更されます。

![change.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/1486381d-8e57-4183-8929-96d406c8225d.gif)

3行まとめ

* `@Model`がついたクラスのインスタンスのプロパティからgetするときに、FrameManagerに`@Model`がついたクラスのインスタンスとRecomposeScope(getした場所)が保存される。
* `@Model`がついたクラスのインスタンスのプロパティをsetするときに、保存された情報を使って、getした場所のRecomposeScopeをinvalidate()する
* invalidate()するとrecompose()され、最終的にプロパティからgetしたときの場所が呼び出される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/4109d064-5d09-42c0-4c17-c816ae085d20.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/26562cc0-985f-d643-95ae-d2c70ba7fbe8.png)




----

コードを読んだ履歴


まずは`ModelList`ではなく、普通の`@Model`がついているクラスでの変更についてちょっと見てみましょう。
インスタンス変更されたときに更新されているのかを探ってみましょう。このあたりのコードをもっと読んだほうが良さそうなどあれば教えて下さい。 :pray: 

まずこう変更してみます。

```kotlin
fun toggleBookmark(postId: String) {
    with(JetnewsStatus) {
        AllFavoriteModeled.isFavorite = !AllFavoriteModeled.isFavorite
    }
}

fun isFavorite(postId: String) = AllFavoriteModeled.isFavorite

@Model
object AllFavoriteModeled {
    var isFavorite: Boolean = false
}
```

この場合、全体で一つのisFavoriteという変数をみるため、両方変更されます。

![change.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/1486381d-8e57-4183-8929-96d406c8225d.gif)

`AllFavoriteModeled`はバイトコード的には `AllFavoriteManual`以下に作ったクラスと同じものになっています。
少し長いですが、とりあえずsetterだけ見ていきましょう。

```kotlin
object AllFavoriteManual : Framed {
    private var record: Record = BooleanRecord()

    init {
        _created(this)
    }

    override val firstFrameRecord: Record
        get() = record

    override fun prependFrameRecord(value: Record) {
        value.next = record
        record = value
    }

    var isFavorite: Boolean
        set(value) {
            val booleanRecord = record.writable(
                framed = this,
                frame = currentFrame()
            ) as BooleanRecord
            booleanRecord.field = value
        }
        get(): Boolean {
            val booleanRecord = record.readable(this) as BooleanRecord
            return booleanRecord.field
        }

    class BooleanRecord : Record {
        override var frameId: Int = currentFrame().id
        override var next: Record? = null

        @JvmField
        var field: Boolean = false

        override fun assign(value: Record) {
            @Suppress("UNCHECKED_CAST")
            (value as? BooleanRecord)?.let {
                this.field = it.field
            }
        }

        override fun create() = BooleanRecord()
    }
}
```

以下のコードで`_writable()`を呼び出しています。

```kotlin
            val booleanRecord = _writable(record, this) as BooleanRecord
            booleanRecord.field = value
```

`_writable()`の呼び出しによって、以下の関数が呼び出されます。まずreadable

```kotlin
fun <T : Record> T.writable(framed: Framed, frame: Frame): T {
    if (frame.readonly) throw IllegalStateException("In a readonly frame")
    val id = frame.id
    val readData = readable<T>(this, id, frame.invalid)
...

    // The first write to an framed in frame
    frame.writeObserver?.let { it(framed, false) }
...
    val newData = synchronized(framed) {
...
    }
    newData.assign(readData)
    newData.frameId = id

    frame.modified?.add(framed)

    return newData
}

```


以下のようなObserverが動きます。 `schedule{}`を呼び出しており、ここで`Handler.post`が行われ、変更が検知されます。

```kotlin:FrameManager.kt
    private val writeObserver: (write: Any, isNew: Boolean) -> Unit = { value, isNew ->
        if (!commitPending) {
            commitPending = true
            // **↓ここでhandlerにpost
            schedule {
                commitPending = false
                // **↓ここでnextFrameが作られる
                nextFrame()
            }
        }
        recordWrite(value, isNew)
    }
```