Jetpack Compose basics やってみた備忘録

# はじめに
https://developer.android.com/codelabs/jetpack-compose-basics#0

このコースに取り組んだ際の学びやポイントをまとめています。
「宣言的UIとは何か？」を理解する手助けになるはずです。
学習の伴走役としてお役立てください。

# 学んだポイント
- コースを通じて宣言的UIが学べた
    - 基本的にコードとUIが対応する → コードからUIが連想できる
    - コールバック関数を下方向に渡してタップなどのイベントを通知、上で通知を受け取り状態変数を更新
    - Compose では、UI 要素を非表示にすることはない、非表示にするのではなく UI ツリーから外す

# 環境
- Android Studio Ladybug | 2024.2.1 Patch 3


# 新規プロジェクトの作成

- 利用可能なテンプレートから Empty Activity 選択
- MainActivityが作られます
- 着目すべきはMyListApplicationThemeの中

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyListApplicationTheme {
                Scaffold( modifier = Modifier.fillMaxSize() ) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}
```

- MyListApplicationTheme の中、Scaffold以降をMyAppに切り出す
- Modifier.fillMaxSizeは引数として渡してあげる
```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyListApplicationTheme {
             MyApp(modifier = Modifier.fillMaxSize()) // ここ
            }
        }
    }
}
```
- 新しく作るコンポーザブル関数には、`@Composable` アノテーションをつける
- これでonCreate()とプレビューではMyApp()を呼び出すだけで良くなる

```kotlin
@Composable
fun MyApp(modifier: Modifier = Modifier){
    Scaffold(modifier = modifier) { innerPadding ->
        Greeting(
            name = "Android",
            modifier = Modifier.padding(innerPadding)
        )
    }
}
```

# Greetingをアップグレードして開閉UIを作る

![6675d41779cac69.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/3007b68d-65ae-0f35-2147-ec3ae39a4323.gif)

## ポイント
Greetingコンポーネントは状態変数を持っていて、ボタンタップを検知してtrue / falseを反転する

val 1. 開閉管理（expanded）：true / false
val 2. padding管理（extraPadding）：48dp / 0dp

padding管理については状態変数 expanded を参照して、true なら Greeting コンポーネントの下方向の padding を 48dp にする。false なら 0dp とすることでアイテムの開閉を実現しています。

## 列と行の作成
- Compose には、基本の標準レイアウト要素として、Column、Row、Box という 3 つのコンポーザブルがある

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/c782714e-9a38-4545-0d1b-80ce48ac68f1.png)

- 内部にアイテムを配置して使う

```kotlin
Column {
 	Text("サンプル1")
 	Text("サンプル2")
 	Text("サンプル3")
 }
```

## ボタンの書き方

- Buttonコンポーザブルの引数にはクリック時の処理を、後置ラムダにはコンポーザブル関数を書き任意のコンテンツを入れられる
- 今回はText()

```Kotlin
Button(
 	onClick = { } // クリック時の処理
 ) {
 	Text("Show more") // 任意のコンテンツ
 }
```

## Composeに状態を持たせる

```kotlin
// Don't copy over
 @Composable
 fun Greeting(name: String) {
     var expanded = false // Don't do this!
 
     Surface(
         color = MaterialTheme.colorScheme.primary,
         modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
     ) {
         Row(modifier = Modifier.padding(24.dp)) {
             Column(modifier = Modifier.weight(1f)) {
                 Text(text = "Hello, ")
                 Text(text = name)
             }
             ElevatedButton(
                 onClick = { expanded = !expanded }
             ) {
                 Text(if (expanded) "Show less" else "Show more")
             }
         }
     }
 }
```

- しかしこれは動かない
-  expanded変数が更新されてもComposeはそれを「状態変数」として検知しないため何もおこならい
- なぜか？変数の値が変わっても再コンポジションされないのは、変更がComposeにトラッキングされていないから
- Greetingコンポーザブルに内部状態を付与するには、mutableStateOf関数を使用する。そうすればCompose は State を読み取る関数を再コンポーズするようになる


```kotlin
import androidx.compose.runtime.mutableStateOf
 
 @Composable
 fun Greeting() {
 	val expanded = mutableStateOf(false)
 }
```

 - しかし、コンポーザブル内の変数にmutableStateOfを割り当てることはできない
 - 再コンポジション（再描画処理）が随時発生して状態変数が毎回 false にリセットされる
 - 再コンポジションの前後で状態を保持するには、remember を使用して可変状態を記憶する


```kotlin
import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 
 @Compose
 fun Greeting(...){
 	val expanded = remember { mutableStateOf(false) }
```


## コード
- 上記を踏まえて実装する

```kotlin
package com.example.mylistapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.mylistapplication.ui.theme.MyListApplicationTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyListApplicationTheme {
             MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier){
    Scaffold(
        modifier = modifier,
    ) { innerPadding ->
        Greeting(
            name = "Android",
            modifier = Modifier.padding(innerPadding),
        )
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) } // 状態変数
    val extraPadding = if (expanded.value) 48.dp else 0.dp // 状態変数によって条件分岐

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(
            vertical = 4.dp,
            horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column (modifier = modifier.weight(1f)
                .padding(bottom = extraPadding)){
                Text("Hello")
                Text(name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) { Text(if (expanded.value) "show less" else "show more") }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MyListApplicationTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }
}
```

とこんな感じになりました

![Screen_recording_20241223_005839.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/461b5079-d704-a729-71e4-42f7cd963ba4.gif)


## 状態ホイスティング

- 状態をホイストすると、状態の重複やバグ発生を回避できる
- 上階層のコンポーザブルに状態を持たせておく

```kotlin
@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false }
        ) {
            Text("Continue")
        }
    }
}
```

- Compose では、UI 要素を非表示にすることはない、非表示にするのではなく UI ツリーから外す
- 例えばMyAppコンポーザブルに、状態変数 shouldShowOnboarding を設置

```kotlin
// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}
```

- しかし、状態変数 shouldShowOnboarding にはアクセスできない

- 状態によって分岐する
    - 状態変数にfalseを代入して更新するような関数を予めラムダで用意しておいて渡す

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572559/912c240d-3d67-d7f0-9a35-3736e6f45cc7.png)

 - コールバック関数を下に渡す
     - 今回の場合、コールバック関数は状態変数をfalseに更新するラムダ式、onContinueClicked
     - コールバックはイベントが発生した時に実行される
- コールバックを下に渡して通知をもらい、状態の変更処理は上で行う
- 状態ではなく関数を渡すのがポイント
    - これで他のコンポーネントによって更新されることはなくなる
    - すなわち、上にある状態変数を保護する

```kotlin
@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}
```

## 効率の良い遅延リストを作成する

- LazyColumn：画面に表示されているアイテムのみをレンダリングする
- LazyColumn と LazyRow は、Android ビュー内の RecyclerView と同等

## 状態の維持
- remember関数はコンポーズがUIツリー内に存在している間だけ状態を保持する
- UIツリーから外れると状態は削除される
- 例えば画面回転が発生するとAndroidはデフォルトでActivityが再生成される
    - その結果ComposeのUIツリーが破棄されて再構築される
    - この時、remember関数で保持している状態は、Activityの再描画に伴ってリセットされる
    - このような場合でも値を保持し続けたい場合は、rememberSaveableを使用すると良い
    - 構成の変更やプロセスの終了後も保持される

```kotlin
import androidx.compose.runtime.saveable.rememberSaveable
    // ...
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
```