Jetpack Compose への移行

1. はじめに

Compose と View システムは併用できます。

この Codelab では、Sunflower の植物詳細画面の一部を Compose に移行します。実際のアプリを Compose に移行してみるために、プロジェクトのコピーを作成しました。

この Codelab を終了すると、必要に応じて移行を続行し、Sunflower の残りの画面を変換できるようになります。

この Codelab の学習を進める際のサポートについて詳しくは、次の Code-Along 動画をご覧ください。

学習内容

この Codelab では、以下について学びます。

  • さまざまな移行方法
  • アプリを段階的に Compose に移行する方法
  • Android ビューを使用して作成した既存の画面に Compose を追加する方法
  • Compose 内から Android View を使用する方法
  • Compose で View システムからのテーマを使用する方法
  • View システムと Compose コードで画面をテストする方法

Prerequisites

  • ラムダを含む Kotlin 構文の使用経験。
  • Compose の基本に関する知識。

必要なもの

2. 移行を計画する

Compose への移行は、ご自身やチーム次第です。Jetpack Compose を既存の Android アプリに統合するには、さまざまな方法があります。一般的な移行戦略は次の 2 つです。

  • Compose で画面をすべて新しく開発する
  • 既存の画面を基に、コンポーネントを段階的に移行する

新しい画面での Compose

アプリを新しいテクノロジーにリファクタリングする際の一般的なアプローチは、アプリのために構築する新機能でそれを採用することです。今回の場合、新しい画面が該当します。アプリの新しい UI 画面を構築する必要がある場合、アプリの残りの部分は View システムのままにして、UI 画面には Compose を使用することを検討してください。

この場合、移行した機能の両端で Compose 相互運用を行います。

Compose と View の併用

ある画面で、一部を Compose に移行し、その他の部分を View システムに移行することができます。たとえば、RecyclerView を移行し、画面の残りの部分を View システムに残すことができます。

またはその逆に、Compose を外部レイアウトとして使用し、Compose では使用できない既存のビュー(MapView や AdView など)を使用します。

完全移行

一度にすべてのフラグメントまたは画面を Compose に移行します。最も簡単ですが、あまり洗練された方法ではありません。

この Codelab で行うこと

この Codelab では、Compose と View を併用して、Sunflower の植物詳細画面を Compose に段階的に移行します。そうすることで、必要に応じて移行を続行できる十分な知識が身につきます。

3. 設定方法

コードを取得する

次のコマンドで、GitHub から Codelab のコードを取得します。

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。

Android Studio を開く

この Codelab には Android Studio Bumblebee が必要です。

サンプルアプリの実行

ダウンロードしたコードには、利用可能なすべての Compose Codelab のコードが含まれています。この Codelab を完了するには、Android Studio 内で MigrationCodelab プロジェクトを開きます。

この Codelab では、Sunflower の植物詳細画面を Compose に移行します。植物のリスト画面に表示されている植物のいずれかをタップすると、植物詳細画面が開きます。

bb6fcf50b2899894.png

プロジェクトの設定

このプロジェクトには複数の git ブランチがあります。

  • main は、チェックアウトまたはダウンロードしたブランチです。Codelab の出発点です。
  • end には、この Codelab の解答があります。

main ブランチのコードから始め、マイペースで Codelab を進めることをおすすめします。

Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。場所によってはコードを削除する必要もありますが、この部分はコード スニペットのコメントに明示的に記載されています。

git を使用して end ブランチを取得するには、次のコマンドを使用します。

$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs

または、次の場所から解答コードをダウンロードします。

よくある質問

4. Sunflower での Compose

Compose は、main ブランチからダウンロードしたコードにすでに追加されています。しかし、動作させるために必要なものを確認しましょう。

app/build.gradle(または build.gradle (Module: compose-migration.app))ファイルを開くと、Compose の依存関係がインポートされ、Android Studio で buildFeatures { compose true } フラグを使用して Compose が扱えるようになる仕組みが確認できます。

app/build.gradle

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        ...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion rootProject.composeVersion
    }
}

