Android 앱 크기 조절

Android 앱 크기 조절

이 Codelab 정보

subject최종 업데이트: 12월 14, 2023
account_circle작성자: Patrick Fuentes, Emilie Roberts

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 파일을 다운로드하고 압축을 풉니다.