Jetpack Compose?なるほど！

<img width="240" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/b5093689-b211-b0a9-1455-fff554804be3.png" style="center"> <img width="240" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/14727728-9ab8-5394-3ea0-7904d6ca455f.png">
![loginCompose.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/5257566c-e01f-d174-d873-b7ba33e01dfc.gif) <img width="308" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/5d893cca-ad4f-a153-b4e7-b4ad440811be.png">

##1、Jetpack Composeとは

>`Jetpack Compose` は、Android のネイティブ UI を構築するための最新のツールキットです。Android の UI 開発を簡素化し、加速します。
少ないコード、パワフルなツール、直感的な `Kotlin API` を使用してアプリをすぐに動かすことができます。


https://developer.android.com/jetpack/compose

ここ数年、業界全体が`宣言型 UI` モデルに移行し始め、ユーザー インターフェースの構築や更新に関連するエンジニアリングは大幅に簡素化されています。web側やモバイル側のUI構築において、`Flutter`、`SwiftUI`、`react`などに比べたら、従来のXMLの優位性がだんだん失っていってます。Googleは2019年にも`Jetpack Compose`を発表したが、まだ実験的なもので、プロダクト環境に使用しない方が良いとその時はアナウンスしていたようです。

つい最近、`Jetpack Compose`のBeta版がリリースされ、正式版が出るのはまもなくのではないかと友達の中にそう思っている人は少なくないです。

##2、環境
####・エディタ

https://developer.android.com/studio/preview

Android Studio Beta 4.2版、またはCanary版では使えそうですが、私の場合はBeta版で`Jetpack Compose`機能を利用しようとすると、下記のインフォとなり、Canary版に切り替えるしかなかったです:robot:( `Compose` UI をすぐにプレビューできる)。

<img width="700" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/93c9ace9-29e4-f7cc-dbfe-77b177b990e7.png">

####・プロジェクト環境
プロジェクトで `Kotlin` 1.4.21 以降を使用していることを確認します。
プロジェクトの `build.gradle`記述:

```gradle
buildscript {
    ext.kotlin_version = "1.4.31"
    ext.compose_version = "1.0.0-beta03"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.0-alpha12'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
```
####・app環境
>アプリの最小 `API レベル`を 21 以上に設定し、アプリの`build.gradle`ファイルで`Jetpack Compose`を有効にする必要があります。
また、`Kotlin`コンパイラ プラグインのバージョンも設定します。

appの `build.gradle`記述:

```gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.example.composedemo"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }

    composeOptions {
        kotlinCompilerVersion kotlin_version
        kotlinCompilerExtensionVersion compose_version
    }
}

dependencies {
    implementation "androidx.compose.ui:ui:$compose_version"
    // Tooling support (Previews, etc.)
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation "androidx.compose.foundation:foundation:$compose_version"
    // Material Design
    implementation "androidx.compose.material:material:$compose_version"
    // Compose Activity Lib
    implementation 'androidx.activity:activity-compose:1.3.0-alpha05'
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
```

これで、`Jetpack Compose`を使ってアプリを開発する準備が整いました。いよいよ、コーディングが始まります。
`Compose`UIは他のクラスファイルを新たに起こして記述した方がいいかもしれません。

![截屏2021-04-03 15.39.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/c1822943-5905-a504-b419-6c3c8974c682.png)

##3、コーディング スタート
>`Jetpack Compose`は、コンポーズ可能な関数に基づいて構築されています。これらの関数を使用すると、UI の形状とデータの依存関係を記述することで、アプリの UI をプログラムで定義できます。UI の構築プロセスを重視する必要はありません。関数名に `@Composable`アノテーションを追加するだけでコンポーズ可能な関数を作成できます。

ここで、注意しなければいけないのはコンポーズ可能な関数の命名は大文字で始まる必要があります。それは何？クラス？オブジェクト？と思われるかもしれないが、実はそれは組み合わせ可能な関数です。`@Composable`アノテーションをマークにし、カスタム`Kotlin`コンパイラ プラグインは、コンポーズ可能な関数をアプリのUI要素に変換します。
`Flutter`、`SwiftUI`、`Compose`...皆一緒ですが、基本の`Text`、`Image`などのUIコンポーネント及び、`Row`、`Column`のようなレイアウトコンポーネントを使いこなせば、ほとんどの画面を書けます。

