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 {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

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

AppCompat Compose Theme Adapter

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

import com.google.accompanist.appcompattheme.AppCompatTheme

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 ライブラリのドキュメントをご覧ください。

画面サイズの変更処理

画面サイズに応じて XML レイアウトを変えるアプリを移行する場合は、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 */
        }
    }
}

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 of 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 ブランチをご覧ください。