【Android】Jetpack Composeの基本

Androidアプリ開発でのUI作成は従来のXMLレイアウトからJetpack Composeと呼ばれるUI作成が主流になりました。Jetpack Composeでは宣言的にUIを構築することができ、データが変わると自動的に関連するViewのUIや処理がリアクションしてくれます。

どのようにしてJetpack Composeで宣言するのか

Androidアプリのプロジェクトを作成した場合にMainActivityにJetpack ComposeでUIを構築するためのコードが定義されています。

@Composableとは

Composableアノテーションをつけた関数はUIを宣言的に記述できるようになります。Composable関数には他のComposable関数からしか呼べないという制約があります。

setContent

Composable関数の呼び出しのルートとなります。

Kotlin
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
)

定義をみると第二引数はクロージャになっていて型は@Composable () -> Unitです。クロージャ内で@Composableを利用して記述することでCompose ランタイムにUIツリーの登録を行います。そのため戻り値が必要ないのでUnitとなっています。

KotlinではSwiftのようにViewを返したりするのではなく、関数呼び出してUIを構築できるように登録します。差分適用においてもComposable関数を呼び出してUIを更新します。

theme

Composable関数として呼び出されているプJetpackComposeSampleThemeはアプリ全体のマテリアルテーマとしてComposable関数内部のButtonやTextFieldなどが参照します。アプリの最上位であるMainActivityにのみ記述するべきものです。

Scaffold

Jetpack ComposeのUI設計における画面の骨組みを提供するComposable関数です。マテリアルデザインでよく使うTobBar、BottomBarなどを統一的なレイアウトで配置してくれるコンポーネントです。

Scaffoldが返すinnerPaddingは余白情報で、自身のレイアウトにはこの余白を適用して配置が被らないようにする必要があります。

Modifier

ModifierはUI要素に対して親レイアウト内での配置、表示、動作を指示します。Paddingなどを設定したい場合はModifier.paddingを利用します。そのほかにも位置揃え、アニメーション化、配置、変換、クリック可能、スクロール可能にするなど様々なものがあります。

Composableの再利用

複数の画面で同じUIを構築する場合は再利用可能な小さいコンポーネントを作成することで別の画面でも利用することが可能となります。無駄なネストが増えないため見やすくもなります。

ベストプラクティスとしては空のModifierを設定しておくことで後から柔軟にレイアウトを微調整できるとのことです。

列と行を作成する

UIの配置としてはColumn(縦並び)、Row(横並び)、Box(重ね合わせ)の3つの配置方法があります。ColumnやRowの内部ではfor文を利用してループでリストを表示させることができます。

Composeの状態

remember

リストにボタンがある場合の選択状態を管理するためにはmutableStateOfを利用します。しかし再コンポジションのたびに値が初期化されるため、rememberを利用して可変状態を記憶します。

状態ホイスティング

状態ホイスティングとは状態を下位Composableに閉じ込めずに、上位Composableへ引き上げ(Hoist)て管理する設計パターンです。

例えば下位のComposableが状態を保持していると、上位のComposableは値を参照することができず、外部から状態を制御することもできなくなります。

そこで、状態を変更するクロージャを下位Composableに渡して、下位Composableはクロージャを変更して状態の変更を行います。

こうすることで状態は親側で保持することができ、変更のイベントも下位から上位へと伝わります。また下位のComposableが特定の状態構造に依存していないため、状態を外部から注入できるのでテストがしやすくなります。

Kotlin
@Composable
fun ParentScreen() {
    var isOn by remember { mutableStateOf(false) }

    ToggleButton(
        isOn = isOn,
        onToggleChange = { isOn = it }
    )
}

@Composable
fun ToggleButton(
    isOn: Boolean,
    onToggleChange: (Boolean) -> Unit
) {
    Button(onClick = { onToggleChange(!isOn) }) {
        Text(if (isOn) "ON" else "OFF")
    }
}