Jetpack ComposeのButton達を比較してみよう

みなさん`Jetpack Compose`使ってますでしょうか 導入しているrepositoryも多くなってきましたね
Composeで遊んでいる中で、<b>Button</b>とつくcomponentが多くどれを使うか迷うこともあったのでまとめることにします
xmlでButtonを使っていて軽くcomposeを触り始めたくらいの人に参考になれば嬉しいです

# 環境

```
kotlin: 1.5.10
compose: 1.0.0-beta09
```

:warning: 投稿時点では`1.0.0-beta09`のため参考にする時点では変更が入っている恐れがあります:warning: 

# Buttons

## Button
<img src="https://developer.android.com/images/reference/androidx/compose/material/contained-button.png" width="480">

### Arguments

ごく普通のボタンです 最初からMaterial仕様なのがいいですね
argumentから以下が設定可能です

- onClick
- enabled
- interactionSource
- elevation
- shape
- border
- colors (enabled/disable等状態に応じてbackgroundなど色を変える)
- contentPadding

<details><summary><font color="blue">展開するとButtonのコードが見れます</font></summary><div>

```kotlin
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
    Surface(
        modifier = modifier,
        shape = shape,
        color = colors.backgroundColor(enabled).value,
        contentColor = contentColor.copy(alpha = 1f),
        border = border,
        elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
        onClick = onClick,
        enabled = enabled,
        role = Role.Button,
        interactionSource = interactionSource,
        indication = rememberRipple()
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                Row(
                    Modifier
                        .defaultMinSize(
                            minWidth = ButtonDefaults.MinWidth,
                            minHeight = ButtonDefaults.MinHeight
                        )
                        .padding(contentPadding),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    content = content
                )
            }
        }
    }
}
```

</div></details>

コードを見るとscopeが`Row`のため`Icon`と`Text`を並べてxmlでの`drawableStart`的な挙動をさせることができます

### ButtonDefaults

実際すべて指定しなくてもdefaultを用意してくれています、それがButtonDefaultsです
(わざわざstyle/attrをjumpしてみなくてもよくなったので可読性上がったなと個人的には思ってます)

<details><summary><font color="blue">展開するとButtonDefaultsのコードの1部が見れます</font></summary><div>

```kotlin
object ButtonDefaults {
    private val ButtonHorizontalPadding = 16.dp
    private val ButtonVerticalPadding = 8.dp

    /**
     * The default content padding used by [Button]
     */
    val ContentPadding = PaddingValues(
        start = ButtonHorizontalPadding,
        top = ButtonVerticalPadding,
        end = ButtonHorizontalPadding,
        bottom = ButtonVerticalPadding
    )

    /**
     * The default min width applied for the [Button].
     * Note that you can override it by applying Modifier.widthIn directly on [Button].
     */
    val MinWidth = 64.dp

    /**
     * The default min width applied for the [Button].
     * Note that you can override it by applying Modifier.heightIn directly on [Button].
     */
    val MinHeight = 36.dp

    /**
     * The default size of the icon when used inside a [Button].
     *
     * @sample androidx.compose.material.samples.ButtonWithIconSample
     */
    val IconSize = 18.dp

    /**
     * The default size of the spacing between an icon and a text when they used inside a [Button].
     *
     * @sample androidx.compose.material.samples.ButtonWithIconSample
     */
    val IconSpacing = 8.dp
```
</div></details>

## TextButton

<img src="https://developer.android.com/images/reference/androidx/compose/material/text-button.png" width="480" >

<details><summary><font color="blue">展開するとTextButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun TextButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = null,
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.textButtonColors(),
    contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
    content: @Composable RowScope.() -> Unit
) = Button(
    onClick = onClick,
    modifier = modifier,
    enabled = enabled,
    interactionSource = interactionSource,
    elevation = elevation,
    shape = shape,
    border = border,
    colors = colors,
    contentPadding = contentPadding,
    content = content
)
```

</div></details>

Buttonと違うのは、

- colorsがTextButton専用
- elevationがnull
- contentPaddingがTextButton専用

くらいでしょうか　公式ドキュメントには

> Text buttons are typically used for less-pronounced actions, including those located in dialogs and cards. In cards, text buttons help maintain an emphasis on card content.

とあるので、主張はしないけれどButtonとして役割を持たせたい時に使えますね

## OutlinedButton

<img src="https://developer.android.com/images/reference/androidx/compose/material/outlined-button.png" width="480" >

<details><summary><font color="blue">展開するとOutlinedButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun OutlinedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = null,
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = ButtonDefaults.outlinedBorder,
    colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) = Button(
    onClick = onClick,
    modifier = modifier,
    enabled = enabled,
    interactionSource = interactionSource,
    elevation = elevation,
    shape = shape,
    border = border,
    colors = colors,
    contentPadding = contentPadding,
    content = content
)
```

