Compose の既存の UI との統合

ビューベースの UI を備えたアプリがある場合、その UI 全体を一度に書き換える必要はありません。このページでは、新しい Compose 要素を既存の UI に追加する方法を紹介します。

共有 UI の移行

Compose に徐々に移行する場合は、Compose と View システムの両方で共有 UI 要素を使用しなければならないことがあります。たとえば、アプリにカスタム CallToActionButton コンポーネントがある場合、Compose と View ベースの両方の画面で使用する必要があるかもしれません。

Compose の場合、共有 UI 要素はコンポーザブルとなり、XML を使用してスタイル設定されている要素やカスタムビューの要素であるかに関係なく、アプリ全体で再利用できます。たとえば、カスタム外部リンクのカスタム Button コンポーネントに CallToActionButton コンポーザブルを作成するとします。

View ベースの画面でコンポーザブルを使用するには、AbstractComposeView から拡張するカスタム ビューラッパーを作成する必要があります。そのオーバーライドした Content コンポーザブルに、以下の例のように Compose テーマでラップして作成したコンポーザブルを配置します。

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

コンポーザブル パラメータは、カスタムビュー内で可変変数になることに注意してください。これにより、カスタムの CallToActionViewButton は、ビュー バインディングなどを使用して、従来のビューのようにインフレータブルかつ使用可能になります。以下の例をご覧ください。

class ExampleActivity : Activity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.something)
            onClick = { /* Do something */ }
        }
    }
}

カスタム コンポーネントに可変状態が含まれている場合は、状態に関する信頼できる情報源をご覧ください。

テーマ設定

Android アプリにテーマを設定するには、マテリアル デザインを導入し、Android 用のマテリアル デザイン コンポーネント(MDC)ライブラリを使用することをおすすめします。Compose のテーマ設定に関するドキュメントで説明したとおり、Compose はこうしたコンセプトを MaterialTheme コンポーザブルで実装します。

Compose で新しい画面を作成するときは、マテリアル コンポーネント ライブラリから UI を出力するコンポーザブルの前に MaterialTheme を適用する必要があります。マテリアル コンポーネント(ButtonText など)は MaterialTheme が設定されているかどうかに左右され、設定されていない場合の動作は未定義になります。

Jetpack Compose サンプルはすべて、MaterialTheme 上に構築されたカスタムの Compose テーマを使用しています。

信頼できる複数の情報源

既存のアプリには、ビューのテーマ設定やスタイル設定が非常に多い可能性があります。既存のアプリに Compose を導入する場合は、Compose 画面で MaterialTheme を使用するようにテーマを移行する必要があります。つまり、アプリのテーマ設定には、View ベースのテーマと Compose テーマの 2 つの信頼できる情報源があります。スタイル設定の変更は、複数の場所で行う必要があります。

アプリを Compose に完全移行する場合は、既存のテーマの Compose バージョンを作成する必要があります。問題は、開発プロセスの早い段階で Compose テーマを作成するほど、開発中のメンテナンスが増えてしまうことです。

MDC Compose Theme Adapter

Android アプリで MDC ライブラリを使用している場合、MDC Compose Theme Adapter ライブラリを使用すると、既存の View ベースのテーマにおけるタイポグラフィシェイプのテーマ設定を、コンポーザブルで簡単に再利用できます。

import com.google.android.material.composethemeadapter.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Use MdcTheme instead of MaterialTheme
            // Colors, typography, and shape have been read from the
            // View-based theme used in this Activity
            MdcTheme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

詳細については、MDC ライブラリのドキュメントをご覧ください。

AppCompat Compose Theme Adapter

AppCompat Compose Theme Adapter ライブラリを使用すると、Jetpack Compose でのテーマ設定に AppCompat XML テーマを簡単に再利用できます。コンテキストのテーマからタイポグラフィの値を使用して MaterialTheme を作成します。

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

デフォルトのコンポーネント スタイル

MDC ライブラリと AppCompat Compose Theme Adapter ライブラリのどちらでも、テーマが定義されているデフォルトのウィジェット スタイルは読み込まれません。これは、Compose にはデフォルトのコンポーザブルのコンセプトがないためです。

コンポーネント スタイルカスタム デザイン システムの詳細については、テーマ設定に関するドキュメントをご覧ください。

Compose のテーマ オーバーレイ

View ベースの画面を Compose に移行する場合は、android:theme 属性の使い方に注意してください。Compose UI ツリーの該当部分に新しい MaterialTheme が必要になる場合があります。

詳しくは、テーマ設定ガイドをご覧ください。

WindowInsets と IME アニメーション

WindowInsets を処理するには、accompanist-insets ライブラリを使用します。このライブラリは、レイアウト内で処理するコンポーザブルと修飾子を提供できるほか、IME アニメーションをサポートします。

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

キーボードを表示するように UI 要素が上下方向にスクロールするアニメーション

図 2. accompanist-insets ライブラリを使用した IME アニメーション

詳細については、accompanists-insets ライブラリのドキュメントをご覧ください。

プレゼンテーションからの状態の分離を優先する

