Android 앱 크기 조절

1. 소개

Android 기기 생태계는 항상 진화합니다. 내장 하드웨어 키보드가 나온 초창기부터 플립형, 폴더블, 태블릿, 크기 조절이 가능한 자유 형식 창이 등장한 최신 환경에 이르기까지 Android 앱은 그 어느 때보다 다양한 기기에서 실행되고 있습니다.

개발자에게는 아주 좋은 소식이지만, 사용성 기대치를 충족하고 다양한 화면 크기에서 뛰어난 사용자 환경을 제공하기 위해서는 특정 앱 최적화가 필요합니다. 새 기기를 모두 한 번에 하나씩 타겟팅하는 대신 반응형/적응형 UI와 복원력이 뛰어난 아키텍처를 사용하면 크기와 모양이 제각기 다른 기기를 사용하는 현재 사용자 및 미래 사용자가 디자인도 뛰어나고 장소와 관계없이 작동도 잘 되는 앱을 사용할 수 있습니다.

크기 조절이 가능한 자유 형식 Android 환경의 도입으로 반응형/적응형 UI를 압력 테스트하여 모든 기기에 맞게 준비할 수 있습니다. 이 Codelab에서는 크기 조절의 영향을 이해하고, 강력하고 간편하게 앱의 크기를 조절하기 위한 권장사항을 구현하는 과정을 안내합니다.

빌드할 항목

자유 형식 크기 조절의 영향을 살펴보고 Android 앱을 최적화하여 크기 조절 권장사항을 보여줍니다. 이 앱에는 다음과 같은 특성이 있습니다.

호환되는 매니페스트를 보유합니다

  • 앱의 크기를 자유롭게 조절할 수 없도록 하는 제한사항을 삭제합니다.

크기 조절 시 상태를 유지합니다

  • 크기 조절 시 rememberSaveable을 사용하여 UI 상태를 유지합니다.
  • UI 초기화를 위한 백그라운드 작업의 불필요한 복제를 피합니다.

필요한 항목

  1. 기본 Android 애플리케이션을 만들 수 있는 지식
  2. Compose의 ViewModel 및 상태에 관한 지식
  3. 자유 형식 창 크기 조절을 지원하는 테스트 기기(예: 다음 중 하나)

이 Codelab을 진행하는 동안 코드 버그, 문법 오류, 불명확한 문구 등의 문제가 발생하면 Codelab 왼쪽 하단에 있는 오류 신고 링크를 통해 신고해 주세요.

2. 시작하기

GitHub에서 저장소를 클론합니다.

git clone https://github.com/android/large-screen-codelabs/

또는 저장소의 ZIP 파일을 다운로드하고 압축을 풉니다.

프로젝트 가져오기

  • Android 스튜디오 열기
  • Import Project 또는 File > New > Import Project 선택
  • 프로젝트를 클론했거나 추출한 위치로 이동
  • resizing 폴더 열기
  • start 폴더에서 프로젝트 열기. 여기에 시작 코드가 포함되어 있습니다.

앱 사용해 보기

  • 앱 빌드 및 실행
  • 앱 크기 조절해 보기

어떻게 생각하시나요?

테스트 기기의 호환성 지원에 따라 사용자 환경이 적합하지 않을 수 있습니다. 앱의 크기를 조절할 수 없고 초기 가로세로 비율로 고정되어 있습니다. 왜 그럴까요?

매니페스트 제한사항

앱의 AndroidManifest.xml 파일을 살펴보면 자유 형식 창 크기 조절 환경에서 앱의 원활한 동작을 막는 몇 가지 제한사항이 추가되어 있습니다.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

문제가 되는 이 세 줄을 매니페스트에서 삭제하고 앱을 다시 빌드한 후 테스트 기기에서 다시 시도해 보세요. 앱의 자유 형식 크기 조절이 더 이상 제한되지 않습니다. 매니페스트에서 이와 같은 제한사항을 삭제하는 것은 자유 형식 창 크기 조절을 위해 앱을 최적화하는 중요한 단계입니다.

3. 크기 조절 구성 변경사항

앱 창의 크기를 조절하면 앱의 구성이 업데이트됩니다. 이러한 업데이트는 앱에 영향을 미칩니다. 이를 이해하고 예측하면 사용자에게 우수한 환경을 제공할 수 있습니다. 가장 눈에 띄는 변경사항은 앱 창의 너비와 높이이며 이러한 변경사항은 가로세로 비율과 방향에도 영향을 미칩니다.

