본문 바로가기
안드로이드(kotlin)

안드로이드 XML로 된 프로젝트를 Compose로 변환하기

by 기계공학 주인장 2024. 10. 21.
반응형

새로 만드는 앱은 Compose로 하는 경우가 많지만 기존 앱들은 아직 XML로 만드는 케이스가 많다고 생각합니다.

 

하지만, 이제는 Compose에 대한 업데이트만 있기 때문에 Compose로 변환 작업을 하려는 사람이 많다고 생각하는데요.

 

이번 포스팅에서 안드로이드 XML로 된 프로젝트를 Compose로 변환하는 방법에 대해 알아보겠습니다.

 


XML에서 Compose로 변경하기 위한 Smaple 프로젝트 준비하기

 

친절하게 Google에서는 XML에서 Compose로의 변환을 위해 Sample 데이터를 만들어 놨습니다.

 

아래의 GitHub에서 해당 샘플 데이터를 받을 수 있습니다.

 

https://github.com/android/codelab-android-compose?tab=readme-ov-file

 

GitHub - android/codelab-android-compose

Contribute to android/codelab-android-compose development by creating an account on GitHub.

github.com

 

다음 스텝으로 넘어가기 전에 해당 프로젝트를 다운로드하여서

 

어떤 기능을 갖고 있는지 앱인지 한 번 실행해 보는 것을 추천합니다.

 


필요한 Depedency 설치하기

 

Compose로 변환을 하기 위해서는 Compose에서 필요한 Depedency를 설치하는 작업이 필요합니다.

 

다음과 같이 Depedency를 버전에 맞게 설치해 줍니다.

 

android {
    //...
    kotlinOptions {
        jvmTarget = '17'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.14"
    }
}

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-preview"
    debugImplementation "androidx.compose.ui:ui-tooling"
    //...
}

 

좀 더 최신 버전이 있을 거라 생각되지만,

 

Compose 라이브러리는 다른 Compose 라이브러리와 버전을 잘 맞춰줘야 하기 때문에 주의가 필요합니다.

 

https://developer.android.com/develop/ui/compose/bom

 

재료명세서 사용  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 재료명세서 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 재료명세서 (BOM)를 사용하면 모

developer.android.com

 

https://developer.android.com/jetpack/androidx/releases/compose-kotlin

 

Compose와 Kotlin의 호환성 지도  |  Jetpack  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose와 Kotlin의 호환성 지도 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 참고: Kotlin 2.0 이상을 사

developer.android.com

 

https://developer.android.com/develop/ui/compose/bom/bom-mapping

 

BOM과 라이브러리 버전 매핑  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. BOM과 라이브러리 버전 매핑 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 선택하기 2024년 9월 1일 2024

developer.android.com

 

라이브러리 버전에 대한 정보는 위 공식 문서들은 영어로 보는 것을 추천드립니다.

 

한글로 볼 경우 이전 버전의 문서를 보여주거나 아얘 내용이 들어있지 않은 경우도 있습니다.


안드로이드 XML을 Compose로 변경하기

 

이제 본격적으로 XML을 Compose로 변경해 보겠습니다.

 

1. fragment_plant_detail.xml에 있는 다음 코드를 삭제합니다.

 

제일 처음에는 그냥 삭제보다는 전부 주석 처리하는 것을 추천합니다.

그 이유는 나중에 어떤 레이아웃이 있었는지 확인하면서 Compose 코드를 작성해야 하기 때문입니다.

 

<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"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            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"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Apple" />
    
        <TextView
            android:id="@+id/plant_watering_header"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            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"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/plant_detail_name" />
    
        <TextView
            android:id="@+id/plant_watering"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_small"
            android:layout_marginEnd="@dimen/margin_small"
            android:gravity="center_horizontal"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/plant_watering_header"
            app:wateringText="@{viewModel.plant.wateringInterval}"
            tools:text="every 7 days" />
    
        <TextView
            android:id="@+id/plant_description"
            style="?android:attr/textAppearanceMedium"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/margin_small"
            android:layout_marginTop="@dimen/margin_small"
            android:layout_marginEnd="@dimen/margin_small"
            android:minHeight="@dimen/plant_description_min_height"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/plant_watering"
            app:renderHtml="@{viewModel.plant.description}"
            tools:text="Details about the plant" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

 

 

2. 그리고 삭제한 부분에 다음과 같은 Compose View를 넣어줍니다.

<androidx.compose.ui.platform.ComposeView
                android:id="@+id/compose_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

 

 

3. 삭제한 부분을 PlantDetailFragment.kt에서 Compose로 보여주도록 다음과 같은 코드를 작성하고 앱을 실행해서 화면을 확인합니다.

 

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

composeView.setContent {
                MaterialTheme{
                    PlantDetailDescription()
                }
            }

 

다음과 같은 화면이 되었다면 성공입니다.

 

 

4. 이제 기존에 삭제했던 View를 전부 Compose로 추가합니다. 데이터 또한 ViewModel을 사용해서 출력하도록 합니다.

 

PlantDetailFragment.kt에 다음과 같은 코드를 작성합니다.

 

단, 기존의 XML을 사용한 TextView에서 app:renderHtml 기능이 들어있던 TextView가 있었는데.

 

해당 기능은 Compose로 구현이 불가능하기 때문에 기존과 같은 TextView를 Compose로 구현하도록 해보겠습니다.

 

AndroidView를 사용하면 Compose를 사용하여 기존의 XML의 View를 구현할 수 있습니다.

 

@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
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.headlineMedium,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

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

@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.primary,
            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)
        )
    }
}

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

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

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

 

위와 같이 구현을 마쳤으면 Preview에서 다음과 같은 화면 출력을 얻을 수 있습니다.

 

 

5. 위에서 사용한 AndroidView의 경우 코드로 직접 Lifecycle에 따른 데이터 생성 / 삭제를 해줘야 하기 때문에 해당 기능을 추가해줍니다.

 

composeView.apply {
    // Clear TextView data based on lifecycle.
    setViewCompositionStrategy(
        ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
    )
    setContent {
        MaterialTheme {
            PlantDetailDescription(plantDetailViewModel)
        }
    }
}

 


결과

다음과 같은 결과를 얻을 수 있습니다.

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글