시배's Android

Compose Docs | Build adaptive layouts 본문

Android/Compose Docs

Compose Docs | Build adaptive layouts

si8ae 2023. 8. 28. 21:53
 

적응형 레이아웃 빌드하기  |  Jetpack Compose  |  Android Developers

적응형 레이아웃 빌드하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱 UI는 다양한 화면 크기, 방향, 폼 팩터를 처리할 수 있도록 반응해야 합니다. 적

developer.android.com

앱의 UI는 다양한 화면 크기, 방향 및 형태 요소에 대응하도록 반응해야 합니다. 적응형 레이아웃은 화면 공간에 따라 변경되며, 이러한 변경 사항은 공간을 채우기 위한 간단한 레이아웃 조정부터 추가 공간을 활용하기 위해 레이아웃을 완전히 변경하는 것까지 다양합니다.

선언적 UI 도구인 Jetpack Compose 다양한 크기에 따라 콘텐츠를 다르게 렌더링하도록 레이아웃을 디자인하고 구현하는 적합합니다. 문서에는 Compose 사용하여 다양한 크기에 맞게 콘텐츠를 다르게 렌더링하는 UI 만드는 방법에 대한 지침이 포함되어 있습니다.

Make large layout changes for screen-level composables explicit

Jetpack Compose를 사용하여 전체 애플리케이션의 레이아웃을 설계할 때, 앱 레벨과 화면 레벨의 컴포저블들은 앱이 렌더링하는 데 제공된 모든 공간을 차지합니다. 이 디자인 단계에서는 더 큰 화면의 이점을 살리기 위해 화면 전체 레이아웃을 변경하는 것이 합리적일 수 있습니다.

  • 앱 레벨 컴포저블: 앱이 주어진 모든 공간을 차지하고 다른 모든 컴포저블을 포함하는 단일 루트 컴포저블입니다.
  • 화면 레벨 컴포저블: 앱 레벨 컴포저블 내부에 포함된 컴포저블로, 앱에 제공된 모든 공간을 차지합니다. 각 화면 레벨 컴포저블은 일반적으로 앱 내에서 이동하는 동안 특정 목적지를 나타냅니다.
  • 개별 컴포저블: 다른 모든 컴포저블입니다. 이것들은 개별 요소, 재사용 가능한 콘텐츠 그룹 또는 화면 레벨 컴포저블 내에 호스팅된 컴포저블일 있습니다.

레이아웃 결정을 위해 물리적인 하드웨어 사용을 피해야 합니다. 고정된 물리적인 값에 기반한 결정을 내리는 것은 유혹적일 있지만 (장치가 태블릿인가요? 물리적인 화면이 특정 종횡비를 가지고 있나요?), 이러한 질문에 대한 답은 UI 작업할 있는 공간을 결정하는 유용하지 않을 있습니다.

태블릿에서는 앱이 다른 앱과 화면을 분할할 수 있는 멀티 윈도우 모드에서 실행될 수 있습니다. ChromeOS에서는 앱이 크기 조정 가능한 창에 있을 수 있습니다. 접이식 장치와 같은 경우 더 많은 물리적인 화면이 있을 수도 있습니다. 이러한 모든 경우에서 물리적인 화면 크기는 콘텐츠를 표시하는 방법을 결정하는 데 관련이 없습니다.

대신 앱에 할당된 실제 화면 부분을 기반으로 결정을 내릴 필요가 있습니다. 예를 들어 Jetpack WindowManager 라이브러리에서 제공하는 현재 창 메트릭스와 같은 윈도우 메트릭스를 사용할 수 있습니다. 

이 접근 방식을 따르면 앱이 위의 모든 시나리오에서 잘 동작하므로 앱이 더 유연해집니다. 레이아웃을 화면 공간에 적응시키면 ChromeOS와 같은 플랫폼, 태블릿 및 접이식 장치와 같은 형태 요소를 지원하기 위한 특별한 처리 양을 줄일 수도 있습니다.

앱에 대해 사용 가능한 관련 공간을 관찰하는 것으로, 관점을 따라가는 것이 도움이 됩니다. 또한 이를 윈도우 크기 클래스(Window Size Classes) 변환하여 의미 있는 크기 클래스로 변환하는 것도 도움이 됩니다. 이는 표준 크기 버킷으로 크기를 그룹화하며, 버킷은 대부분의 고유한 경우를 최적화하기 위해 단순성과 유연성을 균형잡힌 방식으로 제공합니다. 이러한 크기 클래스는 앱의 전체 화면 레이아웃에 영향을 미치는 레이아웃 결정에 대해 사용됩니다. 이러한 크기 클래스를 상태로 전달하거나 중첩된 컴포저블에 전달하기 위해 추가적인 로직을 수행할 있습니다.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

계층 구조 접근 방식은 화면 크기 로직을 여러 곳에 흩뿌리지 않고 단일 위치로 제한함으로써 동기화를 유지해야 하는 많은 위치에 걸쳐 있는 상황을 피합니다. 단일 위치에서 상태를 생성하며, 상태는 다른 컴포저블에 명시적으로 전달될 있습니다. 다른 상태와 마찬가지로 명시적으로 상태를 전달함으로써 중첩된 컴포저블들이 단순화됩니다. 이렇게 하면 중첩된 컴포저블들은 크기 클래스나 지정된 구성과 함께 다른 데이터와 함께 전달되는 보통 컴포저블 함수가 것입니다.

Flexible nested composables are reusable

