Jetpack Compose: BottomNavigationコンポーザブルを使う


BottomNavigationコンポーザブルはandroidx.compose.materialパッケージに含まれており、アプリ内を移動するナビゲーションを担当しています。画面の下部にアプリの主要な目的地を表示し、タップで遷移します。通常、表示する目的地は3~5つでアイコンとテキストで構成します。

BottomNavigationコンポーザブル

BottomNavigationコンポーザブルはRowScope.BottomNavigationItemコンポーザブルと一緒に利用します。マテリアルデザインガイドラインには目的地となる遷移先機能の数、切替時の状態保持、アイコンの選択状態など、BottomNavigationのプラクティスがまとまっています。

BottomNavigationを表示する

BottomNavigationコンポーザブルの表示例

BottomNavigationコンポーザブルに含まれるコンテンツには複数のBottomNavigationItemが含まれている前提があります。BottomNavigationItemは移動したい目的地ごとに用意します。次のサンプルは公式リファレンスで用意されているもので目的地にSongs、Artists、Playlistsの3つを表示します。

@Composable
fun SimpleBottomNavigation() {
    var selectedItem = remember { mutableStateOf(0) }
    val items = listOf("Songs", "Artists", "Playlists")

    BottomNavigation {
        items.forEachIndexed { index, item ->
            BottomNavigationItem(
                icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
                label = { Text(item) },
                selected = selectedItem.value == index,
                onClick = { selectedItem.value = index }
            )
        }
    }
}

ソースコードは最低限動作が確認できる構成です。items.forEachIndexedで3つのBottomNavigationItemを作っています。同じアイコンを使いまわしているので実用性はありませんが複数の目的地を設定したBottomNavigationコンポーザブルかつItemの挙動もそれっぽくなっていることがわかります。

BottomNavigationコンポーザブルの引数

ButtomNavigationコンポーザブルの引数contentには複数のBottomNavigationItemが含まれている必要があります。