####・Text
基本なTextコンポーネントを例に、ソースコードをたどり着いたら、`@Composable`アノテーションがついており、内部にはいろいろ開発者に使われそうな属性が提供しています。

```kotlin
@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    Text(
        AnnotatedString(text),
        modifier,
        color,
        fontSize,
        fontStyle,
        fontWeight,
        fontFamily,
        letterSpacing,
        textDecoration,
        textAlign,
        lineHeight,
        overflow,
        softWrap,
        maxLines,
        emptyMap(),
        onTextLayout,
        style
    )
}
```

さあ、Textをいじてみます。
<img width="210" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/ff0ccc5f-2931-7c32-44bc-16bfa1106363.png">

```kotlin
@Preview(name = "text")
@Composable
fun TextView() {
    Box(
        modifier = Modifier.border(
            border = BorderStroke(
                width = 2.dp,
                color = Color(44, 190, 110),
            ),
            shape = RoundedCornerShape(20f, 20f, 20f, 20f),
        )
    ) {
        Row(
            modifier = Modifier
                .padding(5.dp), verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "Hello", style = TextStyle(color = Color(2, 32, 49))
            )
            Box(modifier = Modifier.width(3.dp))
            Text(
                text = "Compose", style = TextStyle(color = Color(46, 106, 247))
            )
        }
    }
}
```
そこに`Modifier`といったモノが出てきたが、`SwiftUI`の`Modifier`と同じように、Android `Jetpack Compose`の`Modifier`もUIやEventの定義ができます。

>修飾子を使用すると、コンポーザブルの表示方法を微調整できます。修飾子では、次のようなことができます。
>>・コンポーザブルの動作と外観を変更する
・ユーザー補助ラベルなどの情報を追加する
・ユーザー入力を処理する
・要素をクリック可能、スクロール可能、ドラッグ可能、ズーム可能にするなど、高レベルの操作を追加する

>修飾子は標準の `Kotlin` オブジェクトです。`Modifier` クラス関数のいずれかを呼び出して修飾子を作成します。こうした関数を連鎖させてコンポーズできます。

####・Image
まずはImageのソースコードを見てみます。

```kotlin
@Composable
fun Image(
    bitmap: ImageBitmap,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
) {
    val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap) }
    Image(
        painter = bitmapPainter,
        contentDescription = contentDescription,
        modifier = modifier,
        alignment = alignment,
        contentScale = contentScale,
        alpha = alpha,
        colorFilter = colorFilter
    )
}
```
使いそうな属性を活用し、以下のネコ写真を飾ってみました。

<img width="240" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/14727728-9ab8-5394-3ea0-7904d6ca455f.png">


```kotlin
@Preview(name = "icon")
@Composable
fun IconView() {
    val imageBitmap: ImageBitmap = ImageBitmap.imageResource(R.drawable.cat)
    Image(
        bitmap = imageBitmap,
        contentScale = ContentScale.Inside,
        contentDescription = "Cat Image",
        modifier = Modifier
            .background(Color(44, 190, 110))
            .height(150.dp)
            .width(100.dp)
            .padding(horizontal = 5.dp, vertical = 5.dp)
            .clip(
                RectangleImageShapes()
            )
    )
}
```

RectangleImageShapesは`Shape`を継承し、写真・画面をラジアン(弧度)によってトリミングするクラスです。角丸だけでなく、必要に応にて各種大胆、モダンなデザインが実現可能です。さあ、`@Stable`アノテーションはどういう意味？
ソースコードのコメントを覗いてみると、
>Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave...
When applied to a function or a property, the Stable annotation indicates that the function will return the same result if the same parameters are passed in. This is only meaningful if the parameters and results are themselves Stable, Immutable, or primitive.
The invariants that this annotation implies are used for optimizations by the compose compiler, and have undefined behavior if the above assumptions are not met. As a result, one should not use this annotation unless they are certain that these conditions are satisfied.

長いですが、簡単に言えば、`@Stable`アノテーションで声明したアイテムは不可変属性のため、再コンポーズするプロセスを省略できます。不要なUIやデータ状態の更新が削減されます。
詳しくは↓

https://developer.android.com/jetpack/compose/lifecycle