컴포저블은 다양한 장소에 배치될 수 있을 때 더 재사용 가능해집니다. 만약 컴포저블이 특정 위치에 특정 크기로 항상 배치될 것이라고 가정한다면, 다른 위치에서 다른 공간 또는 다른 공간 크기와 함께 재사용하기가 더 어려울 것입니다. 이는 또한 개별적이고 재사용 가능한 컴포저블이 "전역적인" 크기 정보에 묵시적으로 의존하는 것을 피해야 함을 의미합니다.

예를 들어보겠습니다: 리스트-상세 레이아웃을 구현하는 중첩된 컴포저블을 상상해보세요. 레이아웃은 개의 패널 또는 개의 패널을 나란히 표시할 있을 것입니다.

우리는 결정을 앱의 전체 레이아웃의 일부로 만들고자 합니다. 그래서 위에서 보았던 것처럼 화면 레벨의 컴포저블에서 결정을 전달합니다.

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

대신, 컴포저블이 사용 가능한 공간을 기반으로 독립적으로 레이아웃을 변경하길 원하는 경우는 어떨까요? 예를 들어 공간이 허용된다면 추가 세부 정보를 표시하려는 카드 컴포저블이 있을 있습니다. 어떤 사용 가능한 크기를 기반으로 어떤 로직을 수행하길 원하지만, 어떤 크기를 정확히 사용해야 할까요?

위에서 본 대로, 기기의 실제 화면 크기를 사용하지 않는 것이 좋습니다. 이는 여러 개의 화면에서 정확하지 않을 뿐만 아니라 앱이 전체 화면이 아닌 경우에도 정확하지 않을 것입니다.

컴포저블이 화면 레벨 컴포저블이 아닌 경우에도 재사용성을 극대화하기 위해 현재 창 메트릭스를 직접 사용해서는 안 됩니다. 만약 컴포저블이 여백에 배치되는 경우(예: 간격에 대한 것) 또는 네비게이션 레일이나 앱 바 같은 구성 요소가 있는 경우, 컴포저블에 사용 가능한 공간은 앱에 전체적으로 사용 가능한 공간과 크게 다를 수 있습니다.

그래서 컴포저블이 렌더링되는 실제 폭을 사용해야 합니다. 이 폭을 얻기 위한 두 가지 옵션이 있습니다:

콘텐츠가 어디에 어떻게 표시되는지를 변경하려면 수정자(modifier) 컬렉션 또는 사용자 정의 레이아웃을 사용하여 레이아웃을 반응형으로 만들 수 있습니다. 이것은 사용 가능한 모든 공간을 채우는 자식 요소를 가지거나 충분한 공간이 있는 경우 여러 열로 자식 요소를 배치하는 것과 같이 간단할 수 있습니다.

표시 내용을 변경하려면 BoxWithConstraints를 더 강력한 대안으로 사용할 수 있습니다. 이 컴포저블은 사용 가능한 공간을 기반으로 다른 컴포저블을 호출할 수 있는 측정 제약을 제공합니다. 그러나 이는 레이아웃 단계에서 더 많은 작업을 수행하도록 BoxWithConstraints가 구성되며, 이러한 제약이 알려진 경우 레이아웃 단계에서 더 많은 작업을 수행합니다.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Ensure all data is available for different sizes

추가 화면 공간을 활용할 때, 큰 화면에서는 작은 화면보다 사용자에게 더 많은 콘텐츠를 보여줄 수 있습니다. 이러한 동작을 가진 컴포저블을 구현할 때, 현재 크기의 부작용으로 데이터를 로드하려는 유혹이 있을 수 있습니다.

하지만 이는 데이터의 단방향 흐름 원칙에 어긋납니다. 데이터를 끌어올리고 컴포저블에게 적절한 렌더링을 위해 단순히 제공할 있도록 해야 합니다. 컴포저블에는 어떤 크기에서든 표시하는 필요한 충분한 데이터가 제공되어야 합니다. 데이터의 일부가 항상 사용되지 않을 있더라도 컴포저블이 어떤 크기에서든 필요한 데이터를 갖고 있을 있어야 합니다.

@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 예제를 바탕으로 살펴보면, 우리는 항상 Card에 설명(description)을 전달합니다. 설명은 폭이 표시를 허용하는 경우에만 사용되지만, Card는 항상 설명을 요구하므로 사용 가능한 폭과는 무관하게 설명을 필요로 합니다.

데이터를 항상 전달하는 것은 적응형 레이아웃을 더 간단하게 만들며, 상태 변화 시에 부작용을 트리거하지 않도록 합니다(이는 창 크기 변경, 방향 변경 또는 장치의 접힘과 펼침으로 인해 발생할 수 있음).

이 원칙은 또한 레이아웃 변경 시 상태를 보존할 수 있도록 합니다. 모든 크기에서 사용되지 않을 수 있는 정보를 끌어올림으로써 레이아웃 크기 변경 시 사용자의 상태를 보존할 수 있습니다. 예를 들어, 설명을 숨기거나 표시하는 레이아웃 변경 시 사용자의 상태를 보존하기 위해 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)
            }
        }
    }
}

 

'Android > Compose Docs' 카테고리의 다른 글

Compose Docs | Instrinsic measurements  (0) 2023.09.05
Compose Docs | Alignment lines  (0) 2023.09.03
Compose Docs | Custom layouts  (0) 2023.08.26
Compose Docs | Layout  (0) 2023.08.16
Compose Docs | Locally scoped data with CompositionLocal  (0) 2023.08.13