@Composable
fun BottomNavigation(
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = BottomNavigationDefaults.Elevation,
    content: @Composable RowScope.() -> Unit
) {
  • modifier:レイアウトへ反映するmodifier
  • backgroundColor:BottomNavigationの背景色を指定
  • contentColor:コンテンツ色を指定(デフォルトでは背景色と対となるonBackgroundを利用)
  • elevation:BottomNavigationのエレベーション(マテリアルデザインでは標準8.dp)
  • content:BottomNavigationItemを複数含んだRowScope.()->Unit

もうちょっと実践的に表示する

4つのBottomNavigationItemをもつ例

次のサンプルコードはBottomNavigationコンポーザブルの目的地を定義して(Item型)、ある程度見やすくしたものです。移動先の名前とアイコンの表示順序をきめてitemsにまとめてあります。

sealed class Item(var dist: String, var icon: ImageVector) {
    object Home : Item("Home", Icons.Filled.Home)
    object Email : Item("Email", Icons.Filled.Email)
    object Stars : Item("Stars", Icons.Filled.Star)
    object Lists : Item("Lists", Icons.Filled.List)
}

@Composable
fun MultipleItemsBottomNavigation() {
    var selectedItem = remember { mutableStateOf(0) }
    val items = listOf(Item.Home, Item.Email, Item.Stars, Item.Lists)

    BottomNavigation {
        items.forEachIndexed { index, item ->
            BottomNavigationItem(
                icon = { Icon(item.icon, contentDescription = item.dist) },
                label = { Text(item.dist) },
                alwaysShowLabel = false, // 4つ以上のItemのとき
                selected = selectedItem.value == index,
                onClick = { selectedItem.value = index }
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun BottomNavigationPreview2() {
    MyApplicationTheme {
        MultipleItemsBottomNavigation()
    }
}

BottomNavigationItemを適切に設定する

BottomNavigationItemの推奨構成は、BottomNavigation内にあるアイテム数によって違っており、アクティブなアイテム以外のテキスト表示が変わります。移動先が3つであればアイコンとテキストラベルの両方を常時表示し、4つであれば非アクティブなアイコンのテキストは推奨(非表示でもよい)、5つであれば非アクティブなアイコンについてはテキストはスペースが許せば表示、とアイテムの数に応じて優先度が低くなっています(マテリアルデザインガイドラインのRepresenting destinationsより)。

アクティブなアイテムとそれ以外(アイコンが3つのとき)
アクティブなアイテムとそれ以外(アイコンが4つのとき)

この挙動はBottomNavigationItemコンポーザブルの引数alwaysShowLabelで制御できます(アイコンが3つまでであればtrue(デフォルト値)、4つまたは5つであればfalseを検討できる)。

@Composable
fun RowScope.BottomNavigationItem(
    selected: Boolean,
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    label: @Composable (() -> Unit)? = null,
    alwaysShowLabel: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    selectedContentColor: Color = LocalContentColor.current,
    unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
) 
  • selected:選択中のアイテムを示す
  • onClick:アイテムがクリックされたときに実行するコールバック
  • icon:アイテムに使うアイコン(Iconコンポーザブルを指定することが多い)
  • modifier:レイアウトへ反映するmodifier
  • enabled:アイテムの有効状態。無効のとき、クリック不可かつ無効だとわかる見た目になる
  • label:テキスト(オプション)
  • alwaysShowLabel:テキストラベルを常時表示する
  • interactionSource:インタラクションに合わせてスタイルや振る舞いを変えたいときはソースとなるストリームを設定する
  • selectedContentColor:選択時のコンテンツ色(アイコンおよびリップルに適用)
  • unselectedContentColor:非選択状態でのコンテンツ色

BottomNavigationによる機能・画面の遷移はプラットフォームごとにデフォルトの動作は異なっています。Androidの場合、目的地のトップレベルスクリーンに移動し、スクロール位置や選択中のタブ、検索文言等のインライン要素など画面状態・ユーザー操作はリセットしてください(iOSとは違う動作です)。

ただし、このデフォルト挙動がいつでも正解というわけでありません。頻繁に切り替える場合などは目的地を移動して戻ってきた場合、状態を覚えている動作が好ましいでしょう。操作感やアプリの要件に応じてプラットフォームの動作を変更できます。

またBottomNavigationコンポーザブルでは(アプリの要件で要望が多い)バッジ表示もIcon部分を実装することで対応できます。未読バッジや吹き出し表示などを使いたい場合は独自にBadgeIconコンポーザブルを作ってしまえばいいというのは実にComposeらしいアプローチです(Android Viewではライブラリに手を入れないといけなくなり、どうしても不便でした)。

おまけ:レシーバーつき関数リテラル

今回紹介したBottomNavigationコンポーザブルでは表示対象の引数contentをRowScope.()->Unitとして定義しています。このRowScope.()->Unitはレシーバーつき関数リテラルと呼び、Kotlinの言語仕様ではFunction literals with receiverと定義しています。慣れてないと読みにくいのでここで解説しておきます。

レシーバーつき関数リテラルという言語機能を使うと、関数リテラルの内部でレシーバオブジェクトのメソッドを使えるようになります。特別な修飾子などは必要ありません。今回のケースではcontentの実体となる() -> Unitという関数リテラル(ラムダや無名関数のことを関数リテラルと呼びます)のなかでレシーバーオブジェクトRowScope型(実際のRowScopeはインターフェイスですが)がもつ関数やメンバなどにアクセスできる便利な表現です。

sum : Int.(other: Int) -> Int // sum型の定義。sumはレシーバーつき関数リテラル。
// レシーバーつき関数リテラルを書くときと利用するとき
// 書くとき:sumの実体{ ... }ではInt.()と定義しているのでIntのメソッドやメンバーにアクセスできる。
// 利用するとき:レシーバーつき関数リテラルsumはIntの拡張関数のように呼び出せる
1.sum(2)

なぜBottomNavigationで( … content: @Composable RowScope.() -> Unit ) というややこしい引数にしているのか、その理由はレイアウト処理の都合です。コンテンツ部分はアプリ開発者に定義してほしいけど配置とかはライブラリ開発者側でやりたいなぁ。という設計思想で便利だからです。

BottomNavigationコンポーザブル内部でMyRowScope.content()と呼び出せば、レイアウトに必要な周辺情報をもったMyRowScopeの実装はライブラリ提供者が予め準備しておき、contentの実装はアプリ開発者が担当するという分離が可能です(ここで出てきたMyRowScopeはBottomNavigation独自のRowScope実装があるよというダミーの表現です。実際の名前ではありません)。

アプリ開発者の立場ではレイアウトの細かい話題はBottomNavigationに任せてしまい、contentの実装だけすればいいので楽ですね。

BottomNavigationコンポーザブルを使う際に最後尾の引数はラムダとして書ける…というKotlinのルールと組み合わせると次のようにレシーバーつき関数リテラルを利用できます(Android Studioがthis: RowSpaceと教えてくれているはずです)。

BottomNavigation { // this: RowSpace
    // このラムダはcontentにわたす @Composable RowScope.() -> Unit として解釈される
    // RowScope型の関数やメンバにアクセスできる
}

レシーバーつき関数リテラルという言語表現に含まれている「レシーバー」とは関数リテラルの実装を受け取るレシーバーがいるよ(RowScopeに対する処理contentを書いたので、それを受け取って実行するRowScope型のインスタンスがレシーバーだよ)という意味でのレシーバー(受信者)です。

今回の記事はこれでおしまいです。お疲れさまでした。

Add a Comment

Your email address will not be published. Required fields are marked *