새로 만드는 앱은 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
다음 스텝으로 넘어가기 전에 해당 프로젝트를 다운로드하여서
어떤 기능을 갖고 있는지 앱인지 한 번 실행해 보는 것을 추천합니다.
필요한 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
https://developer.android.com/jetpack/androidx/releases/compose-kotlin
https://developer.android.com/develop/ui/compose/bom/bom-mapping
라이브러리 버전에 대한 정보는 위 공식 문서들은 영어로 보는 것을 추천드립니다.
한글로 볼 경우 이전 버전의 문서를 보여주거나 아얘 내용이 들어있지 않은 경우도 있습니다.
안드로이드 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)
}
}
}
결과
다음과 같은 결과를 얻을 수 있습니다.
'안드로이드(kotlin)' 카테고리의 다른 글
안드로이드 특정 Cookie 값을 얻고 setCookie로 삭제하는 방법 (0) | 2024.10.10 |
---|---|
안드로이드 현재 액티비티 Stack 확인하기 (0) | 2023.12.27 |
DiffUtil를 BaseAdapter로 하여 쉽게 RecyclerView만들기 (1) | 2023.11.12 |
Unsupported Java. Your build is currently configured to use Java 20.0.1 and Gradle 8.0. (0) | 2023.05.31 |
안드로이드 코틀린 EncryptedSharedPreferences 사용 방법 (0) | 2023.05.28 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글