dependencies {
    ...
    // Compose
    implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
    implementation "androidx.compose.material:material:$rootProject.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

これらの依存関係のバージョンは、ルート build.gradle ファイルで定義されています。

5. Compose の使用を開始する

植物詳細画面で、画面の全体構造を維持したまま、植物の説明を Compose に移行します。ここでは、「移行を計画する」のセクションで説明した「Compose と View の併用」移行戦略に沿って進めます。

UI をレンダリングするために、Compose はホストのアクティビティまたはフラグメントを必要とします。Sunflower では、すべての画面がフラグメントを使用しているため、ComposeView を使用します。これは、setContent メソッドを使用して Compose UI コンテンツをホストできる Android View です。

XML コードの削除

移行を始めましょう。fragment_plant_detail.xml を開き、次の操作を行います。

  1. コードビューに切り替えます。
  2. NestedScrollView 内の ConstraintLayout コードとネストされた TextView を削除します(この Codelab では、個々のアイテムを移行する際に XML コードを比較して参照するため、コードをコメントアウトすると便利です)。
  3. 代わりに Compose コードをホストする ComposeView を追加し、compose_view をビュー ID とします。

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    // Step 2) Comment out ConstraintLayout and its children
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...

    </androidx.constraintlayout.widget.ConstraintLayout>
    // End Step 2) Comment out until here

    // Step 3) Add a ComposeView to host Compose code
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Compose コードの追加

これで、植物詳細画面の Compose への移行を開始する準備が整いました。

この Codelab では、plantdetail フォルダの PlantDetailDescription.kt ファイルに Compose コードを追加します。このファイルを開いて、"Hello Compose!" というプレースホルダ テキストがプロジェクトですでに利用可能になっていることを確認します。

plantdetail/PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Text("Hello Compose")
}

前のステップで追加した ComposeView からこのコンポーザブルを呼び出して、画面に表示しましょう。plantdetail/PlantDetailFragment.kt を開きます。

画面はデータ バインディングを使用しているため、composeView に直接アクセスして setContent を呼び出すと、Compose コードを画面に表示できます。Sunflower はマテリアル デザインを使用しているため、MaterialTheme 内で PlantDetailDescription コンポーザブルを呼び出します。

plantdetail/PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        ...
    }
}

アプリを実行すると、画面に「Hello Compose!」と表示されます。

66f3525ecf6669e0.png

6. XML からのコンポーザブルの作成

まず、植物の名前を移行しましょう。正確には、fragment_plant_detail.xml で削除した ID @+id/plant_detail_nameTextView です。XML コードは次のとおりです。

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

スタイルは textAppearanceHeadline5、横方向のマージンは 8.dp であり、画面上で横方向に中央揃えされています。ただし、表示されるタイトルは、リポジトリ レイヤからの PlantDetailViewModel で公開される LiveData からモニタリングされます。

LiveData のモニタリングについては後述します。名前が利用可能であり、PlantDetailDescription.kt ファイルで作成した新しい PlantName コンポーザブルにパラメータとして渡されたとしましょう。このコンポーザブルは、後で PlantDetailDescription コンポーザブルから呼び出されます。

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

プレビュー:

d09fe886b98bde91.png

ここで

  • Text のスタイルは MaterialTheme.typography.h5 であり、XML コードの textAppearanceHeadline5 にマッピングされています。
  • 修飾子がテキストを装飾し、XML 版のように調整します。
  • fillMaxWidth 修飾子は XML コードの android:layout_width="match_parent" に対応します。
  • 横方向の paddingmargin_small であり、この値は dimensionResource ヘルパー関数を使用して View システムから取得します。
  • wrapContentWidthText を横方向に揃えます。

7. ViewModel と LiveData

それでは、タイトルを画面につなぎましょう。そのためには、PlantDetailViewModel を使用してデータを読み込む必要があります。そこで、Compose には ViewModelLiveData の統合が用意されています。

ViewModel

Fragment で PlantDetailViewModel のインスタンスが使用されるため、これをパラメータとして PlantDetailDescription に渡すだけで済みます。

PlantDetailDescription.kt ファイルを開き、PlantDetailViewModel パラメータを PlantDetailDescription に追加します。

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    ...
}

このコンポーザブルをフラグメントから呼び出すときに、ViewModel のインスタンスを渡します。

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

これで、PlantDetailViewModelLiveData<Plant> フィールドにアクセスし、植物名を取得できるようになりました。

コンポーザブルから LiveData をモニタリングするには、LiveData.observeAsState() 関数を使用します。

LiveData が出力する値は null になる可能性があるため、その使用を null チェックでラップする必要があります。そのため、再利用性を考えて、LiveData の使用とリッスンを異なるコンポーザブルに分割することをおすすめします。したがって、PlantDetailContent という新しいコンポーザブルを作成して Plant 情報を表示します。

以上により、LiveData モニタリングを追加した PlantDetailDescription.kt ファイルは次のようになります。

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

現時点で PlantDetailContentPlantName を呼び出すだけであるため、プレビューは PlantNamePreview と同じです。

3e47e682cf518c71.png

