기존 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 앱 테마 설정을 위한 권장되는 디자인 시스템은 Material Design입니다.

뷰 기반 앱은 다음 세 가지 버전의 Material을 사용할 수 있습니다.

  • AppCompat 라이브러리를 사용하는 Material Design 1(예: Theme.AppCompat.*)
  • MDC-Android 라이브러리를 사용하는 Material Design 2(예: Theme.MaterialComponents.*)
  • MDC-Android 라이브러리를 사용하는 Material Design 3(예: Theme.Material3.*)

Compose 앱은 다음 두 가지 버전의 Material을 사용할 수 있습니다.

  • Compose Material 라이브러리를 사용하는 Material Design 2(예: androidx.compose.material.MaterialTheme)
  • Compose Material 3 라이브러리를 사용하는 Material Design 3(예: androidx.compose.material3.MaterialTheme)

앱의 디자인 시스템이 지원하는 경우 최신 버전(Material 3)을 사용하는 것이 좋습니다. 뷰 및 Compose를 위한 이전 가이드가 준비되어 있습니다.

Compose에서 새 화면을 만들 때는 사용 중인 Material Design 버전과 관계없이 먼저 MaterialTheme을 적용한 후에 Compose Material 라이브러리에서 UI를 내보내는 컴포저블을 적용해야 합니다. Material 구성요소(Button, Text 등)는 설정된 MaterialTheme에 종속되며 동작도 이 항목이 없으면 정의되지 않습니다.

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

자세한 내용은 Compose의 디자인 시스템을 참고하세요.

여러 정보 소스

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

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

MDC-Android Compose 테마 어댑터

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

Material 3을 사용 중인 경우에는 Mdc3Theme 컴포저블을 사용합니다.

import com.google.android.material.composethemeadapter3.Mdc3Theme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {
            // Use Mdc3Theme instead of M3 MaterialTheme
            // Color scheme, typography, and shapes have been read from the
            // View-based theme used in this Activity
            Mdc3Theme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

Material 2를 사용 중인 경우에는 MdcTheme 컴포저블을 사용합니다.

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

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {
            // Use MdcTheme instead of M2 MaterialTheme
            // Colors, typography, and shapes have been read from the
            // View-based theme used in this Activity
            MdcTheme {
                ExampleComposable(/*...*/)
            }
        }
    }
}

자세한 내용은 MDC-Android Compose 테마 어댑터 라이브러리 문서를 참고하세요.

AppCompat Compose 테마 어댑터

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

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppCompatTheme {
                // Colors and typography have been read from the
                // View-based theme used in this Activity
                // Shapes are the default for M2 as this didn't exist in M1
                ExampleComposable(/*...*/)
            }
        }
    }
}

기본 구성요소 스타일

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

자세한 내용은 Compose의 맞춤 디자인 시스템을 참고하세요.

테마 오버레이

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

WindowInsets 및 IME 애니메이션

Compose 1.2.0부터 수정자를 사용하여 레이아웃 내에 있는 WindowInsets를 처리할 수 있습니다. IME 애니메이션도 지원됩니다.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
              MyScreen()
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

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

그림 2. IME 애니메이션

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

프레젠테이션에서 분할 상태 우선순위 지정

기본적으로 View는 스테이트풀(Stateful)입니다. View는 표시할 내용뿐만 아니라 그 내용을 표시할 방법을 설명하는 필드를 관리합니다. View를 Compose로 변환할 때는 상태 호이스팅에서 설명한 것처럼 단방향 데이터 흐름이 되도록 렌더링되는 데이터를 분리해야 합니다.

예를 들어, View에는 뷰가 표시되는지, 표시되지 않는지, 사라졌는지를 나타내는 visibility 속성이 있습니다. 이 속성은 View의 고유한 속성입니다. 코드의 다른 부분에서 View의 공개 상태를 변경할 수도 있지만, View 자체만 현재 공개 상태를 알 수 있습니다. View가 표시되도록 하는 로직은 오류가 발생하기 쉽고 종종 View 자체에 연결됩니다.

반면, Compose를 사용하면 Kotlin의 조건부 로직을 사용하여 완전히 다른 컴포저블을 쉽게 표시할 수 있습니다.

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

