이동식 저장소

[Android] 반응형 앱 구현 방법론 (2) 본문

Primary/Android

[Android] 반응형 앱 구현 방법론 (2)

해스끼 2022. 10. 18. 21:06

1편에서 이어집니다.

라이브러리

``material3-window-size-class`` 라이브러리의 ``WindowSizeClass`` 클래스와 ``calculateWindowSizeClass()`` 함수를 사용하자.

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

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}

Main composable(여기서는 ``MyApp``)은 ``WindowSizeClass``를 필수 매개변수로 받아야 한다. 화면 크기의 single source of truth가 정의되었으므로, 하위 composable에서 화면 크기를 매번 가져올 필요는 없다. 단지 전달된 값을 매개변수로 받아 UI와 관련된 의사결정을 내리기만 하면 된다.

Composable은 최대한 유연하게

Composable은 최대한 많은 곳에서 사용될 수 있어야 한다. 특정 위치에 특정 크기로만 배치될 수 있는 composable을 다른 화면이나 앱에서 재사용할 수 있을까? 아마도 매우 어려울 것이다.

 

따라서 root가 아닌 composable은 주어진 넓이에 따라 자기 자신을 유연하게 보여줄 수 있어야 한다. 화면의 종류(폴더블? 태블릿?) 혹은 전체 화면의 크기를 참조해서는 안 된다.

 

리스트 item의 상세 정보를 보여주는 레이아웃을 생각해 보자.

예시

보다시피 전체 UI에서 column의 수가 매우 중요하다. 따라서 root composable에서 몇 줄로 보여줄지 결정한 후 하위 composable에 결정된 값을 전파해 보자. 대략 이렇게 작성할 수 있겠다.

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

참고로 이 composable을 root로 취급할 수 있는 이유는 리스트와 아이템이라는 큰 레이아웃의 최상위 composable이기 때문이다.

Non-Root composable에서는?

위에서처럼 화면 크기에 따라 전체 UI의 배치를 결정할 수도 있고, 개별 composable에서 주어진 공간에 따라 레이아웃을 바꿔야 할 수도 있다. 예를 들어 카드 composable에서 너비가 충분하다면 더 많은 정보를 보여주고 싶을 수도 있다. 그러니까 주어진 영역의 너비에 따라 UI 로직을 수행해야 한다.

이렇게

Root composable이 아니므로 앱에 할당된 너비를 사용할 수도 없다. 자신에게 주어진 너비를 어떻게 알 수 있을까?

  1. 데이터가 보이는 위치 또는 방법을 바꾸고 싶다면, ``Modifier``를 사용하거나 아예 커스텀 레이아웃을 만들 수 있다. 예를 들어 하위 composable이 남은 공간을 전부 채우도록 하거나(weight), 아예 하위 composable을 column으로 나열하는 방법이 있다.
  2. 위의 카드 예시처럼 보여줄 내용을 바꾸고 싶다면, ``BoxWithConstraints``를 사용해 보자. ``BoxWithConstraints``내부에서 주어진 공간의 크기를 참조할 수 있다. 하지만 이렇게 하면 공간의 크기에 따라 Composition이 결정되므로, Composition 단계의 일부가 Layout 단계로 이연된다. Compose의 UI 구현 3단계를 일부 깨트리는 셈이다.
@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

데이터는 항상 전부 제공해야 한다

큰 화면에서는 보여주지만 작은 화면에서는 몇몇 데이터를 생략할 수 있다. 그러면 화면 크기가 Compact일 때에만 데이터를 불러와도 되지 않을까?

안돼요

Unidirectional Data Flow를 정면으로 부정하는 행위이다. UDF에서 데이터는 데이터가 어떻게 보이는지와 상관없이 항상 제공되어야 한다. 그래야 필요할 때 즉시 보여줄 수 있기 때문이다. 화면 크기가 1초마다 변한다면, 1초마다 데이터를 로드하겠다는 생각인가?

 

다음 코드를 보자. 위의 Card composable을 구현한 예시이다.

@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)
            }
        }
    }
}

``description``은 ``maxWidth``가 400dp 이상일 때에만 보이지만, 항상(always) 매개변수로 주어진다. 이렇게 하면 반응형 composable을 더 간단하고 작게 유지할 수 있다. 화면의 크기가 변한다고 해도 데이터 로직을 수행할 필요는 없으며(사실 수행해서도 안 된다), 단지 UI 구성만 바꾸면 된다.

 

괜히 잔머리 굴리지 말라는 뜻이다. 정직하게!

UI 테스팅

반응형 UI를 구현했다면, 최대한 많은 비율의 화면을 테스트해 보자. 꼭 테스트 코드를 작성하지 않더라도, 앱을 실행해서 반응형 UI가 의도대로 동작하는지 검사해 보자. 화면 배치가 어색하지는 않은지, 큰 화면에서 더 많은 데이터를 보여줄 수는 없는지.

 

Android Studio에서 에뮬레이터의 화면 비율을 원하는 대로 설정할 수 있으니 참고하길 바란다.

참고문헌

 

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

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

developer.android.com


Compose 실전 편으로 이어집니다.

 

[Compose] 반응형 앱 구현 실전

한빛 캘린더에 반응형 UI를 적용해 보자. Before 처음 UI를 구현할 때 가로로 긴 화면을 전혀 고려하지 않았다. 덕분에 태블릿에서는 가로로 길게 늘어난 화면이 보이게 되었다. 굳이 태블릿까지 안

thinking-face.tistory.com

 

Comments