遷移至 Jetpack Compose

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

1. 簡介

Compose 和 View 系統可以同時搭配使用。

在本程式碼研究室中,您會將 Sunflower 植物詳細資料畫面的一部分內容遷移至 Compose。我們建立了一份專案副本,協助您嘗試將實際的應用程式遷移至 Compose。

完成本程式碼研究室後,您可以繼續進行遷移作業,並視需要轉換 Sunflower 的其餘畫面。

如果您在閱讀本程式碼研究室時需要更多支援,請參閱下列程式碼:

學習目標

在本程式碼研究室,您將學到:

  • 可採用的不同遷移路徑
  • 如何將應用程式逐步遷移至 Compose
  • 如何將 Compose 新增至使用 Android View 所建立的現有畫面
  • 如何在 Compose 中使用 Android View
  • 如何在 Compose 中使用 View 系統的主題
  • 如何使用 View 系統和 Compose 程式碼來測試畫面

必要條件

必備軟硬體

2. 規劃遷移作業

遷移至 Compose 的方式取決於您和您的團隊。將 Jetpack Compose 整合到現有 Android 應用程式中的方法有很多種,兩項常見的遷移策略為:

  • 完全使用 Compose 開發新的畫面
  • 使用現有畫面,並逐步遷移其元件。

新畫面中的 Compose

將您的應用程式重構為新技術的常見方法,就是在為應用程式開發的新功能中採用這種技術。如此一來,就可以套用新的畫面。如果您需要為應用程式建立新的 UI 畫面,不妨使用 Compose,並且讓其他應用程式繼續保留在 View 系統中。

在這種情況下,您將在這些已遷移功能的邊緣實現 Compose 互通性。

同時使用 Compose 及 View

針對畫面,您可以將部分元件遷移到 Compose,其他部分則留在 View 系統中。舉例來說,您可以遷移 RecyclerView,同時將畫面其他部分保留在 View 系統。

您也可以使用 Compose 做為外部版面配置,並使用 Compose 中可能無法使用的某些現有檢視,例如 MapView 或 AdView。

完成遷移

一次將整個片段或畫面移轉至 Compose。這個方法最簡單,但相當不精細。

本程式碼研究室的目標?

在本程式碼研究室中,您會將 Sunflower 植物詳細畫面逐步遷移至 Compose,讓 Compose 和 View 互相搭配運作。完成後,您就可以視需要繼續進行遷移作業。

3. 開始設定

取得程式碼

從 GitHub 取得程式碼研究室程式碼:

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

或者,您也可以將存放區下載為 ZIP 檔案:

執行範例應用程式

您剛才下載的程式碼包含所有 Compose 程式碼研究室可用的程式碼。如要完成本程式碼研究室,請在 Android Studio 中開啟 MigrationCodelab 專案。

在這個程式碼研究室中,您會將「向日葵」植物詳細資料畫面遷移到 Compose。輕觸植物詳細資料畫面上的任一植物,即可開啟植物詳細資訊畫面。

bb6fcf50b2899894.png

專案設定

專案在多個 Git 分支版本中建構而成:

  • main 是您查看或下載的分支版本。這是程式碼研究室的起點。
  • end 包含本程式碼研究室的解決方案。

建議您先從 main 分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。

在本程式碼研究室的學習過程中,我們會為您提供要新增到專案的程式碼片段。在某些地方,您還需要移除程式碼片段註解中明確提及的程式碼。

如要使用 Git 取得 end 分支版本,請使用下列指令:

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

或者,您也可以從以下位置下載解決方案程式碼:

常見問題

4. Sunflower 中的 Compose

Compose 已新增到您從 main 分支版本下載的程式碼中。不過,讓我們看看這個 Compose 需要滿足哪些條件才能運作。

如果您開啟應用程式層級的 build.gradle (或專案層級的 build.gradle) 檔案,請查看該檔案如何匯入 Compose 依附元件,以及如何使用 buildFeatures { compose true } 標記,讓 Android Studio 能夠與 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. Hello Compose!