これで、ViewModel が Compose につながり、Compose で植物名が表示されるようになりました。以降のセクションでは、残りのコンポーザブルを作成して、同様の方法で ViewModel につなぎます。

8. 他の XML コードの移行

UI にない情報(水やり情報、植物の説明など)を簡単に記入できるようになりました。前述の XML コードと同様のアプローチで、画面の残りの部分を移行できます。

以前に fragment_plant_detail.xml から削除した水やり情報の XML コードは、ID が plant_watering_headerplant_watering の 2 つの TextView で構成されています。

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

以前と同様に、PlantWatering という新しいコンポーザブルを作成し、Text を追加して、水やり情報を画面に表示します。

PlantDetailDescription.kt

@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = LocalContext.current.resources.getQuantityString(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

プレビュー:

6f6c17085801a518.png

注意点:

  • 横方向のパディングと配置の装飾は Text コンポーザブルで共有されるため、ローカル変数(centerWithPaddingModifier)に代入することで Modifier を再利用できます。修飾子は標準の Kotlin オブジェクトであるため、このようにできます。
  • Compose の MaterialTheme は、plant_watering_header で使用されている colorAccent と完全一致しません。ひとまず、テーマ設定のセクションで改善する MaterialTheme.colors.primaryVariant を使用しましょう。

すべての要素をつなげて、PlantDetailContent からも PlantWatering を呼び出しましょう。最初に削除した ConstraintLayout XML コードではマージンが 16.dp でした。これを Compose コードに含める必要があります。

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

PlantDetailContent で、Column を作成して名前と水やり情報をまとめて表示し、パディングとします。また、使用する背景色とテキスト色が適切になるように、その処理を行う Surface を追加します。

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

プレビューを更新すると、次のように表示されます。

56626a7118ce075c.png

9. Compose コードでの View

植物の説明文を移行しましょう。fragment_plant_detail.xml のコードでは、TextViewapp:renderHtml="@{viewModel.plant.description}" を指定して、画面に表示されるテキストを XML に伝えていました。renderHtml は、PlantDetailBindingAdapters.kt ファイルにあるバインディング アダプターです。この実装では、HtmlCompat.fromHtml を使用して TextView にテキストを設定しています。

しかし、Compose は現在のところ Spanned クラスや HTML 形式のテキストの表示をサポートしていません。そのため、この制限を回避するために Compose コードで View システムの TextView を使用する必要があります。

Compose ではまだ HTML コードをレンダリングできないため、AndroidView API を使用してプログラムで TextView を作成します。

AndroidViewView をパラメータとして受け取り、View がインフレートされたときのコールバックを提供します。

そのために、新しい PlantDescription コンポーザブルを作成しましょう。このコンポーザブルは、ラムダで記憶した TextView を使用して AndroidView を呼び出します。factory コールバックで、所定の Context を使用して HTML インタラクションに応答する TextView を初期化します。update コールバックで、記憶した HTML 形式の説明を使用してテキストを設定します。

PlantDetailDescription.kt

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

プレビュー:

deea1d191e9087b4.png

htmlDescription は、パラメータとして渡された description について、HTML 形式の説明を記憶しています。description パラメータが変更された場合、remember 内の htmlDescription コードが再度実行されます。

同様に、htmlDescription が変更された場合、AndroidView 更新コールバックが再コンポーズされます。コールバック内で状態が読み取られると、再コンポーズが発生します。

PlantDescriptionPlantDetailContent コンポーザブルに追加し、HTML の説明も表示するようにプレビュー コードを変更しましょう。

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

プレビュー:

7843a8d6c781c244.png

この時点で、元の ConstraintLayout 内のコンテンツはすべて Compose に移行しました。アプリを実行して、想定どおりに動作することを確認します。

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

デフォルトでは、ComposeView がウィンドウからデタッチされるたびに、Compose は Composition を破棄します。複数の理由によりフラグメントで ComposeView が使用されている場合、これは望ましくありません。

  • Composition は、Compose UI の View タイプに対するフラグメントのビュー ライフサイクルに従って、状態を保存する必要があります。
  • 遷移またはウィンドウ遷移が発生したときに、Compose UI 要素を画面上に維持します。遷移中、ComposeView 自体はウィンドウからデタッチされた後も引き続き表示されます。

AbstractComposeView.disposeComposition メソッドを手動で呼び出して、Composition を手動で破棄できます。あるいは、Composition が不要になったときに自動的に破棄するには、別の戦略を設定するか、setViewCompositionStrategy メソッドを呼び出して独自の戦略を作成します。

DisposeOnViewTreeLifecycleDestroyed 戦略を使用して、フラグメントの LifecycleOwner が破棄されたときに Composition を破棄します。

PlantDetailFragment には遷移の出入りがあるため(詳細は nav_garden.xml をご覧ください、後ほど Compose 内で View タイプを使用し、また、ComposeViewDisposeOnViewTreeLifecycleDestroyed 戦略を使用するようにする必要があります。ただし、フラグメントで ComposeView を使用する場合は、常にこの戦略を設定することをおすすめします

plantdetail/PlantDetailFragment.kt

import androidx.compose.ui.platform.ViewCompositionStrategy
...

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.apply {
                // Dispose the Composition when the view's LifecycleOwner
                // is destroyed
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        PlantDetailDescription(plantDetailViewModel)
                    }
                }
            }
        }
        ...
    }
}

