기존 UI와 Compose 통합

뷰 기반의 UI를 사용하는 앱의 경우 전체 UI를 한 번에 재작성하지 않는 것이 좋습니다. 이 페이지에서는 새 Compose 요소를 기존 UI에 추가하는 방법을 설명합니다.

공유된 UI 이전

Compose로 점진적으로 이전하는 경우 공유된 UI 요소를 Compose와 뷰 시스템에 모두 사용해야 할 수 있습니다. 예를 들어 앱에 맞춤 CallToActionButton 구성요소가 있으면 Compose와 뷰 기반 화면에 모두 이 구성요소를 사용해야 할 수 있습니다.

Compose에서 공유된 UI 요소는 그 요소가 XML을 사용하여 스타일을 지정된 것인지 아니면 맞춤 뷰인지에 관계없이 앱 전체에서 재사용할 수 있는 컴포저블이 됩니다. 예를 들어 맞춤 클릭 유도문안 Button 구성요소와 관련해 CallToActionButton 컴포저블을 만들 수 있습니다.

뷰 기반 화면에서 컴포저블을 사용하려면 AbstractComposeView에서 확장되는 맞춤 뷰 래퍼를 만들어야 합니다. 재정의된 Content 컴포저블의 경우 생성한 컴포저블을 아래 예에서와 같이 Compose 테마에 래핑된 상태로 둡니다.

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

구성 가능한 매개변수가 맞춤 뷰 내에서 변경 가능한 변수가 되는 것을 알 수 있습니다. 이렇게 하면 맞춤 CallToActionViewButton 뷰가 기존 뷰처럼 뷰 결합 등을 통해 확장 및 사용 가능하게 됩니다. 아래 예를 참고하시기 바랍니다.

class ExampleActivity : Activity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.something)
            onClick = { /* Do something */ }
        }
    }
}

맞춤 구성요소에 변경 가능한 상태가 포함된 경우 상태 정보 소스를 참고하세요.

테마 설정

Android 앱 테마를 설정하려면 머티리얼 디자인에 따라 Android용 머티리얼 디자인 구성요소(MDC) 라이브러리를 사용하는 것이 좋습니다. Compose 테마 설정 문서에 나와 있는 것처럼, Compose는 MaterialTheme 컴포저블을 사용하여 이러한 개념을 구현합니다.

Compose에서 새 화면을 만드는 경우 MaterialTheme를 적용한 다음, 머티리얼 구성요소 라이브러리에서 UI를 내보내는 컴포저블을 적용합니다. 머티리얼 구성요소(Button, Text 등)는 설정된 MaterialTheme에 종속되며 동작도 이 항목이 없으면 정의되지 않습니다.

모든 Jetpack Compose 샘플MaterialTheme를 기반으로 빌드된 맞춤 Compose 테마를 사용합니다.

여러 정보 소스

기존 앱에는 뷰를 위한 테마 및 스타일 설정이 상당히 많을 수 있습니다. 기존 앱에 Compose를 도입할 경우 Compose 화면에 MaterialTheme를 사용하려면 테마를 이전해야 합니다. 그러면 앱의 테마 설정은 뷰 기반 테마와 Compose 테마라는 두 가지 정보 소스를 갖습니다. 스타일 설정 변경은 여러 위치에서 이루어져야 합니다.

앱을 Compose로 완전히 이전할 계획이라면 결국에는 기존 테마의 Compose 버전을 생성해야 합니다. 문제는 개발 과정에서 Compose 테마를 일찍 생성하면 할수록 개발 중에 유지관리 작업을 더 많이 해야 한다는 점입니다.

MDC Compose 테마 어댑터

Android 앱에서 MDC 라이브러리를 사용하는 경우, MDC Compose 테마 어댑터 라이브러리를 통해 기존 뷰 기반 테마의 색상, 글꼴모양 테마를 컴포저블에 쉽게 재사용할 수 있습니다.

import com.google.android.material.composethemeadapter.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

자세한 내용은 MDC 라이브러리 문서를 참고하세요.

AppCompat Compose 테마 어댑터

AppCompat Compose 테마 어댑터 라이브러리를 사용하면 AppCompat XML 테마를 쉽게 재사용해 Jetpack Compose에 테마를 지정할 수 있습니다. 이 라이브러리는 컨텍스트의 테마에 있는 색상글꼴 값을 사용해 MaterialTheme를 만듭니다.

import com.google.accompanist.appcompattheme.AppCompatTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

기본 구성요소 스타일

MDC와 AppCompat Compose 테마 어댑터 라이브러리는 모두 테마가 정의된 기본 위젯 스타일을 읽지 않습니다. 이는 Compose에 기본 컴포저블의 개념이 없기 때문입니다.

구성요소 스타일맞춤 디자인 시스템에 관한 자세한 내용은 테마 설정 문서를 참고하세요.

Compose에서 테마 오버레이

뷰 기반 화면을 Compose로 이전할 때 android:theme 속성을 사용하는 것에 유의하세요. Compose UI 트리의 관련 부분에 새로운 MaterialTheme가 필요할 수 있습니다.

이에 관한 자세한 내용은 테마 설정 가이드를 참고하세요.

WindowInsets 및 IME 애니메이션

WindowInsetsaccompanist-insets 라이브러리를 사용하여 처리할 수 있습니다. 이 라이브러리는 레이아웃 내에서 이를 처리할 수 있는 컴포저블과 수정자를 제공하고 IME 애니메이션을 지원합니다.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

키보드 표시 공간을 위해 UI 요소를 위아래로 스크롤하는 모습을 보여주는 애니메이션

그림 2. accompanist-insets 라이브러리를 사용하는 IME 애니메이션.

자세한 내용은 accompanists-insets 라이브러리 문서를 참고하세요.

화면 크기 변경 처리

화면 크기에 따라 서로 다른 XML 레이아웃을 사용하는 앱을 이전하는 경우 BoxWithConstraints 컴포저블을 사용하여 컴포저블이 차지할 수 있는 최소 및 최대 크기를 지정합니다.

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

뷰를 사용한 중첩 스크롤

View 시스템과 Jetpack Compose 간의 중첩 스크롤은 아직 사용할 수 없습니다. 이 Issue Tracker 버그에서 관련 진행 상황을 확인할 수 있습니다.

RecyclerView의 Compose

Jetpack Compose는 DisposeOnDetachedFromWindow를 기본 ViewCompositionStrategy로 사용합니다. 즉, 뷰가 창에서 분리될 때마다 Composition이 삭제됩니다.

ComposeViewRecyclerView 뷰 홀더의 일부로 사용할 경우, RecyclerView가 창에서 분리될 때까지 기본 Composition 인스턴스가 메모리에 남아 있기 때문에 기본 전략은 비효율적입니다. RecyclerView가 더 이상 ComposeView에 필요하지 않은 경우 기본 Composition을 삭제하는 것이 좋습니다.

disposeComposition 함수를 사용하면 ComposeView의 기본 Composition을 수동으로 삭제할 수 있습니다. 다음과 같이 뷰가 재활용될 때 이 함수를 호출할 수 있습니다.

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

상호 운용성 API 가이드ComposeView의 ViewCompositionStrategy 섹션에 설명된 것처럼 Compose 뷰 홀더가 모든 시나리오에서 작동하도록 하려면 DisposeOnViewTreeLifecycleDestroyed 전략을 사용해야 합니다.

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

RecyclerView에 사용된 ComposeView의 작동 모습을 보려면 Sunflower 앱의 compose_recyclerview 분기를 확인하세요.