在植物詳細資訊畫面中,我們會把植物的說明遷移至 Compose,同時完整保留畫面的整體結構。這個部分將採用「規劃遷移作業」一節說明的「同時使用 Compose 及 View」遷移策略。

Compose 需要主機 Activity (活動) 或 Fragment (片段),才能轉譯 UI。在 Sunflower 中,由於所有畫面都是使用片段,因此您會使用 ComposeView,也就是能透過其 setContent 方法代管 Compose UI 內容的 Android View。

移除 XML 程式碼

讓我們開始遷移作業!開啟 fragment_plant_detail.xml,然後遵循下列步驟:

  1. 切換至程式碼檢視模式
  2. 移除 NestedScrollView 中的 ConstraintLayout 程式碼和巢狀 TextView (程式碼研究室在遷移個別項目時會比較並參照 XML 程式碼,為程式碼加上註解會很實用)
  3. 新增可代管 Compose 程式碼的 ComposeView 做為資料檢視 ID,而不使用 compose_view

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!

在本程式碼研究室的整個流程中,您都會將程式碼新增至 plantdetail 資料夾的的 PlantDetailDescription.kt 檔案。請開啟該檔案,看看我們如何在專案中預先提供預留位置的 "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」。

66f3525ecf6669e0.png

6. 透過 XML 建立可組合函式

首先遷移植物的名稱。更確切來說,這個名稱是指您在 fragment_plant_detail.xml 中移除的 TextView (ID 為 @+id/plant_detail_name)。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"
  • margin_small 的水平 padding 是來自 View 系統的值,使用 dimensionResource 輔助函式。
  • wrapContentWidth 會水平對齊 Text

7. ViewModels 和 LiveData

現在,讓我們將標題連接至畫面。如要這麼做,您需要使用 PlantDetailViewModel 載入資料。為此,Compose 整合了 ViewModelLiveData

ViewModels

由於 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)
    }
}

PlantDetailContent 目前只會呼叫 PlantName,因此預覽畫面與 PlantNamePreview 相同:

3e47e682cf518c71.png

您現在已完成連結 ViewModel,可在 Compose 中顯示植物名稱。在接下來幾個小節中,您將要建立其他可組成函式,並以類似的方式將這些函式連接至 ViewModel。

8. 其他 XML 程式碼遷移作業

現在,您可以更輕鬆地完成 UI 中遺漏的內容:澆灌資訊和植物說明。您只要按照先前介紹的 XML 程式碼方法,就可以遷移畫面的其餘部分。

您從 fragment_plant_detail.xml 移除的澆灌資訊 XML 程式碼是由兩個 TextView 所組成,ID 分別為 plant_watering_headerplant_watering

<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)
    }
}

預覽如下:

6f6c17085801a518.png

請注意以下事項:

  • 由於水平邊框間距和對齊裝飾由 Text 可組合函式共用,因此您可以將修飾符指派至本機變數 (例如 centerWithPaddingModifier) 以重複使用該修飾符。由於修飾符是一般的 Kotlin 物件,因此可以重複使用。
  • Compose 的 MaterialTheme 並未與 plant_watering_header 中使用的 colorAccent 完全相符。現在,讓我們使用 MaterialTheme.colors.primaryVariant,您將在「互通性主題設定」一節中改進這個主題設定。
  • 在 Compose 1.2.1 中,您必須選擇啟用 ExperimentalComposeUiApi 才能使用 pluralStringResource。不過,在日後的 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)
        }
    }
}

重新整理預覽畫面後,您就會看到以下畫面:

56626a7118ce075c.png

9. Compose 程式碼中的 View