구성 변경사항 확인

Android 뷰 시스템으로 빌드된 앱에서 이러한 변경사항이 발생하는 것을 직접 확인하려면 View.onConfigurationChanged를 재정의하면 됩니다. Jetpack Compose에서는 LocalConfiguration.current에 액세스할 수 있으며 이는 View.onConfigurationChanged가 호출될 때마다 자동으로 업데이트됩니다.

샘플 앱에서 이러한 구성 변경사항을 확인하려면 LocalConfiguration.current의 값을 표시하는 컴포저블을 앱에 추가하거나 이러한 컴포저블로 새 샘플 프로젝트를 만듭니다. 이를 확인하기 위한 UI 예시는 다음과 같습니다.

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

observing-configuration-changes 프로젝트 폴더에서 구현 예를 확인할 수 있습니다. 앱의 UI에 이를 추가하고 테스트 기기에서 실행한 후 앱의 구성이 변경될 때 UI 업데이트를 확인해 보세요.

앱 크기가 조절될 때 변경되는 구성 정보가 앱의 인터페이스에 실시간으로 표시됩니다.

앱 구성의 이러한 변경사항을 통해 작은 핸드셋의 화면 분할에서 예상하는 극단에서 태블릿이나 데스크톱의 전체 화면으로의 이동을 빠르게 시뮬레이션할 수 있습니다. 이를 통해 다양한 화면에서 앱의 레이아웃을 테스트할 뿐만 아니라 앱이 빠른 구성 변경 이벤트를 얼마나 잘 처리하는지 테스트할 수도 있습니다.

4. Activity 수명 주기 이벤트 로깅

앱의 자유 형식 창 크기 조절의 또 다른 영향은 앱에서 발생하는 다양한 Activity 수명 주기 변경입니다. 이러한 변경사항을 실시간으로 확인하려면 onCreate 메서드에 수명 주기 관찰자를 추가하고, onStateChanged를 재정의하여 새로운 각 수명 주기 이벤트를 기록하세요.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

이 로깅이 준비된 상태에서 앱을 테스트 기기에서 다시 실행하고, 앱을 최소화했다가 포그라운드로 다시 가져오려고 할 때 Logcat을 살펴보세요.

앱이 최소화될 때 일시중지되었다가 포그라운드로 가져오면 다시 재개됩니다. 이는 연속성을 다루는 이 Codelab의 다음 섹션에서 살펴볼 앱에 영향을 미칩니다.

크기 조절 시 호출되는 활동 수명 주기 메서드를 보여주는 Logcat

이제 가능한 가장 작은 크기에서 가장 큰 크기로 앱 크기를 조절할 때 어떤 활동 수명 주기 콜백이 호출되는지 Logcat에서 확인합니다.

테스트 기기에 따라 다양한 동작을 확인할 수 있지만 앱 창의 크기가 크게 변경될 때는 활동이 소멸되고 다시 생성되지만 작게 변경될 때는 그렇지 않은 것을 알 수 있습니다. API 24 이상에서는 크기가 크게 변경될 때만 Activity가 재생성되기 때문입니다.

자유 형식 창 환경에서 예상할 수 있는 일반적인 구성 변경사항을 몇 가지 확인했지만 알아야 할 다른 변경사항도 있습니다. 예를 들어 테스트 기기에 외부 모니터가 연결된 경우 디스플레이 밀도와 같은 구성 변경을 고려하여 Activity가 소멸되었다가 다시 생성되는 것을 확인할 수 있습니다.

구성 변경과 관련된 복잡성을 제거하려면 적응형 UI를 구현하는 데 WindowSizeClass와 같은 상위 수준 API를 사용하세요. 다양한 화면 크기 지원도 참고하세요.

5. 연속성: 크기 조절 시 컴포저블의 내부 상태 유지

이전 섹션에서는 자유 형식 창 크기 조절 환경에서 앱이 예상할 수 있는 구성 변경사항을 확인했습니다. 이 섹션에서는 이러한 변경에도 앱의 UI 상태를 계속 유지합니다.

먼저 클릭 시 이메일 주소를 표시하도록 NavigationDrawerHeader 컴포저블 함수(ReplyHomeScreen.kt에 있음)를 확장합니다.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