</div></details>

Buttonと違うのは、

- colorsがOutlinedButton専用
- borderがOutlinedButton専用
- elevationがnull

くらいでしょうか 

> Outlined buttons are medium-emphasis buttons. They contain actions that are important, but aren't the primary action in an app.

とあるのでButtonよりは目立たせたくないものに使いましょう

## IconButton

文字通りIconに使われるButtonということですね
ただその場合ClickableにしたIconとは何が違うのでしょうか

コード及びdocを見てみると

<details><summary><font color="blue">展開するとIconButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun IconButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable () -> Unit
) {
    Box(
        modifier = modifier
            .clickable(
                onClick = onClick,
                enabled = enabled,
                role = Role.Button,
                interactionSource = interactionSource,
                indication = rememberRipple(bounded = false, radius = RippleRadius)
            )
            .then(IconButtonSizeModifier),
        contentAlignment = Alignment.Center
    ) {
        val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
        CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
    }
}
```

</div></details>

docにはさらに詳細な記述があります

>IconButton is a clickable icon, used to represent actions. An IconButton has an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines. content is centered inside the IconButton.
This component is typically used inside an App Bar for the navigation icon / actions. See App Bar documentation for samples of this.
content should typically be an Icon, using an icon from androidx.compose.material.icons.Icons. If using a custom icon, note that the typical size for the internal icon is 24 x 24 dp.

今回は`Button`を使ってはいませんね
`size`が`48dp`で主に`Appbar(Toolbarなど)`, `Navigation`に使うアイコンということですね
`Box`など使いかぶせてサイズ自体を変えることはできますが中のIconButton自体を変える想定はしていないように見えます、rippleもちょうどアイコン分設定されています (`private val RippleRadius = 24.dp`)
そのためカスタムして使うというよりはこのユースケース限定で使うという方がよさそうですね

## IconToggleButton

文字通りIconButtonにToggleが加わったということですね
argumentに`checked, onCheckChange`があるのが特徴です

<details><summary><font color="blue">展開するとIconButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun IconToggleButton(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable () -> Unit
) {
    Box(
        modifier = modifier.toggleable(
            value = checked,
            onValueChange = onCheckedChange,
            enabled = enabled,
            role = Role.Checkbox,
            interactionSource = interactionSource,
            indication = rememberRipple(bounded = false, radius = RippleRadius)
        ).then(IconButtonSizeModifier),
        contentAlignment = Alignment.Center
    ) {
        val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
        CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
    }
}
```

</div></details>

docをみると

> An IconButton with two states, for icons that can be toggled 'on' and 'off', such as a bookmark icon, or a navigation icon that opens a drawer.

とあるので状態(ON/OFF)に応じてIconButtonを変更したい場合に大きく役立ちます
いいねボタンなどがいい例ですね

公式にあるSnippetがとてもわかりやすいです

```kotlin
import androidx.compose.animation.animateColorAsState
import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color

var checked by remember { mutableStateOf(false) }

IconToggleButton(checked = checked, onCheckedChange = { checked = it }) {
    val tint by animateColorAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
    Icon(Icons.Filled.Favorite, contentDescription = "Localized description", tint = tint)
}
```

## FloatingActionButton

<img src="https://developer.android.com/images/reference/androidx/compose/material/floating-action-button.png" width="480">

みなさんご存知FABです 特にxml時代から変わったところもありませんね
`Button`とは違い`enabled`はなく、また`colors`も`background/content`のcolorをそれぞれ指定する形に変わっています
`elevation`は専用のものがdefaultで指定されています

`content`部分には`Text`も使えますが、大体`Icon`を指定することになるでしょう
ちなみに`size`のdefaultは`56dp`です


<details><summary><font color="blue">展開するとFloatingActionButtonのコードが見れます</font></summary><div>

```kotlin
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FloatingActionButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
    backgroundColor: Color = MaterialTheme.colors.secondary,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
    content: @Composable () -> Unit
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = backgroundColor,
        contentColor = contentColor,
        elevation = elevation.elevation(interactionSource).value,
        onClick = onClick,
        role = Role.Button,
        interactionSource = interactionSource,
        indication = rememberRipple()
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(MaterialTheme.typography.button) {
                Box(
                    modifier = Modifier
                        .defaultMinSize(minWidth = FabSize, minHeight = FabSize),
                    contentAlignment = Alignment.Center
                ) { content() }
            }
        }
    }
}
```

</div></details>

## ExtendedFloatingActionButton

<img src="https://developer.android.com/images/reference/androidx/compose/material/extended-floating-action-button.png">

普通のFABとは違い

- `minWidth/Height`は`48dp` (FABは`56dp`)
- 横長も想定 (Extended)
- `Text`も想定されている
- `Icon`はnullableのため`Text`だけも可能

