Jetpack Composeを学ぶ前に知っておきたいこと

## はじめに

Android歴1.5年の私が[Composeコース](https://developer.android.com/courses/jetpack-compose/course?hl=ja)の[Composeの思想](https://developer.android.com/jetpack/compose/mental-model?hl=ja)を読んで自分なりの理解でまとめたものになります

Jetpack Composeを理解していくにあたり特有の考え方や処理のクセなどの理解に役立てばと思います


## 宣言型プログラミングのパラダイム
### 宣言型プログラミングとは

**細かく指示する**Imperative (命令的、指示的) に対してDeclerative (宣言的) は**細かく指示しない**、といったニュアンス

コーヒーの作り方は言わないで、「コーヒー頂戴」と目的を一言で表して細々とは伝えないような形
(see https://qiita.com/Hiroyuki_OSAKI/items/f3f88ae535550e95389d)

なかなか理解が難しい。。


### パラダイムとは
範例、思考の方法論やものの見方

→要するに、宣言型プログラミングのパラダイムとは
**細かく指定しなくても裏でいい感じに実装してくれるからやりたい事を明確にしてきちんと指示を出すことが大事**
なプログラミング手法と理解

### 宣言型プログラミングの利点
従来のxmlを取得してViewを手動で操作する手法は、更新し忘れや更新の競合などで**メンテナンスが複雑**であった

宣言型UIフレームワークであるComposeでは以下の利点がある

画面全体をゼロから再構成して必要変更のみを適用する→手動で更新せずに済む

ただし、画面全体の再構成には時間やCPU、バッテリーの使用にも影響するため、Composeでは必要がある部分だけを<INS>再コンポーズ</INS>することでリソースの使用を軽減している

## シンプルでコンポーズ可能な関数
https://qiita.com/t_hiro2626/items/d7e366222f24dd4d185c

こちらでも少し触れたが、`@Composable`アノテーションをつけることでコンポーズ可能な関数を宣言できる

以下の特徴がある
- @ComposableアノテーションでComposeコンパイラにこの関数がデータをUIに変換するものと伝えることができる
- 引数でパラメータを受け取ることができる
- 他のコンポーズ可能な関数を呼び出すことができる
（UI階層を形成できる）
- Returnで何も返さない/戻り値がない
（UIウィジェットを作成するというより、目的の画面状態を記述するためのもの）

## 宣言型パラダイム シフト
**従来のxml方式（命令型）**
- Viewのツリーをインスタンス化することでUIを初期化
- アプリのロジックでViewを操作できるようgetterとsetterが公開

**Compose（宣言型）**
- Viewはステートレスでgetterとsetterが公開されていない
- 同一のコンポーズ可能な関数を、異なる引数で呼び出すことでUIを更新

**アプリロジックでデータを取得してUI表示する流れ**
![スクリーンショット 2023-04-09 16.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2044482/8bb9c08d-1dc4-29e6-da0b-0a091e27418e.png)

1. アプリロジックは最上位のコンポーズ可能な関数にデータを提供
2. 受け取ったコンポーズ可能な関数が他のコンポーズ可能な関数を呼び出す
3. 階層を下っていきUIを描画 


**ユーザーによる操作によってUIを更新する流れ**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2044482/9ebe51f1-13f1-9be1-6dbc-9b8608235d65.png">

1. `onClick`などでイベントを検知
2. アプリロジックへ伝えられ、必要な箇所の状態を変更
3. 状態が変更されると、コンポーズ可能な関数が新しいデータで再度呼び出される
4. 必要な箇所のUIが再描画される

<INS>この再描画の一連の流れを**再コンポーズ**という</INS>

## 再コンポーズ

変更されたコンポーネントを更新する再コンポーズを理解するには、以下の特徴を知る必要がある

1. 更新する必要があるもののみに対して処理が走る
2. 副作用を含んではいけない
3. Compose可能な関数の動作特性

### 1. 更新する必要があるもののみに対して処理が走る

先述した通り、UIツリー全体を再コンポーズしようとするとCPUやバッテリーのコストがあるため最低限の更新を行うようになっている

基本的な流れは以下になる
1. 入力に変更が検知される（関数の入力が変更）
2. 対応するコンポーズ可能な関数を再度呼び出される
3. 呼び出された関数の中で、変更された可能性がある関数やラムダを呼び出す
4. パラメータを変更しない関数やラムダを全てスキップ
5. 3、4を最後まで繰り返す

以上のステップでComposeでは効率的に再コンポーズを実施している


### 2. 副作用を含んではいけない
<INS>副作用とは、コンポーズ可能な関数の範囲外で発生するアプリの状態の変化のこと</INS>

具体的に以下のようなアクションは危険という記述があった
- 共有オブジェクトのプロパティへの書き込み（varで定義したローカル変数など）
- `ViewModel`で監視可能なデータの更新
- 共有設定の更新（`SharedPreference`など）

しかし、Composeの思想上は副作用がないのが理想であるがスナックバーの表示などの１回限りのイベントをトリガーする場合や特定の状態で別の画面に移動する場合などで副作用が必要になることがある

https://developer.android.com/jetpack/compose/side-effects?hl=ja 

副作用のユーズケースに関連するAPIがいくつかいくつか紹介されていた

- **[LaunchedEffect](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#LaunchedEffect(kotlin.Any,kotlin.coroutines.SuspendFunction1))：コンポーザブルのスコープ内でsuspend関数を実行する**


- **[rememberCoroutineScope](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#rememberCoroutineScope(kotlin.Function0))：コンポーザブルの外部でコルーチンを起動するためにComposition対応スコープを取得する**

- **[rememberUpdatedState](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#rememberUpdatedState(kotlin.Any))：値が変化しても再起動すべきでない作用の値を参照する**

- **[DisposableEffect](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#DisposableEffect(kotlin.Any,kotlin.Function1))：クリーンアップが必要な作用**

- **[SideEffect](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#SideEffect(kotlin.Function0))：Composeの状態を非Composeコードに公開する**

- **[derivedStateOf](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#derivedStateOf(kotlin.Function0))：1つ以上の状態オブジェクトを別の状態に変換する**

- **[snapshotFlow](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary?hl=ja#snapshotFlow(kotlin.Function0))：ComposeのStateをFlowに変換する**

ただし、Compose可能な関数は副作用なしであるべきなので、これらのAPIを本当に<INS>使用する必要があるか検討することが大事</INS>

### 3. Compose可能な関数の動作特性

https://android-developers-jp.googleblog.com/2022/10/composable-functions.html

コンポーズ可能な関数では、以下のような意識しておくべき動作特性がある。副作用を含まないことに加え、<INS>**同じ引数で何度呼び出しても同じ動作になることが重要**</INS>。

#### 1.コンポーズ可能な関数は任意の順序で実行される
次のコードは上から順番に実行されるように思えるが、その通りに実行されるとは限らない

```.kt
@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}
```
`StartScreen()`、`MiddleScreen()`、`EndScreen()`の呼び出しは**任意の順番**で発生する。
Composeでは一部のUI要素の優先順位が他よりも高く設定されていることがあるためこのような仕様になっている

その為、例えば`StartScreen()`の中でグローバル変数を設定して`MiddleScreen()`で利用するということはしてはいけない（副作用）

#### 2.コンポーズ可能な関数は並行して実行できる
Composeでは、コンポーズ可能な関数を**並行して実行することで、再コンポーズを最適化**できる

この最適化はコンポーズ可能な関数がバックグラウンドのスレッドプールで実行される時があり、**複数のスレッドから同時に呼び出される可能性**がある

なので、以下コードのように関数がローカル変数に変更を与えるようなことをしてはいけない

```.kt
@Composable
fun ListWithBug(myList: List<String>) {
    var items = 0 //ローカル変数

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // ❌ラムダ外に影響を与えてはいけない
            }
        }
        Text("Count: $items")
    }
}
```
`items`は、再コンポーズの度に変更されてしまう**副作用**となる。Composeではこのような書き込みはサポートされておらず避けるべきである


#### 3.再コンポーズは可能な限りスキップする
UI の一部が無効な場合、Composeは更新が**必要な部分のみ**を再コンポーズしようとする

```.kt
/**
 * ユーザーがクリックできる名前の一覧をヘッダーで表示する
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // これはheaderが変わったときに再構成され、namesが変わったときには再構成されない
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // （LazyColumnは、RecyclerViewのComposeのようなもの）
        // （items()で渡すラムダは、RecyclerView.ViewHolderと同じようなもの）
        LazyColumn {
            items(names) { name ->
                // nameが更新されると、そのアイテムのアダプターが再コンポーズされる
                // headerが変更されても再コンポーズされない
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}
```

#### 4.再コンポーズは厳密なものではない
Compose可能な関数の引数が変更された可能性があることをComposeが認識する度に再コンポーズが開始されるが、これは<INS>厳密なものではない</INS>

Composeは再度引数が変更されるより前に再コンポーズが終わる事を想定していて、もし再コンポーズが終わるより先に引数の変更が検知されればComposeは再コンポーズを<INS>一旦キャンセルして新しい引数に対応した再コンポーズが開始</INS>されることがある

再コンポーズがキャンセルされた場合、Composeは再コンポーズの処理から**UIツリーを破棄**する。
その為表示されているUIに依存するような**副作用を持ってしまっている場合、変化に対応できずアプリの状態に一貫性がなくなる可能性**がある



#### 5.コンポーズ可能な関数は何度も実行されることがある

コンポーズ可能な関数がコストの高いオペレーション（デバイス ストレージからの読み取りなど）を行う場合、関数によっては**UIジャンク**が発生することがある

例えば、ウィジェットがデバイス設定を読み取ろうとすると、その設定が1秒間に数百回読み取られてしまうなど、**アプリのパフォーマンスに深刻な影響**を及ぼすことなどがある

コンポーズ可能な関数で<INS>データが必要な場合</INS>は、データ用のパラメータを定義し、コストの高い処理をコンポーズ外の**別のスレッドで実行**、`mutableStateOf`または`LiveData`を使用してデータを引数に渡すことで実現可能

## まとめ
- Jetpack Composeは従来のxmlでのUI操作とは全く違う**宣言型プログラミングを採用**している
- コンポーズ可能な関数は、関数が関数を呼ぶことで**UI階層を形成**でき、引数の**パラメータが命**の戻り値なしの関数である
- データを取得してから表示した後は、**必要な部分のみを再コンポーズ**していき**効率的**にUIを更新
- 再コンポーズを理解するには以下三つを前提として理解する必要がある
    1. **更新する必要があるもののみ**に対して処理が走る
    2. **副作用**を含んではいけない
    3. Compose可能な関数の**動作特性**