확장 가능한 헤더를 앱에 추가했다면 다음을 실행하세요.

  1. 테스트 기기에서 앱을 실행합니다.
  2. 헤더를 탭하여 확장합니다.
  3. 창 크기를 조절해 봅니다.

크기를 크게 조절하면 헤더가 상태를 잃습니다.

앱 탐색 창의 헤더를 탭하여 확장하지만 앱 크기가 조절된 후 축소됩니다.

remember는 여러 리컴포지션에도 상태를 유지하는 데 도움이 되지만 활동이나 프로세스 재생성에서는 그렇지 않기 때문에 UI 상태가 손실됩니다. 일반적으로 상태 호이스팅을 사용합니다. 상태를 컴포저블의 호출자로 이동하여 컴포저블을 스테이트리스(Stateless)로 만드는 것으로 이 문제를 완전히 방지할 수 있습니다. 하지만 UI 요소 상태를 컴포저블 함수에 내부적으로 유지할 때 여러 위치에서 remember를 사용할 수 있습니다.

이 문제를 해결하려면 rememberrememberSaveable로 바꿉니다. 이 방법은 rememberSaveable이 기억된 값을 savedInstanceState에 저장하고 복원하므로 효과가 있습니다. rememberrememberSaveable로 변경하고 테스트 기기에서 앱을 실행한 후 다시 앱 크기를 조절해 보세요. 확장 가능한 헤더의 상태가 의도한 대로 크기 조절에도 유지됩니다.

6. 불필요한 백그라운드 작업 중복 방지

자유 형식 창 크기 조절로 인해 자주 발생할 수 있는 구성 변경에도 rememberSaveable을 사용하여 컴포저블의 내부 UI 상태를 유지하는 방법을 알아봤습니다. 하지만 앱에서는 UI 상태 및 로직을 컴포저블에서 호이스팅해야 하는 때가 많습니다. 상태 소유권을 ViewModel로 이동하는 것은 크기 조절 중에 상태를 유지하는 데 좋은 방법입니다. 상태를 ViewModel로 호이스팅할 때 화면을 초기화하는 데 필요한 과도한 파일 시스템 액세스나 네트워크 호출과 같은 장기 실행 백그라운드 작업 문제가 발생할 수 있습니다.

발생할 수 있는 문제의 종류를 예로 확인하려면 ReplyViewModelinitializeUIState 메서드에 로그 구문을 추가합니다.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

이제 테스트 기기에서 앱을 실행하고 앱 창의 크기를 여러 번 조절해 보세요.

Logcat을 보면 초기화 메서드가 여러 번 실행된 것이 앱에 표시됩니다. 이는 UI를 초기화하기 위해 한 번만 실행하려는 작업에 문제가 될 수 있습니다. 추가 네트워크 호출이나 파일 I/O, 기타 작업은 기기의 성능을 저해할 수 있으므로 의도하지 않은 다른 문제가 발생할 수 있습니다.

불필요한 백그라운드 작업을 피하려면 initializeUIState() 호출을 활동의 onCreate() 메서드에서 삭제합니다. 대신 ViewModelinit 메서드에서 데이터를 초기화합니다. 이렇게 하면 ReplyViewModel이 처음 인스턴스화될 때 초기화 메서드가 한 번만 실행됩니다.

init {
    initializeUIState()
}

다시 앱을 실행하면 시뮬레이션된 불필요한 초기화 작업이 앱 창의 크기 조절 횟수와 관계없이 한 번만 실행됩니다. ViewModel이 Activity 수명 주기 이후에도 유지되기 때문입니다. ViewModel 생성 시 초기화 코드를 한 번만 실행하여 Activity 재생성에서 분리하고 불필요한 작업을 방지합니다. 이것이 UI 초기화를 위한 실제로 비용이 많이 드는 서버 호출이나 과도한 파일 I/O 작업이라면 상당한 리소스를 절약하고 사용자 환경을 개선할 수 있습니다.

7. 축하합니다

수고하셨습니다. 훌륭합니다. 이제 ChromeOS와 기타 멀티 윈도우, 멀티스크린 환경에서 Android 앱이 크기를 효과적으로 조절할 수 있는 권장사항을 구현했습니다.

샘플 소스 코드

GitHub에서 저장소를 클론합니다.

git clone https://github.com/android/large-screen-codelabs/

또는 저장소의 ZIP 파일을 다운로드하고 압축을 풉니다.