現在,讓我們來遷移植物說明。fragment_plant_detail.xml 中的程式碼包含具有 app:renderHtml="@{viewModel.plant.description}"TextView,可讓 XML 知道要在畫面上顯示哪些文字。renderHtml 是一個位於 PlantDetailBindingAdapters.kt 檔案中的繫結轉接器。實作程序會使用 HtmlCompat.fromHtml 來設定 TextView 上的文字!

不過,Compose 目前不支援 Spanned 類別或顯示 HTML 格式的文字。因此,我們需要在 Compose 程式碼中使用 View 系統中的 TextView,才能略過這項限制。

由於 Compose 尚不支援轉譯 HTML 程式碼,因此您可以使用 AndroidView API 以程式輔助的方式建立 TextView

AndroidView 會使用 View 做為參數,並在 View 已加載時向您傳送回呼。

讓我們建立一個新的 PlantDescription 可組合項。這個可組合項會使用我們剛才在 lambda 中記住的 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 更新回呼將會重新組合。回呼中讀取的任何狀態都會導致重新組合。

讓我們將 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)
    }
}

預覽如下:

7843a8d6c781c244.png

現在,您已經將原始 ConstraintLayout 中的所有內容遷移至 Compose。您可以執行應用程式以確認是否正常運作。

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

每當 ComposeView 從視窗卸離時,Compose 都會捨棄組合。如果在片段中使用 ComposeView,這並非理想的情況,原因如下:

  • 組合必須遵循片段的檢視區塊生命週期,Compose UI View 類型才能儲存狀態;且
  • 在轉換或視窗轉換時,將 Compose UI 元素保留在畫面上。轉換期間,仍可以檢視 ComposeView,即使已從視窗卸離也不受影響。

您可以手動呼叫 AbstractComposeView.disposeComposition 方法來手動處理 Composition (組合)。或者,如果要自動處置不再需要的 Composition (組合),請設定其他策略。或透過呼叫 setViewCompositionStrategy 方法來建立自己的策略。

當片段的 LifecycleOwner 遭到刪除時,使用 DisposeOnViewTreeLifecycleDestroyed 策略來處置 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 並未使用正確的主題色彩。其中的植物名稱使用紫色,但應該是綠色。

在這個早期的遷移階段,您可能會希望讓 Compose 繼承 View 系統提供的主題,而非從頭開始撰寫質感主題。質感主題能與 Compose 提供的質感設計元件完美搭配運作。

如要在 Compose 中重複使用 View 系統質感設計元件 (MDC) 主題,您可以使用 compose-theme-adapterMdcTheme 函式會自動讀取主機內容的 MDC 主題、為您傳送至 MaterialTheme,以用於淺色和深色主題。即使您只需要此程式碼研究室的主題色彩,這個程式庫也會讀取 View 系統的形狀和字體排版。

此程式庫「已經」納入 app/build.gradle 檔案中,如下所示:

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

如要使用此程式庫,請將 MdcTheme 的用法替換為 MaterialTheme。例如,在 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")
    }
}

如預覽畫面所示,MdcTheme 現在是從 styles.xml 檔案中擷取主題的色彩。

886d7eaea611f4eb.png

您也可以預覽深色主題的 UI,方法是建立新函式並將 Configuration.UI_MODE_NIGHT_YES 傳送至預覽的 uiMode

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) 時,您必須運用 createAndroidComposeRule 來整合 ActivityScenarioRuleComposeTestRule,藉此測試 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()
}

當您執行測試時,便會看到這些測試通過。

dd59138fac1740e4.png

13. 恭喜

恭喜,您已經成功完成本程式碼研究室!

原始 Sunflower GitHub 專案的 compose 分支版本可將植物的詳細資料畫面完全遷移至 Compose。除了您在本程式碼研究室中完成的工作之外,此版本也會模擬 CollapsingToolbarLayout 的行為。這些行為包括:

  • 使用 Compose 載入圖片
  • 動畫
  • 更完善的維度處理方式
  • 還有更多功能!

後續步驟

請參閱 Compose 課程中的其他程式碼研究室:

其他資訊