のためIconやTextを使ってメインのアクションをさせたいときに使いましょう

<details><summary><font color="blue">展開するとExtendedFloatingActionButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun ExtendedFloatingActionButton(
    text: @Composable () -> Unit,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    icon: @Composable (() -> Unit)? = null,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
    backgroundColor: Color = MaterialTheme.colors.secondary,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation()
) {
    FloatingActionButton(
        modifier = modifier.sizeIn(
            minWidth = ExtendedFabSize,
            minHeight = ExtendedFabSize
        ),
        onClick = onClick,
        interactionSource = interactionSource,
        shape = shape,
        backgroundColor = backgroundColor,
        contentColor = contentColor,
        elevation = elevation
    ) {
        Box(
            modifier = Modifier.padding(
                start = ExtendedFabTextPadding,
                end = ExtendedFabTextPadding
            ),
            contentAlignment = Alignment.Center
        ) {
            if (icon == null) {
                text()
            } else {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    icon()
                    Spacer(Modifier.width(ExtendedFabIconPadding))
                    text()
                }
            }
        }
    }
}
```

</div></details>

## RadioButton

<img src="https://developer.android.com/images/reference/androidx/compose/material/radio-buttons.png" width="480" >

おなじみRadioButtonです
コードを見てみるとdotとcanvasでシンプルに構成されているのが面白いですね

<details><summary><font color="blue">展開するとExtendedFloatingActionButtonのコードが見れます</font></summary><div>

```kotlin
@Composable
fun RadioButton(
    selected: Boolean,
    onClick: (() -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: RadioButtonColors = RadioButtonDefaults.colors()
) {
    val dotRadius by animateDpAsState(
        targetValue = if (selected) RadioButtonDotSize / 2 else 0.dp,
        animationSpec = tween(durationMillis = RadioAnimationDuration)
    )
    val radioColor by colors.radioColor(enabled, selected)
    val selectableModifier =
        if (onClick != null) {
            Modifier.selectable(
                selected = selected,
                onClick = onClick,
                enabled = enabled,
                role = Role.RadioButton,
                interactionSource = interactionSource,
                indication = rememberRipple(
                    bounded = false,
                    radius = RadioButtonRippleRadius
                )
            )
        } else {
            Modifier
        }
    Canvas(
        modifier
            .then(selectableModifier)
            .wrapContentSize(Alignment.Center)
            .padding(RadioButtonPadding)
            .requiredSize(RadioButtonSize)
    ) {
        drawRadio(radioColor, dotRadius)
    }
}
```

</div></details>


公式にあるこのsnippetが面白いのですが、Composeではこのように`selected`と`onClick`にて`state`をわざわざsetすることがほとんどです
これはComposeが生まれた思想の中に、これまでのAndroidのView自体が中にstateを持ってしまっていてそれによってViewを変えていることが多く(Spinner/CheckBoxなど)、現状のMVVMはじめAndroidの設計と噛み合わないところも出てきているため今後はこうなっていくのが一般的なようですね

```kotlin
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.RadioButton
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

// We have two radio buttons and only one can be selected
var state by remember { mutableStateOf(true) }
// Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior
Row(Modifier.selectableGroup()) {
    RadioButton(
        selected = state,
        onClick = { state = true }
    )
    RadioButton(
        selected = !state,
        onClick = { state = false }
    )
}
```

なぜこのような思想にしているのか、これまでのAndroid ViewとJetPack Composeは何が違うのかについては

この動画がとても面白いので見ることをお勧めします
<iframe width="560" height="315" src="https://www.youtube.com/embed/VsStyq4Lzxo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# まとめ
今回はJetPack ComposeにあるButtonの違いをコード交えながら見てきました
個人的にはIconButtonが印象的です
要所要所で使い分けて良いCompose Lifeを送りましょう

# Ref

- [Mateial Design - Button](https://material.io/components/buttons/android)
- [Button](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#Button(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.ButtonElevation,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.BorderStroke,androidx.compose.material.ButtonColors,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1))
- [TextButton](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#TextButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.ButtonElevation,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.BorderStroke,androidx.compose.material.ButtonColors,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1))
- [OutlinedButton](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#OutlinedButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.ButtonElevation,androidx.compose.ui.graphics.Shape,androidx.compose.foundation.BorderStroke,androidx.compose.material.ButtonColors,androidx.compose.foundation.layout.PaddingValues,kotlin.Function1))
- [IconButton](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#IconButton(kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0))
- [IconToggleButton](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#IconToggleButton(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function0))
- [FAB](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#FloatingActionButton(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.material.FloatingActionButtonElevation,kotlin.Function0))
- [ExtendedFAB](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ExtendedFloatingActionButton(kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.material.FloatingActionButtonElevation))
- [RadioButton](https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#RadioButton(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.RadioButtonColors))