Jetpack Compose日記#1


主に[コードラボ](https://github.com/googlecodelabs/android-compose-codelabs)に沿ってComposeの基礎を学ぶ。
##### 利点
1, [コードの削減](https://developer.android.com/jetpack/compose/why-adopt?hl=ja#less-code)
2, [直感的](https://developer.android.com/jetpack/compose/why-adopt?hl=ja#intuitive)
3, [開発を加速させる](https://developer.android.com/jetpack/compose/why-adopt?hl=ja#accelerate-development)
4, [パワフル](https://developer.android.com/jetpack/compose/why-adopt?hl=ja#powerful)

プロジェクトの作成、「Empty Compose Activity」を選択することでプロジェクトはComposeを使用するように構成され、必要な依存関係等が追加される。ComposeがサポートするAPIレベルは21のため、最小レベルを21に設定。

##### コンポーズ可能な関数
コンポーズ可能な関数とは、「@Composable」がついている通常の関数。これにより内部で他のコンポーズ関数を呼び出すことが可能になる。Compose を使用する場合でも、Activity は依然として Android アプリへのエントリーポイントである。

```
setContent {
            BasicCodelabTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    //Greeting:コンポーズ可能な関数
                    Greeting("Android")
                }
            }
        }
```
Android Viewとは異なり、setContent内でコンポーズ可能な関数を呼び出すことでレイアウトを定義する。
どのように表示されるかは、アプリを実行するか[Androidプレビュー](https://developer.android.com/jetpack/compose/preview?hl=ja)を利用する。

```
@Composable
fun Greeting(name: String) {
    Surface(color = MaterialTheme.colorScheme.primary) {    
        Text(text = "Hello $name!")
    }
}
```
Surfaceでラップすることで背景色を指定。プレビューをリフレッシュすることで更新される。「プレビュー画面の左上、緑色のマーク」。背景色を変更すると文字色も変更されるが、マテリアルコンポーネントの設計上適切な色を選択するようになっている。

##### 修飾子

Surface や Text といったほとんどの Compose UI 要素は、省略可能な modifier パラメータを受け入れます。修飾子は、UI 要素に対して親レイアウト内での配置、表示、動作を指示します。以下はpaddingの例。
```
Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
```
他にもクリック可能・スクロール可能等がある。詳しい修飾子については[ Compose修飾子のリスト]を参照。(https://developer.android.com/jetpack/compose/modifiers-list?hl=ja)

##### コンポーザブルを再利用
コンポーネントが多くなるとネストのレベルが増え、読みやすさに影響する場合がある。再利用可能な小さいコンポーネントを作成することで、アプリで使用するUI要素のライブラリを簡単に構築することができる。

ベスト プラクティスとして、デフォルトでは空の修飾子を割り当てた修飾子パラメータを関数に含めることをおすすめします。この修飾子を、関数内で最初に呼び出すコンポーザブルに転送します。こうすると、呼び出し元のサイトがコンポーズ可能な関数の外部でレイアウトの手順と動作を調整できます。
```
@Composable
private fun MyApp(modifier: Modifier = Modifier) { //デフォルト値は空
    //呼び出し元で調整されたmodifireを利用
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}
```
##### 列と行を作成する

Compose には、基本の標準レイアウト要素として、Column、Row、Box という 3 つのコンポーザブルがあります。
![イメージ](https://developer.android.com/static/codelabs/jetpack-compose-basics/img/518dbfad23ee1b05_1920.png?hl=ja)
これらのコンポーザブルは、コンポーズ可能なコンテンツを受け取るコンポーズ可能な関数であり、内部にアイテムを配置できます。たとえば、Column 内のそれぞれの子は縦方向に配置されます。

コンポーズ可能な関数は、Kotlin の他の関数と同様に使用できます。つまり、UI の表示方法に影響を与えるステートメントを追加できるので、UI の作成が非常に容易になります。

たとえば、for ループを使用して Column に要素を追加できます。
```
@Composable
    private fun MyApp(modifier: Modifier = Modifier, names: List<String> = listOf("World", "Compose")) {
        //note: コンポーズ可能な関数内でkotlinの他の関数呼び出し。
        Column(modifier) {
            names.forEach { name ->
                Greeting(name)
            }
        }
    }
```

##### Button
コンポーザブルを最後の引数として受け取ります。[後置ラムダ](https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas)はかっこの外側に移動できるため、任意のコンテンツを子としてボタンに追加できます。たとえば、次のように Text を追加できます。(種類：Button、ElevatedButton、FilledTonalButton、OutlinedButton、TextButton等が有り、ElevatedButtonが一番オーソドックス)
```
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}
```
##### Composeの状態
レイアウトがユーザーの変更によって対応されるようにする。
Compose アプリは、コンポーズ可能な関数を呼び出すことにより、データを UI に変換します。データが変更されると、Compose はそれらの関数を新しいデータで再実行し、更新された UI を作成します。これを再コンポジションと呼びます。

以下のような記述は期待通りに動作しない。expanded変数に異なる値が設定されても、composeはそれらを「状態変更」として検出しないためである。
```
@Composable
private fun Greeting(name: String) {
    var expanded = false 

    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")
            }
        }
    }
}
```
変数の値が変わっても再コンポジションがトリガーされないのは、変更がComposeによってトラッキングされていないためです。また、Greeting が呼び出されるたびに、変数は false にリセットされます。

コンポーザブルに内部状態を追加するには、***mutableStateOf***関数を使用します。そうすれば、Compose は State を読み取る関数を再コンポーズするようになります。(State と MutableState は、なんらかの値を保持し、その値が変化するたびに UI の更新（再コンポジション）をトリガーするインターフェースです。)

```
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) 
}
```
しかし、***コンポーザブル内の変数に mutableStateOf を割り当てることはできません***。前述のとおり、再コンポジションが随時発生する可能性があります。その結果、コンポーザブルが再度呼び出されて変更可能な状態がリセットされ、値が新たに false になることが考えられます。

再コンポジションの前後で状態を保持するには、***remember*** を使用して可変状態を「記憶」します。

```
@Composable
fun Greeting() {
    val expanded = remember { mutableStateOf(false) }
    ...
}
```
remember を使用すると、再コンポジションから***保護***されるため、状態はリセットされません。

なお、同じコンポーザブルを画面の複数の部分から別々に呼び出すと、異なる UI 要素が作成され、状態もそれぞれで別になるので注意してください。***内部状態は、クラス内のプライベート変数と見なすことができます***。

##### アイテムを展開する
状態に依存する変数を追加する。以下はexpandedの状態により、それに依存するextraPaddingの値が変更され、レイアウトが変更される
```
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
...
Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
...
```

##### 状態ホイスティング
コンポーズ可能な関数では、複数の関数によって読み取られるか変更される状態は、共通の祖先に配置される必要があります。そのためのプロセスを***状態ホイスティング***と呼びます。状態をホイスト可能にすると、状態の重複やバグの発生を回避できます。

オンボーディング画面のコンポーズ可能な関数を追加する
```
@Composable
fun OnBoardingScreen(onContinueClicked : () -> Unit,
    modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Welcome to the Basics Codelab!")
        Button(modifier = Modifier.padding(vertical = 24.dp), onClick = onContinueClicked) {
            Text("Continue")
        }
    }
}


@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}
```
- shouldShowOnboarding は、= キーワードではなく ***byキーワード***を使用しています。これは、毎回 .value を入力する手間を省くためのプロパティ デリゲートです。

Compose では、UI 要素を非表示にすることはありません。非表示にするのではなく、単に UI 要素をコンポジションに追加しないようにして、それらの要素が Compose の生成する UI ツリーに追加されないようにします。そのために、シンプルな Kotlin の条件ロジックを使用します。たとえば、オンボーディング画面またはあいさつ文のリストを表示するには、次のようなコードを記述します。

```
@Composable
private fun MyApp(modifier: Modifier = Modifier, names: List<String> = listOf("World", "Compose")) {
    var shouldShowOnBoarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnBoarding) {
            //処理を親で決める
            OnBoardingScreen(onContinueClicked = {
                shouldShowOnBoarding = false
            })
        } else {
            Greetings()
        }
    }
}

```
イベントを上方向に渡すには、***コールバックを下方向に渡します***。コールバックは、他の関数に引数として渡され、イベントが発生したときに実行される関数です。

状態の値をなんらかの方法で親と共有する代わりに、状態を***ホイスト***します。ホイストは、状態にアクセスする必要がある共通の祖先に状態を移動するだけです。

##### 効率の良い遅延リストを作成する
Greetingsのnamesを変更。
```
names: List<String> = List(1000) { "$it" }
```
スクロール可能な列を表示するには、***LazyColumn*** を使用します。LazyColumn は画面に表示されているアイテムのみをレンダリングするので、大きなリストを表示する場合は効率が良くなります。

注: ***LazyColumn と LazyRow は、Android ビュー内の RecyclerView と同等です***。

LazyColumn API の基本的な利用方法では、スコープ内で items 要素を使用します。この要素には、個々のアイテムをレンダリングするロジックを記述します。
```
@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}
```
注: 必ず androidx.compose.foundation.lazy.items をインポートしてください。Android Studio は、デフォルトでは別のアイテム関数を選択するからです。

注: LazyColumn は、RecyclerView のように子のリサイクルは行いません。スクロールされている間は新しいコンポーザブルを出力しますが、効率は低下しません。これは、コンポーザブルの出力は Android Views のインスタンス化に比べて低コストであるためです。

##### 状態を維持
今回のアプリには 1 つ問題があります。デバイスでアプリを実行し、ボタンをクリックしてから回転させると、オンボーディング画面が再度表示されます。remember 関数は、***コンポーザブルが Composition 内で保持されている間にのみ*** 機能します。回転させると、アクティビティ全体が再起動され、すべての状態が失われます。この問題は、構成の変更やプロセスの終了の際にも発生します。
remember の代わりに ***rememberSaveable*** を使用できます。そうすれば、個々の状態が保存され、構成の変更（回転など）やプロセスの終了後も保持されます。

##### リストのアニメーション化
Compose には、シンプルなアニメーションのための高レベル API から、完全なコントロールと複雑な遷移のための低レベルのメソッドまで、UI をアニメーション化するさまざまな方法があります。詳しくは、こちらの[ドキュメント](https://developer.android.com/jetpack/compose/animation?hl=ja)をご覧ください。

***animateDpAsState*** コンポーザブルを使用します。この関数は State オブジェクトを返しますが、このオブジェクトの value はアニメーションが終了するまで継続的に更新されます。また、型が Dp の「ターゲット値」を受け取ります。
animate*AsState で作成されたアニメーションは、すべて ***割り込み可能*** です。つまり、アニメーションの途中でターゲット値が変更されると、animate*AsState はアニメーションを再実行し、新しい値をポイントします。割り込みは、バネを基にしたアニメーションでは特に自然に見えます。
![movie](https://developer.android.com/static/codelabs/jetpack-compose-basics/img/e7d244875b3b6b6f.gif?hl=ja)
[様々なアニメーションAPI](https://developer.android.com/jetpack/compose/animation?hl=ja)

##### アプリのスタイルとテーマの設定

ui/theme/Theme.kt ファイルを開くと、BasicsCodelabTheme の実装で MaterialTheme が使用されていることがわかります。
MaterialTheme は、[マテリアルデザイン仕様](https://m3.material.io/)のスタイル設定の原則を反映したコンポーズ可能な関数です。このスタイル設定情報は、content 内のコンポーネントにカスケードされて適用されます。各コンポーネントは情報を読み取って自身のスタイルを設定できます。

BasicsCodelabTheme は MaterialTheme を内部的にラップするので、MyApp はテーマで定義されているプロパティでスタイル設定されます。任意の子孫コンポーザブルから、MaterialTheme の 3 つのプロパティ（colorScheme、typography、shapes）を取得できます。これらを使用して、Text の 1 つのヘッダー スタイルを設定します。
```
 Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }
```
上記の例の Text コンポーザブルは、新しい TextStyle を設定しています。独自の TextStyle を作成することも、推奨される MaterialTheme.typography を使用して、テーマで定義されているスタイルを取得することもできます。

一般的に、色、シェイプ、フォント スタイルは ***MaterialTheme*** 内で保持することをおすすめします。たとえば、色をハードコードするとダークモードの実装が困難になり、修正が必要な間違いやすい作業が多数発生する可能性があります。

ただし、選択済みの色やフォント スタイルとわずかに異なるものを使用したい場合があります。そのような場合は、既存の色やスタイルを基にして設定することをおすすめします。

この場合、copy 関数を使用すると、定義済みのスタイルを変更できます。番号を極太にします。
```
 Text(text = name,
        style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)
```
このようにすれば、headlineMedium のフォント ファミリーやその他の属性を変更する必要がある場合でも、わずかな例外について心配せずに済みます。

##### ダークモードでプレビュー

@PreviewのパラメーターにuiModeを追加してダークモードでのプレビューが可能。
```
@Preview(showBackground = true, widthDp = 320, uiMode = UI_MODE_NIGHT_YES, name = "Dark")
```
##### アプリのテーマを微調整する
現在のテーマに関連するものは、すべて ui/theme フォルダ内のファイルで確認できます。たとえば、これまでに使用したデフォルトの色は Color.kt で定義されています。

以下のように、Color.ktに新しい色を定義しましょう。
```
val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)
```

次に、これらの色を Theme.kt 内にある MaterialTheme のパレットに代入します。

```
private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)
```