설계상 CautionIcon은 자신이 표시되고 있는 이유를 알 필요도 없고 관리할 필요도 없으며 자신이 컴포지션 내에 있는지 없는지를 나타내는 visibility에 관한 개념도 없습니다.

상태 관리와 프레젠테이션 로직을 명확하게 구분하여 상태를 UI로 변환하는 것처럼 콘텐츠 표시 방식을 더 자유롭게 변경할 수 있습니다. 상태 소유권이 더 유연하므로 필요한 경우 상태를 호이스팅할 수 있으면 컴포저블의 재사용 가능성이 커집니다.

구성요소의 캡슐화 및 재사용 촉진

View 요소는 종종 자신이 Activity, Dialog, Fragment 내에 있는지 또는 다른 View 계층 구조 내에 있는지 알 수 있습니다. 이러한 요소는 정적 레이아웃 파일에서 확장되는 경우가 많으므로 View의 전체 구조는 매우 견고합니다. 그 결과 결합이 더 긴밀해지고 View를 변경하거나 재사용하기가 더 어려워집니다.

예를 들어, 맞춤 View가 특정 ID를 가진 특정 유형의 하위 뷰를 갖는다고 가정하고 어떤 작업의 응답으로 속성을 직접 변경할 수 있습니다. 이는 이러한 View 요소를 함께 긴밀히 결합합니다. 맞춤 View는 하위 요소를 찾지 못하면 다운되거나 손상될 수 있고 하위 요소는 상위 맞춤 View가 없어서 재사용되지 못할 가능성이 있습니다.

재사용 가능한 컴포저블을 사용하는 Compose에서는 거의 문제가 되지 않습니다. 상위 요소는 상태와 콜백을 쉽게 지정할 수 있으므로, 재사용 가능한 컴포저블의 정확한 사용 위치를 모르더라도 이 컴포저블을 작성할 수 있습니다.

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

위의 예에서는 세 부분이 모두 더 캡슐화되어 덜 결합됩니다.

  • ImageWithEnabledOverlay는 현재 isEnabled 상태만 알면 됩니다. ControlPanelWithToggle의 존재 여부와 제어 방법은 알 필요가 없습니다.

  • ControlPanelWithToggleImageWithEnabledOverlay가 있는지 알 수 없습니다. isEnabled를 표시하는 방법은 0개, 1개 또는 그 이상 있을 수 있으며 ControlPanelWithToggle은 변경할 필요가 없습니다.

  • ImageWithEnabledOverlay 또는 ControlPanelWithToggle이 얼마나 깊이 중첩되었는지는 상위 요소에 중요하지 않습니다. 이러한 하위 요소는 변경사항을 애니메이션 처리하거나 콘텐츠를 바꾸거나 다른 하위 요소에 콘텐츠를 전달할 수 있습니다.

이 패턴을 컨트롤 반전이라고 하며, CompositionLocal 문서에서 자세히 알아볼 수 있습니다.

화면 크기 변경 처리

다양한 창 크기에 따라 다른 리소스를 사용하는 것은 반응형 View 레이아웃을 만드는 주요 방법 중 하나입니다. 정규화된 리소스는 여전히 화면 수준 레이아웃을 결정하는 옵션이지만, Compose를 사용하면 일반적인 조건부 로직으로 코드에서 레이아웃을 전체적으로 훨씬 더 쉽게 변경할 수 있습니다. 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 */
        }
    }
}

Compose가 제공하는 적응형 UI 빌드 기법에 관한 자세한 내용은 적응형 레이아웃 빌드를 참고하세요.

뷰를 사용한 중첩 스크롤

양방향으로 중첩되어 있으며 스크롤 가능한 뷰 요소와 컴포저블 간에 중첩 스크롤 상호 운용성을 사용 설정하는 방법에 관한 자세한 내용은 중첩 스크롤 상호 운용성을 참고하세요.

RecyclerView의 Compose

RecyclerView의 컴포저블은 RecyclerView 버전 1.3.0-alpha02부터 뛰어난 성능을 발휘합니다. 이러한 이점을 확인하려면 1.3.0-alpha02 버전 이상의 RecyclerView를 사용해야 합니다.