다양한 디스플레이 크기를 지원하면 최대한 많은 사용자가 다양한 기기에서 앱을 사용하도록 할 수 있습니다.
다양한 기기 화면이든 멀티 윈도우 모드의 다양한 앱 창이든 가능한 한 많은 디스플레이 크기를 지원하려면 앱 레이아웃을 반응형 및 적응형으로 디자인합니다. 반응형/적응형 레이아웃은 디스플레이 크기와 관계없이 최적화된 사용자 환경을 제공하여 앱이 스마트폰과 태블릿, 폴더블, ChromeOS 기기, 세로 및 가로 방향, 크기 조절 가능한 디스플레이 구성(예: 화면 분할 모드 및 데스크톱 창 모드)을 수용할 수 있습니다.
반응형/적응형 레이아웃은 사용 가능한 디스플레이 공간에 따라 변경됩니다. 변경 범위는 공간을 채우는 작은 레이아웃 조정 (반응형 디자인)부터 앱이 다양한 디스플레이 크기를 가장 잘 수용할 수 있도록 한 레이아웃을 다른 레이아웃으로 완전히 대체하는 것 (적응형 디자인)까지 다양합니다.
Jetpack Compose는 선언형 UI 도구 키트로 다양한 디스플레이 크기에서 콘텐츠를 다르게 렌더링하도록 동적으로 변경되는 레이아웃을 설계하고 구현하는 데 적합합니다.
콘텐츠 수준 컴포저블의 대규모 레이아웃 변경을 명시하기
앱 수준 및 콘텐츠 수준 컴포저블은 앱에서 사용할 수 있는 모든 디스플레이 공간을 차지합니다. 이러한 유형의 컴포저블의 경우 대형 디스플레이에서 앱의 전체 레이아웃을 변경하는 것이 좋습니다.
레이아웃을 결정할 때 실제 하드웨어 값을 사용하지 않습니다. 실재하는 고정된 값을 기반으로 결정하고 싶을 수 있지만 (기기가 태블릿인가요? 실제 화면의 가로세로 비율이 일정한가요?), 이러한 질문에 대한 답변은 UI에 사용할 수 있는 공간을 결정하는 데 유용하지 않을 수 있습니다.
태블릿에서는 앱이 멀티 윈도우 모드에서 실행될 수 있으며, 이는 앱이 다른 앱과 화면을 분할하고 있을 수 있다는 의미입니다. 데스크톱 창 모드 또는 ChromeOS에서는 앱이 크기 조절이 가능한 창에서 실행될 수도 있습니다. 폴더블 기기와 같이 실제 화면이 두 개 이상 있을 수도 있습니다. 이러한 경우는 모두 실제 화면 크기와 콘텐츠 표시 방법을 결정하는 것 사이에 아무 관련이 없습니다.
대신 Jetpack WindowManager 라이브러리에서 제공하는 현재 창 측정항목에 설명된 대로 앱에 할당된 화면의 실제 부분에 따라 결정을 내립니다. Compose 앱에서 WindowManager를 사용하는 방법의 예는 JetNews 샘플을 참고하세요.
레이아웃을 사용할 수 있는 디스플레이 공간에 맞게 레이아웃이 자동 조정되도록 하면 ChromeOS와 같은 플랫폼과 태블릿 및 폴더블 같은 폼 팩터를 지원하기 위한 특수 처리의 양도 감소합니다.
앱에 사용할 수 있는 공간의 측정항목을 결정한 후에는 원시 크기를 창 크기 클래스 사용에 설명된 대로 창 크기 클래스로 변환합니다. 창 크기 클래스는 앱 로직 단순성과 대부분의 디스플레이 크기에 맞게 앱을 최적화하는 유연성 사이에서 균형을 이루도록 설계된 중단점입니다.
창 크기 클래스는 앱의 전체 창을 참조하므로 전체 앱 레이아웃에 영향을 주는 레이아웃 결정에 이 클래스를 사용합니다. 이러한 창 크기 클래스를 상태로 전달하거나 추가 로직을 실행하여 중첩된 컴포저블에 전달하기 위한 파생 상태를 만들 수 있습니다.
@Composable fun MyApp( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).windowSizeClass ) { // Decide whether to show the top app bar based on window size class. val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND) // MyScreen logic is based on the showTopAppBar boolean flag. MyScreen( showTopAppBar = showTopAppBar, /* ... */ ) }
계층화된 접근 방식은 디스플레이 크기 로직을 동기화를 유지해야 하는 앱의 여러 위치에 분산하는 대신 단일 위치로 제한합니다. 단일 위치는 상태를 생성하며, 이 상태는 다른 앱 상태와 마찬가지로 다른 컴포저블에 명시적으로 전달될 수 있습니다. 명시적으로 상태를 전달하면 개별 컴포저블이 간소화됩니다. 이러한 컴포저블은 창 크기 클래스 또는 다른 데이터와 함께 지정된 구성을 사용하기 때문입니다.
유연한 중첩 컴포저블은 재사용 가능함
컴포저블은 다양한 위치에 배치할 수 있을 때 더 많이 재사용할 수 있습니다. 컴포저블을 특정 크기로 특정 위치에 배치해야 하는 경우 컴포저블은 다른 컨텍스트에서 재사용할 수 없습니다. 즉, 재사용 가능한 개별 컴포저블은 전역 디스플레이 크기 정보에 암시적으로 종속되는 것을 피해야 합니다.
목록 세부정보 레이아웃을 구현하는 중첩된 컴포저블이 있고 이 컴포저블이 하나의 창 또는 두 개의 창을 나란히 표시한다고 가정해 보세요.
목록 세부정보 결정은 앱의 전체 레이아웃의 일부여야 하므로 결정은 콘텐츠 수준 컴포저블에서 전달됩니다.
@Composable fun AdaptivePane( showOnePane: Boolean, /* ... */ ) { if (showOnePane) { OnePane(/* ... */) } else { TwoPane(/* ... */) } }
대신 컴포저블이 사용할 수 있는 디스플레이 공간에 따라 독립적으로 레이아웃을 변경하도록 하려면 어떻게 해야 할까요? 예를 들어 공간이 허용되는 경우 추가 세부정보를 표시하는 카드가 있습니다. 사용 가능한 디스플레이 크기를 기반으로 일부 로직을 실행하려고 하지만 어떤 크기를 구체적으로 사용해야 할까요?
기기의 실제 화면 크기를 사용하지 마세요. 화면 유형이 다르면 정확하지 않으며 앱이 전체 화면이 아닌 경우에도 정확하지 않습니다.
컴포저블은 콘텐츠 수준 컴포저블이 아니므로 현재 창 측정항목을 직접 사용하지 마세요.
구성요소가 패딩과 함께 배치되거나 (예: 인셋) 탐색 레일 또는 앱 바와 같은 구성요소가 앱에 포함된 경우 컴포저블에 사용할 수 있는 디스플레이 공간의 양은 앱에 사용할 수 있는 전체 공간과 크게 다를 수 있습니다.
컴포저블이 실제로 자체 렌더링에 사용하는 너비를 사용합니다. 이 너비를 가져오는 방법에는 두 가지가 있습니다.
콘텐츠가 표시되는 위치 또는 방법 을 변경하려면 수정자 모음 또는 맞춤 레이아웃을 사용하여 레이아웃을 반응형으로 만들면 됩니다. 이는 하위 요소가 사용 가능한 모든 공간을 채우거나 공간이 충분한 경우 여러 열로 하위 요소를 배치하는 것만큼 간단할 수 있습니다.
표시하는 항목을 변경하려면
BoxWithConstraints를 더 강력한 대안으로 사용합니다.BoxWithConstraints는 사용 가능한 디스플레이 공간에 따라 다양한 컴포저블을 호출하는 데 사용할 수 있는 측정 제약 조건을 제공합니다. 하지만 이는 알려진 제약 조건이 있을 때BoxWithConstraints가 레이아웃 단계까지 구성을 지연하여 레이아웃 중에 더 많은 작업이 진행되도록 하므로 비용이 발생합니다.
@Composable fun Card(/* ... */) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(/* ... */) Title(/* ... */) } } else { Row { Column { Title(/* ... */) Description(/* ... */) } Image(/* ... */) } } } }
모든 데이터를 다양한 디스플레이 크기로 사용할 수 있도록 하기
추가 디스플레이 공간을 활용하는 컴포저블을 구현할 때는 효율적으로 구현하고 싶고 데이터를 현재 디스플레이 크기의 부작용으로 로드하려는 경향이 있을 수 있습니다.
하지만 이렇게 하면 단방향 데이터 흐름의 원칙에 어긋납니다. 이 흐름에서는 데이터를 끌어올릴 수 있고 적절한 렌더링을 위해 컴포저블에 제공할 수 있습니다. 콘텐츠의 일부가 항상 사용되지는 않더라도 모든 디스플레이 크기에 충분한 콘텐츠를 컴포저블이 항상 보유하도록 충분한 데이터가 컴포저블에 제공되어야 합니다.
@Composable fun Card( imageUrl: String, title: String, description: String ) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description(description) } Image(imageUrl) } } } }
Card 예를 기반으로, 항상 description을 Card에 전달합니다. 너비가 설명을 표시하도록 허용하는 경우에만 description을 사용하더라도 사용할 수 있는 너비에 상관없이 Card는 항상 description 값을 요구합니다.
항상 충분한 콘텐츠를 전달하면 레이아웃을 덜 스테이트풀하게 만들어서 적응형 레이아웃을 더 간단하게 만들 수 있고 디스플레이 크기 간에 전환 시 창 크기 조절, 방향 변경 또는 기기를 접거나 펼치는 동작으로 인해 발생할 수 있는 부작용이 발생하지 않습니다.
또한, 이 원칙을 사용하면 레이아웃을 변경해도 상태를 유지할 수 있습니다. 일부 디스플레이 크기에서 사용되지 않을 수도 있는 정보를 호이스팅하여 레이아웃 크기가 변경될 때 앱 상태를 유지할 수 있습니다.
예를 들어, 디스플레이 크기 조절로 인해 레이아웃이 콘텐츠 숨기기와 콘텐츠 표시하기 간에 전환될 때 앱 상태가 유지되도록 showMore 불리언 플래그를 호이스팅할 수 있습니다.
@Composable fun Card( imageUrl: String, title: String, description: String ) { var showMore by remember { mutableStateOf(false) } BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description( description = description, showMore = showMore, onShowMoreToggled = { newValue -> showMore = newValue } ) } Image(imageUrl) } } } }
자세히 알아보기
Compose의 적응형 레이아웃에 관한 자세한 내용은 다음 리소스를 참고하세요.
샘플 앱
- CanonicalLayouts는 대형 디스플레이에서 최적의 사용자 환경을 제공하는 검증된 디자인 패턴의 저장소입니다.
- JetNews는 사용 가능한 디스플레이 공간을 활용할 수 있도록 UI를 조정하는 앱을 디자인하는 방법을 보여줍니다.
- Reply는 스마트폰, 태블릿, 폴더블을 지원하는 적응형 샘플입니다.
- Now in Android는 적응형 레이아웃을 사용하여 다양한 디스플레이 크기를 지원하는 앱입니다.
동영상