Material 3 Expressive実装例 : App bar

Material 3 Expressive の実装例の紹介シリーズです。
今回は App bar 系を紹介します。
(`androidx.compose.material3:material3:1.4.0-alpha15` 時点での内容になります。)

## Material 3 Expressive での変更点

- Top app bar から App bar にリネーム
- Search app bar の追加
- Material 3 で追加された Medium と Large の `TopAppBar` が廃止され、新たに Medium flexible と Large flexible の App bar が追加


https://m3.material.io/components/app-bars/overview

## 実装
### Small app bar

`TopAppBar` に title や subtitle、navigationIcon、　actions 等が Slot として用意されています。

```kotlin
TopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    }
)

// with subtitle
TopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    },
    subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
)

// with actions
TopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    },
    subtitle = { 
        Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) 
    },
    navigationIcon = {
        IconButton(onClick = {}) {
            Icon(...)
        }
    },
    actions = {
        IconButton(onClick = {}) {
            Icon(...)
        }
    }
)

// Center aligned
CenterAlignedTopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    },
    navigationIcon = {
        IconButton(onClick = {}) { ... }
    },
    actions = {
        IconButton(onClick = {}) { ... }
    }
)

// Center aligned with subtitle
TopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    },
    subtitle = { 
        Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) 
    },
    titleHorizontalAlignment = Alignment.CenterHorizontally,
    navigationIcon = {
        IconButton(onClick = {}) { ... }
    },
    actions = {
        IconButton(onClick = {}) { ... }
    }
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/702f6eb8-cba1-4d11-a6b1-fe8e9e9623f2.png" width="300px" />





### Medium flexible app bar

`MediumFlexibleTopAppBar` を使います。基本的には `TopAppBar` の使い方と同じになっています。

```kotlin
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
    modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
    topBar = {
        MediumFlexibleTopAppBar(
            title = {
                Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
            },
            subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
            navigationIcon = { ... },
            actions = { ... },
            scrollBehavior = scrollBehavior
        )
    },
) { ... }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/d4a5335a-43d2-468e-a2c5-51ac8cbc02e0.gif" width="300px" />



### Large flexible app bar

`LargeFlexibleTopAppBar` を使います。こちらも基本的には `TopAppBar` の使い方と同じになっています。

```kotlin
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
    modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
    topBar = {
        LargeFlexibleTopAppBar(
            title = {
                Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
            },
            subtitle = { Text("Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis) },
            navigationIcon = { ... },
            actions = { ... },
            scrollBehavior = scrollBehavior
        )
    },
) { ... }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/686b64af-5453-4df7-ad4d-e449f36369f9.gif" width="300px" />


### App bar + Adaptive actions

App bar の action 部分に `AppBarRow` / `AppBarColumn` を使うことで、画面サイズで action のアイテムを自動で省略する実装ができます。


:::note warn
App bar の action のアイテム数は Compact の時は最大 2 つまでに留め、それ以上の場合は Toolbar を検討しましょう。
Toolbar の配置が困難な場合に `AppBarRow` / `AppBarColumn` で省略するようにしましょう。
https://m3.material.io/components/app-bars/guidelines#224e8ca0-4177-45f4-9f4f-eef1fbb0e4b2
:::

```kotlin
val sizeClass = currentWindowAdaptiveInfo().windowSizeClass
val maxItemCount = if (sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) {
    5
} else {
    3
}
TopAppBar(
    title = {
        Text("Title", maxLines = 1, overflow = TextOverflow.Ellipsis)
    },
    actions = {
        AppBarRow(
            maxItemCount = maxItemCount,
            overflowIndicator = {
                IconButton(onClick = { it.show() }) {
                    Icon(
                        painter = painterResource(R.drawable.ic_more),
                        contentDescription = "Other options"
                    )
                }
            }
        ) {
            clickableItem(
                onClick = {},
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_add),
                        contentDescription = null
                    )
                },
                label = "Create"
            )
    
            clickableItem(
                onClick = {},
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_star),
                        contentDescription = null
                    )
                },
                label = "Favorites"
            )
    
            clickableItem(
                onClick = {},
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_search),
                        contentDescription = null
                    )
                },
                label = "Search"
            )
    
            clickableItem(
                onClick = {},
                icon = {
                    Icon(
                        painter = painterResource(R.drawable.ic_settings),
                        contentDescription = null
                    )
                },
                label = "Settings"
            )
        }
    }
}
```

|Compact|Medium|
|:--:|:--:|
|![Screenshot_20250609_101107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/1d5ce6ad-128a-4b61-85fe-b85d47f1edc9.png)|![Screenshot_20250609_101116.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/0e321d1c-b744-4cbd-a12a-18368d8f60db.png) |


### Search app bar

ガイドラインに記載されているような見た目の Search app bar の標準の実装はなく、現時点では `SearchBar` をカスタムする形になります。
追加され次第追記します。


### `TwoRowsTopAppBar` によるカスタマイズ

`MediumFlexibleTopAppBar` と `LargeFlexibleTopAppBar` は `TwoRowsTopAppBar` の上に実装されており、元となる `TwoRowsTopAppBar` を使うことで自由な Flexible app bar を実装することができます。
引数で `collapsedHeight` と `expandedHeight` のサイズを指定すれば固定の高さに、未指定であればコンテンツの高さになります。

```kotlin
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
TwoRowsTopAppBar(
    title = { expanded ->
        if (expanded) {
            Text("Expanded TopAppBar\nWith two lines")
        } else {
            Text("Collapsed TopAppBar", maxLines = 1, overflow = TextOverflow.Ellipsis)
        }
    },
    subtitle = { expanded ->
        if (expanded) {
            Text(
                "Expanded Subtitle",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.padding(bottom = 24.dp)
            )
        } else {
            Text("Collapsed Subtitle", maxLines = 1, overflow = TextOverflow.Ellipsis)
        }
    },
    navigationIcon = { ... },
    scrollBehavior = scrollBehavior
)    
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/8e3513cc-0f93-4b0a-be6f-0617efe4e2c0.gif" width="300px" />