```kotlin
@Stable
class RectangleImageShapes(var radian: Float = 70f) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val path = Path()
        path.moveTo(0f, 0f)
        path.lineTo(0f, size.height - radian)
        path.quadraticBezierTo(size.width / 2f, size.height, size.width, size.height - radian)
        path.lineTo(size.width, 0f)
        path.close()
        return Outline.Generic(path)
    }
}
```

####・組み合わせView (ログインページ)
`Box`、`Column`、`Row`、`Text`、`Image`、`TextField`、`Button`などのコンポーネント使って、ログインページを作成します。
<img width="308" alt="text preview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/5d893cca-ad4f-a153-b4e7-b4ad440811be.png">

```kotlin
@Preview(name = "loginPage")
@Composable
fun LoginPage() {
    val imageBitmap: ImageBitmap = ImageBitmap.imageResource(R.drawable.jetpack_compose)
    val userIcon: ImageBitmap = ImageBitmap.imageResource(R.drawable.user_icon)
    var idValue by remember { mutableStateOf(TextFieldValue("")) }
    var pwValue by remember { mutableStateOf(TextFieldValue("")) }
    var checkedState by remember { mutableStateOf(true) }
    Box(contentAlignment = Alignment.Center) {
        Column() {
            Box(contentAlignment = Alignment.Center) {
                Image(
                    bitmap = imageBitmap,
                    contentDescription = "Jetpack Compose",
                    contentScale = ContentScale.FillWidth,
                    modifier = Modifier
                        .padding(top = 0.dp)
                        .fillMaxWidth()
                        .height(210.dp)
                        .clip(RectangleImageShapes(170f))
                )
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier
                        .padding(0.dp)
                        .clip(CircleShape)
                        .background(Color(206, 236, 250, 121))
                        .width(130.dp)
                        .height(130.dp)
                ) {
                    Image(
                        bitmap = userIcon,
                        contentDescription = "User Icon",
                        contentScale = ContentScale.FillBounds,
                        modifier = Modifier
                            .height(80.dp)
                            .width(80.dp)
                            .background(color = Color(44, 190, 110), shape = CircleShape)
                            .padding(3.dp)
                            .clip(
                                CircleShape
                            )
                            .shadow(elevation = 100.dp, clip = true)
                    )
                }
            }
            Box(
                contentAlignment = Alignment.Center, modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 20.dp)
            ) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text(text = "JetPack Compose", fontSize = 18.sp)
                    Text(
                        text = "Less code, Intuitive, Accelerate Development, Powerful~",
                        fontSize = 12.sp,
                        color = Color.Gray
                    )

                }
            }
            Box(
                contentAlignment = Alignment.Center, modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 30.dp)
            ) {
                TextField(
                    value = idValue,
                    onValueChange = { idValue = it },
                    placeholder = {
                        Text("PlayCompose")
                    },
                    shape = RoundedCornerShape(18.dp),
                    colors = TextFieldDefaults.textFieldColors(
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent,
                        disabledIndicatorColor = Color.Transparent,
                        backgroundColor = Color.White
                    ),
                    modifier = Modifier.border(
                        1.dp,
                        Color(111, 111, 111, 66),
                        shape = RoundedCornerShape(18.dp)
                    ),
                    leadingIcon = { Icon(Icons.Default.Person, "account") })
            }
            Box(
                contentAlignment = Alignment.Center, modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 10.dp)
            ) {
                TextField(
                    value = pwValue,
                    onValueChange = { pwValue = it },
                    placeholder = {
                        Text("Password")
                    },
                    shape = RoundedCornerShape(18.dp),
                    colors = TextFieldDefaults.textFieldColors(
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent,
                        disabledIndicatorColor = Color.Transparent,
                        backgroundColor = Color.White
                    ),
                    visualTransformation = PasswordVisualTransformation(),
                    modifier = Modifier.border(
                        1.dp,
                        Color(111, 111, 111, 66),
                        shape = RoundedCornerShape(18.dp)
                    ),
                    leadingIcon = { Icon(Icons.Default.Lock, "password") },
                    trailingIcon = {
                        Icon(Icons.Rounded.Close, "delete", modifier = Modifier.clickable {
                            pwValue = TextFieldValue("")
                        })
                    })
            }
            Box {
                Row(
                    horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 62.dp, vertical = 10.dp)
                ) {
                    Box {
                        Row {
                            Checkbox(
                                checked = checkedState,
                                onCheckedChange = { checkedState = it },
                                colors = CheckboxDefaults.colors(checkedColor = Color(44, 190, 110))
                            )
                            Text(
                                "Remember me",
                                color = Color(44, 190, 110),
                                modifier = Modifier.padding(start = 3.dp)
                            )
                        }
                    }
                    Text(
                        "Privacy Policy",
                        modifier = Modifier.clickable {
                            //ToDo go to Privacy Policy Page etc...

                        },
                        style = TextStyle(textDecoration = TextDecoration.Underline),
                        color = Color(44, 190, 110)
                    )
                }
            }
            Button(
                onClick = { /* Do something like get textField's value, checkbox's value, and validate it to enter the homepage...*/ },
                shape = RoundedCornerShape(18.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 30.dp, start = 100.dp, end = 100.dp, bottom = 50.dp),
                colors = ButtonDefaults.buttonColors(backgroundColor = Color(44, 190, 110))
            ) {
                Text("Come In", color = Color.White)
            }
        }
    }
}
```

