1. 소개
Compose와 뷰 시스템이 함께 작동할 수 있습니다.
이 Codelab에서는 Sunflower의 식물 세부정보 화면 일부를 Compose로 이전합니다. 실제 같은 앱을 Compose로 이전해 볼 수 있도록 프로젝트 사본을 만들었습니다.
Codelab을 마치고 난 후 원하는 경우 이전을 계속 진행하여 Sunflower의 화면 나머지를 전환할 수 있습니다.
이 Codelab을 진행하는 동안 추가 지원을 받으려면 다음 코드를 함께 체크아웃하세요.
학습할 내용
이 Codelab에서는 다음에 관해 알아봅니다.
- 선택할 수 있는 다양한 이전 경로
- 앱을 Compose로 점진적으로 이전하는 방법
- 뷰를 사용하여 빌드된 기존 화면에 Compose를 추가하는 방법
- Compose 내에서 뷰를 사용하는 방법
- Compose에서 테마를 만드는 방법
- 뷰와 Compose가 혼합되어 작성된 화면을 테스트하는 방법
기본 요건
- 람다를 포함한 Kotlin 문법 사용 경험
- Compose 기본사항에 관한 지식
필요한 항목
2. 이전 전략
Jetpack Compose는 처음부터 뷰 상호 운용성을 고려하여 설계되었습니다. Compose로 이전하려면 앱이 Compose로 완전히 이전될 때까지 Compose와 뷰가 코드베이스에 공존하는 증분 이전을 사용하는 것이 좋습니다.
권장되는 이전 전략은 다음과 같습니다.
- Compose를 사용하여 새 화면 빌드
- 기능을 빌드하면서 재사용 가능한 요소를 식별하고 공통 UI 구성요소의 라이브러리 만들기
- 기존 기능을 한 번에 한 화면씩 대체
Compose를 사용하여 새 화면 빌드
Compose를 사용하여 전체 화면이 포함된 새로운 기능을 빌드하는 것은 Compose 채택을 유도하는 가장 좋은 방법입니다. 이 전략을 사용하면 기능을 추가하고 Compose의 이점을 활용하면서 회사의 비즈니스 요구사항도 충족할 수 있습니다.
새로운 기능에는 전체 화면이 포함될 수도 있으며, 이 경우 전체 화면이 Compose에 있습니다. 프래그먼트 기반 탐색을 사용 중인 경우에는 새 프래그먼트를 만들고 콘텐츠를 Compose에 포함합니다.
기존 화면에 새로운 기능을 도입할 수도 있습니다. 이 경우 뷰와 Compose가 동일한 화면에 공존합니다. 예를 들어 추가하는 기능이 RecyclerView의 새로운 뷰 유형이라고 가정하겠습니다. 이러한 새 뷰 유형은 Compose에 있지만 다른 항목은 동일하게 유지됩니다.
공통 UI 구성요소의 라이브러리 빌드
Compose를 사용하여 기능을 빌드하다 보면 결국 구성요소 라이브러리를 빌드하게 됩니다. 공유 구성요소가 단일 정보 소스를 가지도록 앱 전체에서 재사용을 촉진할 수 있는 재사용 가능한 구성요소를 파악하는 것이 좋습니다. 그러면 빌드하는 새 기능이 이 라이브러리에 종속될 수 있습니다.
기존 기능을 Compose로 교체
새로운 기능을 빌드하는 것 외에도 앱의 기존 기능을 Compose로 점진적으로 이전하는 것이 좋습니다. 접근하는 방식은 개발자가 선택할 수 있지만, 적합한 옵션은 다음과 같습니다.
- 간단한 화면: 일부 UI 요소와 시작 화면, 확인 화면, 설정 화면 같은 역동성이 있는 앱의 간단한 화면입니다. 코드 몇 줄로 작성할 수 있으므로 Compose로 이전하는 데 적합한 방법입니다.
- 뷰와 Compose가 혼합된 화면: 이미 약간의 Compose 코드가 포함되어 있는 화면은 계속해서 이 화면에 있는 요소를 조금씩 이전할 수 있으므로 또 다른 적합한 옵션입니다. Compose에 하위 트리만 있는 화면이 있는 경우 전체 UI가 Compose에 속할 때까지 트리의 다른 부분을 계속 이전할 수 있습니다. 이를 상향식 이전 접근 방식이라고 합니다.
이 Codelab의 접근 방식
이 Codelab에서는 Sunflower의 식물 세부정보 화면을 Compose로 점진적으로 이전하여 Compose와 뷰가 공존하게 할 것입니다. 이후 원하면 이전을 계속 진행할 수 있습니다.
3. 설정
코드 가져오기
GitHub에서 Codelab 코드를 가져옵니다.
$ git clone https://github.com/android/codelab-android-compose
또는 저장소를 ZIP 파일로 다운로드할 수 있습니다.
샘플 앱 실행
다운로드한 코드에는 사용 가능한 모든 Compose Codelab용 코드가 포함되어 있습니다. 이 Codelab을 완료하려면 Android 스튜디오 내에서 MigrationCodelab
프로젝트를 엽니다.
이 Codelab에서는 Sunflower의 식물 세부정보 화면을 Compose로 이전합니다. 식물 목록 화면에서 식물 하나를 탭하여 식물 세부정보 화면을 열 수 있습니다.
프로젝트 설정
프로젝트는 여러 git 브랜치로 빌드됩니다.
main
브랜치는 Codelab의 시작점입니다.end
브랜치에는 이 Codelab의 솔루션이 포함되어 있습니다.
main
브랜치의 코드로 시작하고 각자의 속도에 맞게 Codelab을 단계별로 따라하는 것이 좋습니다.
Codelab을 진행하는 중에 프로젝트에 추가해야 하는 코드 스니펫이 제공됩니다. 코드 스니펫의 댓글에 명시된 코드를 삭제해야 하는 경우도 있을 수 있습니다.
git을 사용하여 end
브랜치를 가져오려면 cd
를 MigrationCodelab
프로젝트의 디렉터리에 넣고 다음 명령어를 사용합니다.
$ git checkout end
또는 다음 위치에서 솔루션 코드를 다운로드합니다.
자주 묻는 질문(FAQ)
4. Sunflower의 Compose
main
브랜치에서 다운로드한 코드에 Compose가 이미 추가되어 있습니다. 하지만 제대로 작동하기 위해 필요한 사항을 살펴보겠습니다.
앱 수준 build.gradle
파일을 연 경우 Compose 종속 항목을 가져오는 방법을 확인하고 buildFeatures { compose true }
플래그를 사용하여 Android 스튜디오가 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:2024.09.02')
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.material3:material3"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
//...
}
이러한 종속 항목의 버전은 프로젝트 수준 build.gradle
파일에 정의되어 있습니다.
5. Compose 시작
식물 세부정보 화면에서는 식물의 설명을 Compose로 이전하되 화면의 전체 구조는 그대로 둡니다.
Compose에서 UI를 렌더링하려면 호스트 활동 또는 프래그먼트가 필요합니다. Sunflower에서는 모든 화면이 프래그먼트를 사용하므로 setContent
메서드를 사용하여 Compose UI 콘텐츠를 호스팅할 수 있는 Android 뷰인 ComposeView
를 사용하게 됩니다.
XML 코드 삭제
이전을 시작해 보겠습니다. fragment_plant_detail.xml
을 열고 다음 안내를 따르세요.
- 코드 보기로 전환합니다.
NestedScrollView
내에서ConstraintLayout
코드 및 중첩된 4개의TextView
를 삭제합니다(Codelab에서는 개별 항목을 이전할 때 XML 코드를 비교하고 참조하므로 코드를 주석 처리하면 유용함).- 대신 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로 이전할 준비가 되었습니다.
Codelab 전반에 걸쳐 plantdetail
폴더 아래 PlantDetailDescription.kt
파일에 Compose 코드를 추가합니다. 파일을 열어 자리표시자 "Hello Compose"
텍스트를 프로젝트에서 바로 사용할 수 있는 것을 확인할 수 있습니다.
PlantDetailDescription.kt
@Composable
fun PlantDetailDescription() {
Surface {
Text("Hello Compose")
}
}
이전 단계에서 추가한 ComposeView
에서 이 컴포저블을 호출하여 화면에 표시해 보겠습니다. PlantDetailFragment.kt
를 엽니다.
화면에서 데이터 결합을 사용하므로 composeView
에 직접 액세스하고 setContent
를 호출하여 화면에 Compose 코드를 표시할 수 있습니다. Sunflower에서 Material Design을 사용하므로 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.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.margin_small))
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
@Preview
@Composable
private fun PlantNamePreview() {
MaterialTheme {
PlantName("Apple")
}
}
미리보기는 다음과 같습니다.
여기서
Text
의 스타일은MaterialTheme.typography.headlineSmall
로, XML 코드의textAppearanceHeadline5
와 유사합니다.- 수정자는 XML 버전처럼 보이도록 텍스트를 장식합니다.
fillMaxWidth
수정자는 사용 가능한 최대 너비를 차지하도록 사용됩니다. 이 수정자는 XML 코드에서layout_width
속성의match_parent
값에 해당합니다.padding
수정자를 사용하여 가로 패딩 값margin_small
을 적용합니다. 이 값은 XML의marginStart
및marginEnd
선언에 해당합니다.margin_small
값은dimensionResource
도우미 함수를 사용하여 가져오는 기존 크기 리소스이기도 합니다.wrapContentWidth
수정자는 텍스트가 가로로 화면 가운데 표시되도록 하는 데 사용됩니다. XML에서gravity
가center_horizontal
인 것과 유사합니다.
7. ViewModel 및 LiveData
이제 제목을 화면에 연결해 보겠습니다. 이렇게 하려면 PlantDetailViewModel
을 사용하여 데이터를 로드해야 합니다. 이를 위해 Compose는 ViewModel 및 LiveData 통합 기능을 지원합니다.
ViewModel
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)
}
}
PlantDetailContent
는 PlantName
을 호출하기 때문에 PlantNamePreview
는 직접 업데이트할 필요 없이 변경사항을 반영합니다.
이제 Compose에 식물 이름이 표시되도록 ViewModel에 연결했습니다. 다음 몇 개의 섹션에서 나머지 컴포저블을 빌드하고 비슷한 방식으로 ViewModel에 연결할 것입니다.
8. 추가 XML 코드 이전
이제 UI에서 누락된 정보, 즉 물주기 정보와 식물 설명을 더 쉽게 완성할 수 있습니다. 이전과 비슷한 접근 방식을 따르면 화면의 나머지 부분도 이전할 수 있습니다.
이전에 fragment_plant_detail.xml
에서 삭제한 물주기 정보 XML 코드는 ID가 plant_watering_header
및 plant_watering
인 두 개의 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.colorScheme.primaryContainer,
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
)에 할당하여 수정자를 재사용할 수 있습니다. 수정자는 일반 Kotlin 객체이므로 가능합니다. - Compose의
MaterialTheme
이plant_watering_header
에 사용되는colorAccent
와 정확하게 일치하지 않습니다. 일단은MaterialTheme.colorScheme.primaryContainer
를 사용하고 상호 운용성 테마 설정 섹션에서 개선하도록 하겠습니다. - Compose 1. 2.1에서
pluralStringResource
를 사용하려면ExperimentalComposeUiApi
를 선택해야 합니다. Compose의 향후 버전에서는 이 작업이 더 이상 필요하지 않을 수 있습니다.
모두 서로 연결하고 PlantDetailContent
에서 PlantWatering
도 호출합니다. 처음에 삭제한 ConstraintLayout XML 코드는 Compose 코드에 포함해야 하는 16.dp
의 여백을 갖고 있었습니다.
<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 코드의 뷰
이제 식물 설명을 이전해 보겠습니다. fragment_plant_detail.xml
의 코드에는 app:renderHtml="@{viewModel.plant.description}"
을 포함하는 TextView
가 있어 화면에 표시할 텍스트를 XML에 알려줍니다. renderHtml
은 PlantDetailBindingAdapters.kt
파일에서 찾을 수 있는 결합 어댑터입니다. 구현은 HtmlCompat.fromHtml
을 사용하여 TextView
에 텍스트를 설정합니다.
그러나 Compose는 현재 Spanned
클래스를 지원하지 않으며 HTML 형식 텍스트도 표시하지 않습니다. 따라서 이 제한을 우회하려면 Compose 코드에서 뷰 시스템의 TextView
를 사용해야 합니다.
Compose는 아직 HTML 코드를 렌더링할 수 없으므로 프로그래매틱 방식으로 TextView
를 만들어 AndroidView
API를 사용하여 정확히 렌더링을 해야 합니다.
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
람다 내에서 상태를 읽게 되면 재구성됩니다.
PlantDetailContent
컴포저블에 PlantDescription
을 추가하고 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
Compose는 ComposeView
가 창에서 분리될 때마다 컴포지션을 삭제합니다. 다음 두 가지 이유로 프래그먼트에서 ComposeView
가 사용될 때에는 바람직하지 않습니다.
- 컴포지션은 Compose UI
View
유형을 위한 프래그먼트의 뷰 수명 주기에 따라 상태를 저장해야 합니다. - 전환이 발생할 때 기본
ComposeView
가 분리된 상태가 됩니다. 그러나 이러한 전환 중에도 Compose UI 요소는 계속 표시됩니다.
이 동작을 수정하려면 프래그먼트의 뷰 수명 주기를 따르도록 적절한 ViewCompositionStrategy
로 setViewCompositionStrategy
를 호출합니다. 특히 DisposeOnViewTreeLifecycleDestroyed
전략을 사용하여 프래그먼트의 LifecycleOwner
가 소멸되면 컴포지션을 삭제하려고 합니다.
이렇게 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. Material Theming
식물 세부정보의 텍스트 콘텐츠가 Compose로 이전되었습니다. 그러나 Compose가 알맞은 테마 색상을 사용하고 있지 않을 수도 있습니다. 식물 이름에는 녹색을 사용해야 하는데 보라색을 사용하고 있습니다.
올바른 테마 색상을 사용하려면 자체 테마를 정의하고 테마의 색상을 제공하여 MaterialTheme
를 맞춤설정해야 합니다.
MaterialTheme
맞춤설정
나만의 테마를 만들려면 theme
패키지에서 Theme.kt
파일을 엽니다. Theme.kt
는 콘텐츠 람다를 허용하고 이를 MaterialTheme
에 전달하는 SunflowerTheme
라는 컴포저블을 정의합니다.
아직 흥미로운 동작이 실행된 것은 없습니다. 다음에 맞춤설정하게 됩니다.
Theme.kt
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@Composable
fun SunflowerTheme(
content: @Composable () -> Unit
) {
MaterialTheme(content = content)
}
MaterialTheme
를 사용하면 색상, 서체, 도형을 맞춤설정할 수 있습니다. 지금은 Sunflower View의 테마에 동일한 색상을 제공하여 색상을 맞춤설정합니다. SunflowerTheme
는 시스템이 어두운 모드이면 기본값이 true
이고 그렇지 않으면 false
인 darkTheme
라는 불리언 매개변수를 허용할 수도 있습니다. 이 매개변수를 사용할 경우 올바른 색상 값을 MaterialTheme
에 전달하여 현재 설정된 시스템 테마와 일치시킬 수 있습니다.
Theme.kt
@Composable
fun SunflowerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val lightColors = lightColorScheme(
primary = colorResource(id = R.color.sunflower_green_500),
primaryContainer = 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 = darkColorScheme(
primary = colorResource(id = R.color.sunflower_green_100),
primaryContainer = 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(
colorScheme = colors,
content = content
)
}
이 속성을 사용하려면 SunflowerTheme
에 관한 MaterialTheme
사용을 대체합니다. 예를 들어 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
을 사용하는 대신 ComposeTestRule
을 ActivityScenarioRule
과 통합하는 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을 확인하세요.