伝統的に View はステートフルです。View は、表示する方法に加えて、表示する内容を記述するフィールドを管理します。View を Compose に変換する際は、状態ホイスティングで詳しく説明しているように、レンダリングされるデータを分離して単方向データフローを実現するよう努めてください。

たとえば、View には、「表示される」、「表示されない」、「消失した」のいずれかの状態を記述する visibility プロパティがあります。これは View の固有のプロパティです。View の可視性は他のコードによって変更されることがありますが、現在の可視性の状態を実際に認識するのは View 自身のみです。View が確実に表示されるようにするロジックはエラーが発生しやすく、多くの場合 View 自身に関連付けられます。

一方、Compose では、Kotlin の条件付きロジックを使用して、まったく異なるコンポーザブルを簡単に表示できます。

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

設計上、CautionIcon は自身が表示されている理由を知る必要も考慮する必要もなく、visibility の概念はありません。それは Composition 内にあるかないかのいずれかです。

状態管理とプレゼンテーション ロジックを明確に分離することで、コンテンツを状態の UI への変換として表示する方法をより自由に変更できます。また、必要なときに状態をホイスティングできるので、コンポーザブルがより再利用しやすくなります。これは、状態の所有権の柔軟性がより高いためです。

カプセル化された再利用可能なコンポーネントを活用する

多くの場合、View 要素にはそれが存在する場所(Activity 内、Dialog 内、Fragment 内、または別の View 階層内のどこか)の情報が含まれています。それらはしばしば静的レイアウト ファイルからインフレートされるので、View の全体的な構造は非常に固定的なものになりがちです。その結果、結合が密になり、View の変更または再利用が困難になります。

たとえば、カスタム View は、特定の ID を持つ特定の型の子ビューを持っていると想定し、なんらかのアクションに応じてそのプロパティを直接変更します。これにより、それらの View 要素は互いに密結合されます。カスタム View は、子を見つけられない場合、クラッシュしたり破損したりすることがあります。また、子はカスタム View の親がないと再利用できないことがあります。

再利用可能なコンポーザブルを使用する Compose では、こうしたことはそれほど問題になりません。親は状態とコールバックを簡単に指定できるので、再利用可能なコンポーザブルは、それが使用される場所が正確にわからなくても記述できます。

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

上記の例では、3 つの部分すべてがより高度にカプセル化され、結合が疎になっています。

  • ImageWithEnabledOverlay が知る必要があるのは、現在の isEnabled の状態だけです。ControlPanelWithToggle が存在するかどうか、またそれが制御可能かどうかを知る必要はありません。

  • ControlPanelWithToggleImageWithEnabledOverlay の存在を認識しません。isEnabled の表示方法は無指定にすることも、1 つまたは複数指定することもできます。ControlPanelWithToggle は変更の必要がありません。

  • 親にとって、ImageWithEnabledOverlay または ControlPanelWithToggle のネストの深さは問題になりません。このような子には、アニメーションの変更、コンテンツのスワップアウト、他の子へのコンテンツの引き渡しなどがあります。

このパターンは「制御の反転」と呼ばれます。詳しくは、CompositionLocal のドキュメントをご覧ください。

画面サイズの変更処理

レスポンシブな View レイアウトの主な作成方法の一つは、異なるウィンドウ サイズごとに異なるリソースを用意することです。従来のように、修飾されたリソースを使用して画面レベルのレイアウトを決定することもできますが、Compose では、通常の条件付きロジックを使用してもっと簡単にコード内でレイアウトを完全に変更できます。BoxWithConstraints のようなツールを使用すると、個々の要素が使用できるスペースに応じて決定を行えます。これは、修飾されたリソースでは不可能です。

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

アダプティブ UI を作成するために Compose が提供する手法については、アダプティブ レイアウトを作成するをご覧ください。

View でのネストされたスクロール

View システムと Jetpack Compose の間でのネストされたスクロールは、現時点ではまだ使用できません。進捗状況につきましては、こちらのIssue Tracker バグでご確認ください。

RecyclerView の Compose

Jetpack Compose は、デフォルトの ViewCompositionStrategy として DisposeOnDetachedFromWindow を使用します。つまり、ビューがウィンドウから切り離されるたびに、Composition は廃棄されます。

ComposeViewRecyclerView ビューホルダーの一部として使用すると、RecyclerView がウィンドウから切り離されるまで基になる Composition インスタンスがメモリ内に残るため、デフォルトの戦略は非効率的になります。RecyclerViewComposeView が不要になったら、基になる Composition を破棄することをおすすめします。

disposeComposition 関数を使用すると、ComposeView の基になる Composition を手動で破棄できます。この関数は、次のようにビューがリサイクルされたときに呼び出すことができます。

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

相互運用 API ガイドの ComposeView の ViewCompositionStrategy のセクションで説明したように、Compose ビューホルダーをすべてのシナリオで機能させるには、DisposeOnViewTreeLifecycleDestroyed 戦略を使用する必要があります。

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

RecyclerView で使用されている ComposeView の動作を確認するには、Sunflower アプリの compose_recyclerview ブランチをご覧ください。