1. 소개
Android 기기 생태계는 항상 진화합니다. 내장 하드웨어 키보드가 나온 초창기부터 플립형, 폴더블, 태블릿, 크기 조절이 가능한 자유 형식 창이 등장한 최신 환경에 이르기까지 Android 앱은 그 어느 때보다 다양한 기기에서 실행되고 있습니다.
개발자에게는 아주 좋은 소식이지만, 사용성 기대치를 충족하고 다양한 화면 크기에서 뛰어난 사용자 환경을 제공하기 위해서는 특정 앱 최적화가 필요합니다. 새 기기를 모두 한 번에 하나씩 타겟팅하는 대신 반응형/적응형 UI와 복원력이 뛰어난 아키텍처를 사용하면 크기와 모양이 제각기 다른 기기를 사용하는 현재 사용자 및 미래 사용자가 디자인도 뛰어나고 장소와 관계없이 작동도 잘 되는 앱을 사용할 수 있습니다.
크기 조절이 가능한 자유 형식 Android 환경의 도입으로 반응형/적응형 UI를 압력 테스트하여 모든 기기에 맞게 준비할 수 있습니다. 이 Codelab에서는 크기 조절의 영향을 이해하고, 강력하고 간편하게 앱의 크기를 조절하기 위한 권장사항을 구현하는 과정을 안내합니다.
빌드할 항목
자유 형식 크기 조절의 영향을 살펴보고 Android 앱을 최적화하여 크기 조절 권장사항을 보여줍니다. 이 앱에는 다음과 같은 특성이 있습니다.
호환되는 매니페스트를 보유합니다
- 앱의 크기를 자유롭게 조절할 수 없도록 하는 제한사항을 삭제합니다.
크기 조절 시 상태를 유지합니다
- 크기 조절 시 rememberSaveable을 사용하여 UI 상태를 유지합니다.
- UI 초기화를 위한 백그라운드 작업의 불필요한 복제를 피합니다.
필요한 항목
- 기본 Android 애플리케이션을 만들 수 있는 지식
- Compose의 ViewModel 및 상태에 관한 지식
- 자유 형식 창 크기 조절을 지원하는 테스트 기기(예: 다음 중 하나)
- ADB 설정이 있는 Chromebook
- 삼성 DeX 모드나 생산성 모드를 지원하는 태블릿
- Android 스튜디오의 데스크톱 Android Virtual Device 에뮬레이터
이 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에서 확인합니다.
테스트 기기에 따라 다양한 동작을 확인할 수 있지만 앱 창의 크기가 크게 변경될 때는 활동이 소멸되고 다시 생성되지만 작게 변경될 때는 그렇지 않은 것을 알 수 있습니다. 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)
),
)
}
}
}
확장 가능한 헤더를 앱에 추가했다면 다음을 실행하세요.
- 테스트 기기에서 앱을 실행합니다.
- 헤더를 탭하여 확장합니다.
- 창 크기를 조절해 봅니다.
크기를 크게 조절하면 헤더가 상태를 잃습니다.
remember
는 여러 리컴포지션에도 상태를 유지하는 데 도움이 되지만 활동이나 프로세스 재생성에서는 그렇지 않기 때문에 UI 상태가 손실됩니다. 일반적으로 상태 호이스팅을 사용합니다. 상태를 컴포저블의 호출자로 이동하여 컴포저블을 스테이트리스(Stateless)로 만드는 것으로 이 문제를 완전히 방지할 수 있습니다. 하지만 UI 요소 상태를 컴포저블 함수에 내부적으로 유지할 때 여러 위치에서 remember
를 사용할 수 있습니다.
이 문제를 해결하려면 remember
를 rememberSaveable
로 바꿉니다. 이 방법은 rememberSaveable
이 기억된 값을 savedInstanceState
에 저장하고 복원하므로 효과가 있습니다. remember
를 rememberSaveable
로 변경하고 테스트 기기에서 앱을 실행한 후 다시 앱 크기를 조절해 보세요. 확장 가능한 헤더의 상태가 의도한 대로 크기 조절에도 유지됩니다.
6. 불필요한 백그라운드 작업 중복 방지
자유 형식 창 크기 조절로 인해 자주 발생할 수 있는 구성 변경에도 rememberSaveable
을 사용하여 컴포저블의 내부 UI 상태를 유지하는 방법을 알아봤습니다. 하지만 앱에서는 UI 상태 및 로직을 컴포저블에서 호이스팅해야 하는 때가 많습니다. 상태 소유권을 ViewModel로 이동하는 것은 크기 조절 중에 상태를 유지하는 데 좋은 방법입니다. 상태를 ViewModel
로 호이스팅할 때 화면을 초기화하는 데 필요한 과도한 파일 시스템 액세스나 네트워크 호출과 같은 장기 실행 백그라운드 작업 문제가 발생할 수 있습니다.
발생할 수 있는 문제의 종류를 예로 확인하려면 ReplyViewModel
의 initializeUIState
메서드에 로그 구문을 추가합니다.
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()
메서드에서 삭제합니다. 대신 ViewModel
의 init
메서드에서 데이터를 초기화합니다. 이렇게 하면 ReplyViewModel
이 처음 인스턴스화될 때 초기화 메서드가 한 번만 실행됩니다.
init {
initializeUIState()
}
다시 앱을 실행하면 시뮬레이션된 불필요한 초기화 작업이 앱 창의 크기 조절 횟수와 관계없이 한 번만 실행됩니다. ViewModel이 Activity
수명 주기 이후에도 유지되기 때문입니다. ViewModel
생성 시 초기화 코드를 한 번만 실행하여 Activity
재생성에서 분리하고 불필요한 작업을 방지합니다. 이것이 UI 초기화를 위한 실제로 비용이 많이 드는 서버 호출이나 과도한 파일 I/O 작업이라면 상당한 리소스를 절약하고 사용자 환경을 개선할 수 있습니다.
7. 축하합니다
수고하셨습니다. 훌륭합니다. 이제 ChromeOS와 기타 멀티 윈도우, 멀티스크린 환경에서 Android 앱이 크기를 효과적으로 조절할 수 있는 권장사항을 구현했습니다.
샘플 소스 코드
GitHub에서 저장소를 클론합니다.
git clone https://github.com/android/large-screen-codelabs/
또는 저장소의 ZIP 파일을 다운로드하고 압축을 풉니다.