Jetpack Compose アーキテクチャのレイヤリング

このページでは、Jetpack Compose を構成するアーキテクチャ レイヤの概要と、このデザインを決定付けるコア原則について説明します。

Jetpack Compose は単一のモノリシック プロジェクトではありません。完全なスタックを形成するために組み合わされた複数のモジュールから作成されています。Jetpack Compose を構成するさまざまなモジュールについて理解することで、次のことが可能になります。

  • 適切なレベルの抽象化を使用して、アプリまたはライブラリをビルドする
  • より低いレベルに「降格」させて、きめ細かい制御や高度なカスタマイズを行えるケースを知る
  • 依存関係を最小限に抑える

レイヤ

Jetpack Compose の主なレイヤは次のとおりです。

図 1. Jetpack Compose の主なレイヤ。

各レイヤは下位レベルをベースにビルドされ、機能を組み合わせて上位レベルのコンポーネントが作成されます。各レイヤは下位レイヤの公開 API をベースにビルドされ、モジュールの境界を検証することができ、必要に応じて任意のレイヤを置き換えることもできます。これらのレイヤを下から順に見てみましょう。

ランタイム
このモジュールは、基本的な Compose ランタイム、たとえば、remembermutableStateOf@Composable アノテーション、SideEffect を提供します。Compose のツリー管理機能のみが必要で、UI が必要でない場合は、このレイヤをベースに直接ビルドすることを検討してください。
UI
UI レイヤは、複数のモジュール(ui-textui-graphicsui-tooling など)で構成されています。これらのモジュールは、LayoutNodeModifier、入力ハンドラ、カスタム レイアウト、描画など、基本的な UI ツールキットを実装しています。UI ツールキットの基本的なコンセプトのみが必要な場合は、このレイヤをベースにビルドすることを検討してください。
基盤
このモジュールは、デザイン システムに依存しないビルディング ブロック、たとえば、RowColumnLazyColumn、特定のジェスチャーの認識などを Compose UI に提供します。独自のデザイン システムをビルドするには、基盤レイヤをベースにしたビルドを検討してください。
マテリアル
このモジュールは、マテリアル デザイン システムの実装、テーマ設定システム、スタイル付きコンポーネント、リップル表示、アイコンを Compose UI に提供します。アプリでマテリアル デザインを使用する際には、このレイヤをベースにビルドします。

設計の原則

Jetpack Compose の基本原則は、少数のモノリシック コンポーネントを提供することではなく、一緒に組み立てる(または構成する)ことができる、小さい集中型機能を提供することです。このアプローチには多くのメリットがあります。

制御

コンポーネントのレベルが高いほど、自動で行われる操作は増えますが、デベロッパーが直接制御できる操作は制限されます。きめ細かい制御が必要な場合は、「降格」させて下位レベルのコンポーネントを使用することが可能です。

たとえば、コンポーネントの色にアニメーション効果を加える場合は、animateColorAsState API を使用します。

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

ただし、コンポーネントが常にグレーで始める必要がある場合は、この API では行えません。代わりに、プルダウンから下位レベルの Animatable API を使用できます。

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

上位レベルの animateColorAsState API 自体は、下位レベルの Animatable API をベースにビルドされています。下位レベルの API を使用すると操作が煩雑になりますが、より細かく制御できます。ニーズに適した抽象化レベルを選択してください。

カスタマイズ

小さなビルディング ブロックから上位レベルのコンポーネントを組み立てることで、必要に応じてコンポーネントを簡単にカスタマイズできるようになります。たとえば、マテリアル レイヤで提供される Button実装を考えてみましょう。

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Button は、次の 4 つのコンポーネントで構成されています。

  1. マテリアル Surface: 背景、形状、クリック処理などを提供します。

  2. CompositionLocalProvider: ボタンで有効と無効を切り替えて、コンテンツのアルファ版を変更します。

  3. ProvideTextStyle: 使用するデフォルトのテキスト スタイルを設定します。

  4. Row: ボタンのコンテンツ用のデフォルトのレイアウト ポリシーを提供します。

構造をわかりやすくするため、一部のパラメータとコメントを省略していますが、これら 4 つのコンポーネントを組み合わせてボタンを実装するだけなので、コンポーネント全体のコードは約 40 行しかありません。Button のようなコンポーネントは、公開するパラメータを自身で決定し、一般的なカスタマイズの実装と、コンポーネントの利便性に影響を及ぼすパラメータの爆発的な増加とのバランスをとっています。たとえば、マテリアル コンポーネントはマテリアル デザイン システムで指定されたカスタマイズに対応しているため、マテリアル デザインの原則に沿って進めやすくなります。

ただし、コンポーネントのパラメータを超えてカスタマイズする場合は、あるレベルを「プルダウン」してコンポーネントをフォークできます。たとえば、マテリアル デザインでは、ボタンは無地の背景にする必要があります。グラデーションの背景が必要な場合、このオプションは Button パラメータではサポートされません。その際は、参照として Material Button 実装を使用し、独自のコンポーネントを作成します。

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

上記の実装では、マテリアル レイヤのコンポーネント、たとえば、現在のコンテンツのアルファ版、現在のテキスト スタイルのマテリアルのコンセプトなどが引き続き使用されます。ただし、マテリアル SurfaceRow に置き換えて、望ましい外観になるようスタイルを設定することはできます。

独自のカスタム デザイン システムを構築する場合など、マテリアル コンセプトをまったく使用しない場合は、基盤レイヤ コンポーネントのみを使用するようにプルダウンできます。

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose では、最上位のコンポーネントの名前が最もシンプルになっています。たとえば、androidx.compose.material.Textandroidx.compose.foundation.text.BasicText をベースとしています。これにより、上位レベルを置き換えたい場合には独自の実装に見つけやすい名前を付けられるようになります。

適切な抽象化の選択

Compose には、再利用可能なレイヤ化されたコンポーネントをビルドするという哲学があるので、常に下位のビルディング ブロックのビルドを目指すべきではありません。上位レベルのコンポーネントの多くは、機能をより多く提供するだけでなく、ユーザー補助機能のサポートなどのベスト プラクティスを実装しています。

たとえば、独自のカスタム コンポーネントにジェスチャー サポートを追加する場合、Modifier.pointerInput を使用してゼロから作成できます。ただし、それより上に他の上位コンポーネント(たとえば、Modifier.draggableModifier.scrollableModifier.swipeable)があり、これをさらに優れた出発点にできるかもしれません。

原則として、最上位レベルのコンポーネントをベースにビルドすることをおすすめします。そうすれば、そのコンポーネントのベスト プラクティスからメリットを得るために必要な機能を確保できます。

詳細

カスタム デザイン システムの構築例については、Jetsnack サンプルをご覧ください。