特に考えてもらいたいのは、`Jetpack Compose`のUIやStateの更新方式です。`宣言型UI`モデルと言われているが、`命令型UI`モデルとどう違いますか。

https://developer.android.com/jetpack/compose/mental-model

>`命令型 UI` モデルでウィジェットを変更するには、ウィジェットでセッターを呼び出して内部状態を変更します。`Compose` では、コンポーズ可能な関数を新しいデータで再度呼び出します。すると、関数が再コンポーズされます。関数によって出力されるウィジェットは、必要に応じて新しいデータで再描画されます。`Compose` フレームワークでは、変更されたコンポーネントのみをインテリジェントに再コンポーズできます。

再コンポーズは可能な限りスキップするのは`宣言型UI`モデルだと思います。これにより、アプリのUIレンダリング性能やデータ交換効率が大幅に上がります。さらに更新必要なところだけ自動で再コンポーズする方式では、従来命令型の`Set`、`Show`、`hidden`などの手動更新手法に比べてUI管理漏れによる表示間違いが削減されます。これは我々開発者にとっては魅力的です。:hugging:

>コンポーズ可能な関数でデータが必要な場合は、データ用のパラメータを定義する必要があります。すると、コストの高い処理をコンポーズ外の別のスレッドに移動でき、`mutableStateOf` または `LiveData` を使用してデータを `Compose` に渡せます。

例えば、ログインページのアカウント入力にある値「idValue」は最初に声明したときは`mutableStateOf`を使用しています。それにより、自動サブスクリプションが適用されます。

```kotlin
var idValue by remember { mutableStateOf(TextFieldValue("")) }
```
「idValue」の値が変化すると、アカウントIDの表示が追従します。


##4、運用性
真新しいアプリをビルドする場合、`Compose` を使用して UI 全体を実装することはもちろん、`Compose` と既存の UI デザインの実装を組み合わせることも可能です。

>ビューベースの UI と `Compose` を組み合わせる方法は主に 2 つあります。
・`Compose` の要素を既存の UI に追加するには、まったく新しい `Compose` ベースの画面を作成するか、既存のアクティビティ、フラグメントまたはビュー レイアウトに `Compose` 要素を追加します。
・ビューベースの UI 要素をコンポーズ可能な関数に追加できます。これにより、Android ビューを `Compose` ベースのデザインに追加できます。

`Compose` ベースのログインページを`MainActivity`にセットします。

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        setContent {
            LoginPage()
        }

    }

}
```
アプリをビルドしたら、`MainActivity`画面に`Compose` ベースのログインページが表示されます。操作性も通常のビューベースの UI と違いがありません。

![loginCompose.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678085/5257566c-e01f-d174-d873-b7ba33e01dfc.gif)


##5、終わり

Androidネイティブアプリの開発者にとっては`Jetpack Compose`は非常に重要かつ示唆的なものでしょう。`Flutter`、`SwiftUI`、`react`、`Jetpack Compose`など、業界全体が`宣言型 UI` モデルに移行しつつあり、その分野の`宣言型 UI`構築手法を理解、学習しないとついて行けなくなるかもしれません。