1. はじめに
Compose と View システムは併用できます。
この Codelab では、Sunflower の植物詳細画面の一部を Compose に移行します。実際のアプリを Compose に移行してみるために、プロジェクトのコピーを作成しました。
この Codelab を終了すると、必要に応じて移行を続行し、Sunflower の残りの画面を変換できるようになります。
この Codelab の学習を進める際のサポートについて詳しくは、次の Code-Along 動画をご覧ください。
学習内容
この Codelab では、以下について学びます。
- さまざまな移行方法
- アプリを段階的に Compose に移行する方法
- ビューを使用して作成した既存の画面に Compose を追加する方法
- Compose 内からビューを使用する方法
- Compose でテーマを作成する方法
- ビューと Compose の両方で作成された混合画面をテストする方法
前提条件
- ラムダを含む Kotlin 構文の使用経験。
- Compose の基本に関する知識。
必要なもの
2. 移行戦略
Jetpack Compose は、ビューの相互運用性を最初から考慮して設計されています。Compose に移行する場合については、アプリが完全に Compose に移行されるまで、コードベースに Compose とビューを共存させる増分移行をおすすめします。
推奨される移行戦略は次のとおりです。
- Compose を使用して新しい画面を作成します。
- 機能を構築しながら、再利用可能な要素を特定し、一般的な UI コンポーネントのライブラリの作成を開始します。
- 既存の機能を 1 画面ずつ置き換えます。
Compose を使用して新しい画面を作成する
Compose を使用した画面全体を含む新しい機能の構築は、Compose の導入を促進する最良の方法です。この戦略で機能を追加することで、企業のビジネスニーズに対応しながら Compose のメリットを活用できます。
新機能には画面全体が含まれることがあり、その場合は画面全体が Compose に含まれます。フラグメント ベースのナビゲーションを使用する場合は、新しいフラグメントを作成し、そのコンテンツを Compose に追加します。
既存の画面に新しい機能を導入することもできます。この場合、ビューと Compose が同じ画面に併存します。たとえば、追加する機能が RecyclerView の新しいビュータイプであるとします。その場合、新しいビュータイプは Compose に存在し、他のアイテムは同じ状態で保持されます。
一般的な UI コンポーネントのライブラリを作成する
Compose を使用して機能を作成する場合、結果としてコンポーネントのライブラリを構築することになります。そのため、アプリ全体で再利用を促進するために再利用可能なコンポーネントを特定し、共有コンポーネントに信頼できる唯一の情報源を提供する必要があります。作成した機能は、このライブラリに依存させることができます。
既存の機能を Compose で置き換える
新しい機能を構築するだけでなく、アプリの既存の機能を Compose に段階的に移行する必要があるとします。このためのアプローチは任意ですが、次のような方法をおすすめします。
- シンプルな画面 - ウェルカム画面、確認画面、設定画面など、少数の UI 要素が配置されており、動的に生成されるアプリ内のシンプルな画面。数行のコードで実行できるため、Compose に移行する際におすすめできる方法です。
- 混在ビュー画面と Compose 画面 - すでに少しの Compose コードが含まれている画面も、要素を 1 つずつ順に移行できるため適しています。Compose にサブツリーのみの画面がある場合は、UI 全体が Compose に配置されるまで、ツリーの他の部分を移行できます。これは移行のボトムアップ アプローチと呼ばれます。
この Codelab のアプローチ
この Codelab では、Compose とビューを併用して、Sunflower の植物詳細画面を Compose に段階的に移行します。そうすることで、必要に応じて移行を続行できる十分な知識が身につきます。
3. 設定方法
コードを取得する
次のコマンドで、GitHub から Codelab のコードを取得します。
$ git clone https://github.com/android/codelab-android-compose
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
サンプルアプリの実行
ダウンロードしたコードには、利用可能なすべての Compose Codelab のコードが含まれています。この Codelab を完了するには、Android Studio 内で MigrationCodelab
プロジェクトを開きます。
この Codelab では、Sunflower の植物詳細画面を Compose に移行します。植物のリスト画面に表示されている植物のいずれかをタップすると、植物詳細画面が開きます。
プロジェクトの設定
このプロジェクトには複数の git ブランチがあります。
main
ブランチは Codelab の出発点です。end
にはこの Codelab の解答があります。
main
ブランチのコードから始め、ご自身のペースで Codelab を進めることをおすすめします。
Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。場所によってはコードを削除する必要もありますが、この部分はコード スニペットのコメントに明示的に記載されています。
git を使用して end
ブランチを取得するには、cd
で MigrationCodelab
プロジェクトのディレクトリに移動してから、次のコマンドを使用します。
$ git checkout end
または、次の場所から解答コードをダウンロードします。
よくある質問
4. Sunflower での Compose
Compose は、main
ブランチからダウンロードしたコードにすでに追加されています。しかし、動作させるために必要なものを確認しましょう。
アプリレベルの build.gradle
ファイルを開くと、Compose の依存関係がインポートされ、Android Studio で buildFeatures { compose true }
フラグを使用して Compose が扱えるようになる仕組みを確認できます。
app/build.gradle
android {
//...
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
//...
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
dependencies {
//...
// Compose
def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
implementation(composeBom)
androidTestImplementation(composeBom)
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material:material"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
//...
}
これらの依存関係のバージョンは、プロジェクト レベルの build.gradle
ファイルで定義されています。
5. Compose の使用を開始する
植物詳細画面で、画面の全体構造を維持したまま、植物の説明を Compose に移行します。
UI をレンダリングするために、Compose はホストのアクティビティまたはフラグメントを必要とします。Sunflower では、すべての画面がフラグメントを使用しているため、ComposeView
を使用します。これは、setContent
メソッドを使用して Compose UI コンテンツをホストできる Android View です。
XML コードの削除
移行を始めましょう。fragment_plant_detail.xml
を開き、次の操作を行います。
- コードビューに切り替えます。
NestedScrollView
内のConstraintLayout
コードとネストされた 4 つのTextView
を削除します(この Codelab では、個々のアイテムを移行する際に XML コードを比較して参照するため、コードをコメントアウトすると便利です)。- 代わりに 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"
というプレースホルダ テキストがプロジェクトですでに利用可能になっていることを確認します。
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
前のステップで追加した ComposeView
からこのコンポーザブルを呼び出して、画面に表示しましょう。PlantDetailFragment.kt
を開きます。
画面はデータ バインディングを使用しているため、composeView
に直接アクセスして setContent
を呼び出すと、Compose コードを画面に表示できます。Sunflower はマテリアル デザインを使用しているため、MaterialTheme
内で PlantDetailDescription
コンポーザブルを呼び出します。
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
」と表示されます。
6. XML からのコンポーザブルの作成
まず、植物の名前を移行しましょう。正確には、fragment_plant_detail.xml
で削除した ID @+id/plant_detail_name
の TextView
です。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")
}
}
プレビュー:
ここで
Text
のスタイルはMaterialTheme.typography.h5
であり、XML コードのtextAppearanceHeadline5
に類似しています。- 修飾子がテキストを装飾し、XML 版のように表示します。
fillMaxWidth
修飾子は、利用可能な最大幅を占有するために使用されます。この修飾子は、XML コードのlayout_width
属性のmatch_parent
値に対応します。padding
修飾子は、水平方向のパディング値margin_small
が適用されるように設定するために使用されます。これは XML のmarginStart
宣言とmarginEnd
宣言に対応します。margin_small
値は、dimensionResource
ヘルパー関数を使用して取得される既存のディメンション リソースでもあります。wrapContentWidth
修飾子を使用して、テキストを水平方向に中央揃えします。これは、XML でcenter_horizontal
のgravity
を使用する場合と類似しています。
7. ViewModel と LiveData
それでは、タイトルを画面につなぎましょう。そのためには、PlantDetailViewModel
を使用してデータを読み込む必要があります。そこで、Compose には ViewModel と LiveData の統合が用意されています。
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
これで、PlantDetailViewModel
の LiveData<Plant>
フィールドにアクセスし、植物名を取得できるようになりました。
コンポーザブルから LiveData をモニタリングするには、LiveData.observeAsState()
関数を使用します。
LiveData が出力する値は null
になる可能性があるため、その使用を null
チェックでラップする必要があります。そのため、再利用性を実現するため、LiveData の使用とリッスンを異なるコンポーザブルに分割することをおすすめします。そのため、Plant
情報を表示する PlantDetailContent
という新しいコンポーザブルを作成しましょう。
これらの更新により、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)
}
}
PlantNamePreview
は、PlantDetailContent
が PlantName
を呼び出すだけであるため、直接更新しなくとも変更を反映します。
これで、ViewModel が Compose につながり、Compose で植物名が表示されるようになりました。以下の各セクションでは、残りのコンポーザブルを作成して、同様の方法で ViewModel に接続します。
8. 他の XML コードの移行
UI にない情報(水やり情報、植物の説明など)を簡単に記入できるようになりました。以前と同様のアプローチで、画面の残りの部分を移行できます。
以前に fragment_plant_detail.xml
から削除した水やり情報の XML コードは、ID が plant_watering_header
と plant_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
@OptIn(ExperimentalComposeUiApi::class)
@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 = pluralStringResource(
R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
)
Text(
text = wateringIntervalText,
modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
)
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
MaterialTheme {
PlantWatering(7)
}
}
プレビュー:
注意点:
- 横方向のパディングと配置の装飾は
Text
コンポーザブルで共有されるため、ローカル変数(centerWithPaddingModifier
)に代入することで Modifier を再利用できます。修飾子は標準の Kotlin オブジェクトであるため、このようにできます。 - Compose の
MaterialTheme
は、plant_watering_header
で使用されているcolorAccent
と完全一致しません。ひとまず、相互運用性のテーマ設定のセクションで改善するMaterialTheme.colors.primaryVariant
を使用しましょう。 - Compose 1.2.1 では、
pluralStringResource
を使用するにはExperimentalComposeUiApi
にオプトインする必要があります。Compose の今後のバージョンでは、これが不要になる可能性があります。
すべての要素をつなげて、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)
}
}
}
プレビューを更新すると、次のように表示されます。
9. Compose コードでの View
植物の説明文を移行しましょう。fragment_plant_detail.xml
のコードでは、TextView
に app:renderHtml="@{viewModel.plant.description}"
を指定して、画面に表示されるテキストを XML に伝えていました。renderHtml
は、PlantDetailBindingAdapters.kt
ファイルにあるバインディング アダプターです。この実装では、HtmlCompat.fromHtml
を使用して TextView
にテキストを設定しています。
しかし、Compose は現在のところ Spanned
クラスや HTML 形式のテキストの表示をサポートしていません。そのため、この制限を回避するために Compose コードで View システムの TextView
を使用する必要があります。
Compose ではまだ HTML コードをレンダリングできないため、AndroidView
API を使用してプログラムで TextView
を作成してレンダリングの処理を行います。
AndroidView
を使用すると、factory
ラムダで View
を作成できます。また、ビューがインフレートされた場合に後続の再コンポジションで呼び出される update
ラムダも用意されています。
そのために、新しい PlantDescription
コンポーザブルを作成しましょう。このコンポーザブルは、factory
ラムダで TextView
を構築する AndroidView
を呼び出します。factory
ラムダで、HTML 形式のテキストを表示する TextView
を初期化してから、movementMethod
を LinkMovementMethod
のインスタンスに設定します。最後に、update
ラムダで TextView
のテキストを htmlDescription
に設定します。
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")
}
}
プレビュー:
htmlDescription
は、パラメータとして渡された description
について、HTML 形式の説明を記憶しています。description
パラメータが変更された場合、remember
内の htmlDescription
コードが再度実行されます。
その結果、htmlDescription
が変更された場合に、AndroidView
更新コールバックが再コンポーズされます。update
ラムダ内で状態が読み取られると、再コンポーズが行われます。
PlantDescription
を PlantDetailContent
コンポーザブルに追加し、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)
}
}
プレビュー:
この時点で、元の ConstraintLayout
内のコンテンツはすべて Compose に移行しました。アプリを実行して、想定どおりに動作することを確認します。
10. ViewCompositionStrategy
ComposeView
がウィンドウからデタッチされるたびに、Compose は Composition を破棄します。フラグメントで ComposeView
が使用されている場合、これは 2 つの理由で望ましくありません。
- Composition は、Compose UI の
View
タイプに対するフラグメントのビュー ライフサイクルに従って、状態を保存する必要があります。 - 遷移が発生すると、基盤となる
ComposeView
は分離された状態になります。ただし、これらの遷移中も Compose UI 要素は引き続き表示されます。
この動作を変更するには、適切な ViewCompositionStrategy
を指定して setViewCompositionStrategy
を呼び出し、代わりにフラグメントのビューのライフサイクルに従うようにします。具体的には、DisposeOnViewTreeLifecycleDestroyed
戦略を使用して、フラグメントの LifecycleOwner
が破棄されたときに Composition を破棄できます。
PlantDetailFragment
には遷移の出入りがあるため(詳細は nav_garden.xml
をご覧ください)、後ほど Compose 内で View
タイプを使用し、また、ComposeView
が DisposeOnViewTreeLifecycleDestroyed
戦略を使用するようにする必要があります。ただし、フラグメントで ComposeView
を使用する場合は、常にこの戦略を設定することをおすすめします。
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 では適切なテーマカラーが使用されていません。緑色を使用すべき植物名に、紫色を使用しています。
正しいテーマカラーを使用するには、独自のテーマを定義し、テーマの色を指定して、MaterialTheme
をカスタマイズする必要があります。
MaterialTheme
のカスタマイズ
独自のテーマを作成するには、theme
パッケージの Theme.kt
ファイルを開きます。Theme.kt
は、コンテンツ ラムダを受け取って MaterialTheme
に渡す SunflowerTheme
というコンポーザブルを定義します。
まだ特に何も起こりません。次はカスタマイズします。
Theme.kt
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun SunflowerTheme(
content: @Composable () -> Unit
) {
MaterialTheme(content = content)
}
MaterialTheme
を使用すると、色、タイポグラフィ、シェイプをカスタマイズできます。ひとまず、Sunflower View のテーマと同じ色を指定して、色をカスタマイズします。SunflowerTheme
は、darkTheme
というブール値のパラメータを受け入れることもできます。これは、システムがダークモードの場合は true
になり、そうでない場合は false
になります。このパラメータを使用すると、現在設定されているシステムテーマに合わせて、適切な色の値を MaterialTheme
に渡すことができます。
Theme.kt
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColors(
primary = colorResource(id = R.color.sunflower_green_500),
primaryVariant = colorResource(id = R.color.sunflower_green_700),
secondary = colorResource(id = R.color.sunflower_yellow_500),
background = colorResource(id = R.color.sunflower_green_500),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
)
val darkColors = darkColors(
primary = colorResource(id = R.color.sunflower_green_100),
primaryVariant = colorResource(id = R.color.sunflower_green_200),
secondary = colorResource(id = R.color.sunflower_yellow_300),
onPrimary = colorResource(id = R.color.sunflower_black),
onSecondary = colorResource(id = R.color.sunflower_black),
onBackground = colorResource(id = R.color.sunflower_black),
surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
onSurface = colorResource(id = R.color.sunflower_white),
)
val colors = if (darkTheme) darkColors else lightColors
MaterialTheme(
colors = colors,
content = content
)
}
これを使用するには、MaterialTheme
を SunflowerTheme
に置き換えます。たとえば、PlantDetailFragment
では次のようになります。
PlantDetailFragment.kt
class PlantDetailFragment : Fragment() {
...
composeView.apply {
...
setContent {
SunflowerTheme {
PlantDetailDescription(plantDetailViewModel)
}
}
}
}
PlantDetailDescription.kt
ファイル内のすべてのプレビュー コンポーザブルは、次のようになります。
PlantDetailDescription.kt
@Preview
@Composable
private fun PlantDetailContentPreview() {
val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
@Preview
@Composable
private fun PlantNamePreview() {
SunflowerTheme {
PlantName("Apple")
}
}
@Preview
@Composable
private fun PlantWateringPreview() {
SunflowerTheme {
PlantWatering(7)
}
}
@Preview
@Composable
private fun PlantDescriptionPreview() {
SunflowerTheme {
PlantDescription("HTML<br><br>description")
}
}
プレビューからわかるように、色は Sunflower テーマの色と一致しています。
新しい関数を作成し、プレビューの uiMode
に Configuration.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, "")
SunflowerTheme {
PlantDetailContent(plant)
}
}
プレビュー:
アプリを実行すると、ライトモードとダークモードの両方で、移行前とまったく同じように動作します。
12. テスト
植物詳細画面の一部を Compose に移行した後は、何も問題がないことを確認するためのテストが不可欠です。
Sunflower では、androidTest
フォルダにある PlantDetailFragmentTest
によってアプリの一部の機能がテストされます。このファイルを開き、現在のコードを確認します。
testPlantName
は、画面上の植物の名前を確認しますtestShareTextIntent
は、共有ボタンをタップした後に正しいインテントがトリガーされることを確認します
アクティビティやフラグメントで Compose を使用する場合、ActivityScenarioRule
を使用するのではなく、ActivityScenarioRule
を ComposeTestRule
と統合する createAndroidComposeRule
を使用する必要があります。これにより、Compose コードをテストできるようになります。
PlantDetailFragmentTest
で、ActivityScenarioRule
の使用を createAndroidComposeRule
に置き換えます。テストを設定するためにアクティビティ ルールが必要な場合は、次のように createAndroidComposeRule
の activityRule
属性を使用します。
@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()
}
テストを実行すると、すべて合格します。
13. 完了
これで、この Codelab は終了です。
元の Sunflower GitHub プロジェクトの compose
ブランチは、植物詳細画面を Compose に完全に移行しています。この Codelab で行ったこととは別に、CollapsingToolbarLayout の動作もシミュレートします。これには以下が含まれます。
- Compose による画像の読み込み
- アニメーション
- ディメンション処理の向上
- その他
次のステップ
Compose パスウェイに関する他の Codelab をご確認ください。
参考資料
- Jetpack Compose の移行についての Code-Along 動画
- 既存のビューベースのアプリの移行についてのドキュメント ページ