11. 相互運用性のテーマ設定

植物の詳細のテキスト コンテンツを Compose に移行しました。しかし、Compose では適切なテーマカラーが使用されていません。緑色を使用すべき植物名に、紫色を使用しています。

この移行の初期段階では、Compose で独自のマテリアル テーマを最初から書き換えるのではなく、View システムで利用可能なテーマを継承することをおすすめします。マテリアル テーマは、Compose に付属するすべてのマテリアル デザイン コンポーネントに対応しています。

View システムのマテリアル デザイン コンポーネント(MDC)テーマを Compose で再利用するには、compose-theme-adapter を使用します。MdcTheme 関数は、ホスト コンテキストの MDC テーマを自動的に読み取り、ライトモードとダークモードの両方でデベロッパーに代わって MaterialTheme に渡します。この Codelab ではテーマカラーだけでなく、View システムのシェイプとタイポグラフィも読み取ります。

このライブラリはすでに、app/build.gradle ファイルに次のように含まれています。

...
dependencies {
    ...
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

これを使用するには MaterialTheme の使用を MdcTheme に置き換えます。たとえば、PlantDetailFragment では次のようになります。

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            MdcTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

PlantDetailDescription.kt ファイル内のすべてのプレビュー コンポーザブルは、次のようになります。

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    MdcTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MdcTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MdcTheme {
        PlantDescription("HTML<br><br>description")
    }
}

プレビューからわかるように、MdcThemestyles.xml ファイルのテーマから色を選択します。

886d7eaea611f4eb.png

新しい関数を作成し、プレビューの uiModeConfiguration.UI_MODE_NIGHT_YES を渡すことで、UI をダークモードでプレビューすることもできます。

import android.content.res.Configuration
...

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

プレビュー:

cfe11c109ff19eeb.png

アプリを実行すると、ライトモードとダークモードの両方で、移行前とまったく同じように動作します。

c99216fc77699dd7.gif

12. テスト

植物詳細画面の一部を Compose に移行した後は、何も問題がないことを確認するためのテストが不可欠です。

Sunflower では、androidTest フォルダにある PlantDetailFragmentTest によってアプリの一部の機能がテストされます。このファイルを開き、現在のコードを確認します。

  • testPlantName は、画面上の植物の名前を確認します
  • testShareTextIntent は、共有ボタンをタップした後に正しいインテントがトリガーされることを確認します

アクティビティやフラグメントで Compose を使用する場合、ActivityScenarioRule を使用するのではなく、ActivityScenarioRuleComposeTestRule と統合する createAndroidComposeRule を使用する必要があります。これにより、Compose コードをテストできるようになります。

PlantDetailFragmentTest で、ActivityScenarioRule の使用を createAndroidComposeRule に置き換えます。テストを設定するためにアクティビティ ルールが必要な場合は、次のように createAndroidComposeRuleactivityRule 属性を使用します。

@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {

    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()

    ...

    @Before
    fun jumpToPlantDetailFragment() {
        populateDatabase()

        composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }
    }

    ...
}

テストを実行すると、testPlantName が失敗します。testPlantName は、TextView が画面上にあるかどうかを確認します。しかし、UI のその部分は Compose に移行済みです。そのため、代わりに Compose のアサーションを使用する必要があります。

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

テストを実行すると、すべて合格します。

dd59138fac1740e4.png

13. 完了

これで、この Codelab は終了です。

元の Sunflower github プロジェクトの compose ブランチは、植物詳細画面を Compose に完全に移行しています。この Codelab で行ったこととは別に、CollapsingToolbarLayout の動作もシミュレートします。これには以下が含まれます。

  • Compose による画像の読み込み
  • アニメーション
  • ディメンション処理の向上
  • その他

次のステップ

Compose パスウェイに関する他の Codelab をご確認ください。

関連情報