구성 변경 처리

일부 기기 구성은 앱이 실행되는 동안 변경될 수 있습니다. 여기에는 다음이 포함되지만 이에 국한되지 않습니다.

  • 앱 디스플레이 크기
  • 화면 방향
  • 글꼴 크기 및 두께
  • 언어
  • 어두운 모드와 밝은 모드 비교
  • 키보드 사용 가능 여부

이러한 구성 변경은 대부분 사용자 상호작용으로 인해 발생합니다. 예를 들어 기기를 회전하거나 접으면 앱에서 사용할 수 있는 화면 공간의 크기가 달라집니다. 마찬가지로 글꼴 크기나 언어, 기본 테마와 같은 기기 설정을 변경하면 Configuration 객체에서 각각의 값이 변경됩니다.

이러한 매개변수를 사용하려면 일반적으로 변경 시 Android 플랫폼에 전용 메커니즘이 있는 애플리케이션의 UI를 크게 변경해야 합니다. 이 메커니즘은 Activity 재생성입니다.

활동 재생성

시스템은 구성 변경이 발생하면 Activity를 다시 만듭니다. 이를 위해 시스템은 onDestroy()를 호출하고 기존 Activity 인스턴스를 소멸시킵니다. 그런 다음 onCreate()를 사용하여 새 인스턴스를 만들면 이 새 Activity 인스턴스는 업데이트된 새 구성으로 초기화됩니다. 또한 시스템에서도 새 구성으로 UI를 다시 만듭니다.

재생성 동작은 새 기기 구성과 일치하는 대체 리소스로 애플리케이션을 자동으로 새로고침하여 애플리케이션이 새 구성에 맞게 조정되도록 지원합니다.

재생성 예

레이아웃 XML 파일에 정의된 대로 android:text="@string/title"을 사용하여 정적 제목을 표시하는 TextView를 생각해 보겠습니다. 뷰를 만들면 현재 언어에 따라 텍스트가 정확히 한 번 설정됩니다. 언어가 변경되면 시스템은 활동을 재생성합니다. 따라서 시스템은 뷰도 재생성하고 새 언어에 따른 올바른 값으로 이를 초기화합니다.

또한 재생성은 Activity에서 또는 포함된 FragmentView, 다른 객체에서 필드로 유지된 상태를 삭제합니다. 이는 Activity 재생성에서 완전히 새로운 Activity 및 UI 인스턴스를 만들기 때문입니다. 또한 이전 Activity는 더 이상 표시되거나 유효하지 않으므로 이전 Activity 또는 포함된 객체에 대한 남은 참조는 비활성 상태가 됩니다. 버그, 메모리 누수, 비정상 종료가 발생할 수 있습니다.

사용자 기대치

앱 사용자는 상태가 보존되기를 기대합니다. 사용자가 양식을 작성하는 도중에 정보 참조를 위해 멀티 윈도우 모드에서 다른 앱을 여는 경우, 돌아왔을 때 작성하던 양식이 지워져 있거나 완전히 다른 지점이 표시된다면 사용자 경험을 해치게 됩니다. 개발자는 구성 변경 및 활동 재생성을 통해 일관된 사용자 환경을 제공해야 합니다.

상태가 애플리케이션에 보존되는지 확인하려면 앱이 포그라운드에 있는 동안과 백그라운드에 있는 동안 모두 구성 변경을 일으키는 작업을 실행해 보면 됩니다. 이러한 작업에는 다음이 포함됩니다.

  • 기기 회전
  • 멀티 윈도우 모드 시작
  • 멀티 윈도우 모드나 자유 형식 창에서 애플리케이션 크기 조절
  • 여러 디스플레이가 제공되는 폴더블 기기 접기
  • 시스템 테마 변경(예: 어두운 모드 또는 밝은 모드)
  • 글꼴 크기 변경
  • 시스템 또는 앱 언어 변경
  • 하드웨어 키보드 연결 또는 연결 해제
  • 도크 연결 또는 연결 해제

Activity 재생성을 통해 관련 상태를 보존하는 데는 세 가지 기본 접근 방식을 활용할 수 있습니다. 보존하려는 상태 유형에 따라 사용할 접근 방식이 다릅니다.

  • 로컬 지속성: 복잡하거나 큰 데이터의 프로세스 종료를 처리합니다. 영구 로컬 저장소에는 데이터베이스 또는 DataStore가 포함됩니다.
  • 유지된 객체(예: ViewModel 인스턴스): 사용자가 앱을 활발히 사용하는 동안 메모리에서 UI 관련 상태를 처리합니다.
  • 저장된 인스턴스 상태: 시스템에서 시작된 프로세스 종료를 처리하고 사용자 입력이나 탐색에 따른 일시적인 상태를 유지합니다.

이러한 각 항목의 API와 각 접근 방식의 적절한 사용 시점에 관한 자세한 내용은 UI 상태 저장을 참고하세요.

Activity 재생성 제한

특정 구성 변경 시 자동 활동 재생성을 방지할 수 있습니다. Activity를 다시 만들면 전체 UI와 Activity에서 파생된 객체도 다시 만들어집니다. 이러한 상황을 피해야 할 합당한 이유가 있을 수 있습니다. 예를 들어 앱이 특정 구성 변경 중에 리소스를 업데이트하지 않아도 되거나, 성능 제한이 있을 수도 있습니다. 이 경우 활동이 구성 변경을 직접 처리한다고 선언하여 시스템이 활동을 다시 시작하지 못하도록 할 수 있습니다.

특정 구성 변경에 관한 활동 재생성을 사용 중지하려면 AndroidManifest.xml 파일의 <activity> 항목에서 구성 유형을 android:configChanges에 추가합니다. 가능한 값은 android:configChanges 속성 문서에 나와 있습니다.

다음 매니페스트 코드는 화면 방향과 키보드 사용 가능 여부가 변경될 때 MyActivityActivity 재생성을 사용 중지합니다.

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

일부 구성 변경은 항상 활동이 다시 시작하도록 합니다. 이를 사용 중지할 수는 없습니다. 예를 들어 Android 12L(API 수준 32)에서 도입된 동적 색상 변경은 사용 중지할 수 없습니다.

뷰 시스템에서 구성 변경에 반응

View 시스템에서 Activity 재생성을 사용 중지한 구성 변경이 발생하면 활동은 Activity.onConfigurationChanged() 호출을 수신합니다. 모든 연결된 뷰도 View.onConfigurationChanged() 호출을 수신합니다. android:configChanges에 추가하지 않은 구성 변경의 경우 시스템은 평소와 같이 활동을 다시 만듭니다.

onConfigurationChanged() 콜백 메서드는 새 기기 구성을 지정하는 Configuration 객체를 수신합니다. Configuration 객체의 필드를 읽고 새 구성이 무엇인지 확인합니다. 이후에 변경하려면 인터페이스에서 사용하는 리소스를 업데이트하세요. 시스템에서 이 메서드를 호출하면 활동의 Resources 객체가 새 구성에 따라 리소스를 반환하도록 업데이트됩니다. 이를 통해 시스템에서 활동을 다시 시작하지 않고도 UI 요소를 재설정할 수 있습니다.

예를 들어 다음 onConfigurationChanged() 구현은 키보드를 사용할 수 있는지 확인합니다.

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
    } else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
    }
}

Java

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
    } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
    }
}

이러한 구성 변경을 기반으로 애플리케이션을 업데이트하지 않아도 되는 경우, 대신 onConfigurationChanged()를 구현하지 않아도 됩니다. 이 경우 구성 변경 전에 사용된 모든 리소스가 계속 사용되며 활동의 다시 시작만 피한 것이 됩니다. 예를 들어 TV 앱은 블루투스 키보드가 연결되거나 분리될 때 반응하지 않을 수도 있습니다.

상태 유지

이 기법을 사용할 때도 일반 활동 수명 주기 동안 상태를 유지해야 합니다. 이는 다음과 같은 이유 때문입니다.

  • 피할 수 없는 변경사항: 방지할 수 없는 구성 변경은 애플리케이션을 다시 시작할 수 있습니다.
  • 프로세스 종료: 애플리케이션은 시스템에서 시작된 프로세스 종료를 처리할 수 있어야 합니다. 사용자가 애플리케이션을 종료하고 앱이 백그라운드로 이동하면 시스템은 앱을 소멸시킬 수 있습니다.

Jetpack Compose에서 구성 변경에 반응

Jetpack Compose를 사용하면 앱이 구성 변경에 더 쉽게 반응할 수 있습니다. 하지만 가능한 경우 모든 구성 변경에 관해 Activity 재생성을 사용 중지하더라도 앱은 여전히 구성 변경을 올바르게 처리해야 합니다.

Configuration 객체는 Compose UI 계층 구조에서 LocalConfiguration 컴포지션 로컬과 함께 사용할 수 있습니다. 변경될 때마다 LocalConfiguration.current에서 읽는 구성 가능한 함수가 재구성됩니다. 컴포지션 로컬의 작동 방식에 관한 자세한 내용은 CompositionLocal을 사용한 로컬 범위 지정 데이터를 참고하세요.

다음 예에서 컴포저블은 특정 형식으로 날짜를 표시합니다. 컴포저블은 LocalConfiguration.currentConfigurationCompat.getLocales()를 호출하여 시스템 언어 구성 변경에 반응합니다.

@Composable
fun DateText(year: Int, dayOfYear: Int) {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(
        "MMM dd",
        ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
    )
    Text(
        dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
    )
}

언어가 변경될 때 Activity 재생성을 방지하려면 Compose 코드를 호스팅하는 Activity가 언어 구성 변경을 선택 해제해야 합니다. 이렇게 하려면 android:configChangeslocale|layoutDirection으로 설정하세요.

구성 변경: 주요 개념 및 권장사항

다음은 구성 변경 작업 시 알아야 하는 주요 개념입니다.

  • 구성: 기기 구성은 앱 디스플레이 크기나 언어, 시스템 테마 등 UI가 사용자에게 표시되는 방식을 정의합니다.
  • 구성 변경: 구성은 사용자 상호작용을 통해 변경됩니다. 예를 들어 사용자는 기기 설정을 변경하거나 기기와 상호작용하는 물리적인 방식을 변경할 수 있습니다. 구성 변경을 방지할 수 있는 방법은 없습니다.
  • Activity 재생성: 기본적으로 구성 변경으로 인해 Activity 재생성이 발생합니다. 이는 새 구성의 앱 상태를 다시 초기화하는 기본 메커니즘입니다.
  • Activity 소멸: Activity 재생성으로 인해 시스템이 이전 Activity 인스턴스를 소멸시키고 대신 새 인스턴스를 만듭니다. 이전 인스턴스는 이제 더 이상 사용되지 않습니다. 참조가 남아 있으면 메모리 누수나 버그, 비정상 종료가 발생합니다.
  • 상태: 이전 Activity 인스턴스의 상태는 새 Activity 인스턴스에 없습니다. 서로 다른 두 개의 객체 인스턴스이기 때문입니다. UI 상태 저장에 설명된 대로 앱 및 사용자의 상태를 보존합니다.
  • 선택 해제: 구성 변경 유형에 관해 활동 재생성을 선택 해제하는 것이 최적화일 수 있습니다. 새로운 구성에 반응하여 앱이 제대로 업데이트되어야 합니다.

우수한 사용자 환경을 제공하려면 다음 권장사항을 따르세요.

  • 빈번한 구성 변경에 대비: API 수준, 폼 팩터 또는 UI 도구 키트와 관계없이 구성 변경이 드물거나 발생하지 않는다고 가정하지 마세요. 사용자로 인해 구성 변경이 발생하면 사용자는 앱이 업데이트되어 새 구성에서도 계속 올바르게 작동하기를 기대합니다.
  • 상태 보존: Activity 재생성이 발생할 때 사용자의 상태를 손실하지 마세요. UI 상태 저장에 설명된 대로 상태를 보존합니다.
  • 선택 해제를 빠른 해결 방법으로 선택하지 않기: 상태 손실을 방지하기 위한 빠른 방법으로 Activity 재생성을 선택 해제하지 마세요. 활동 재생성을 선택 해제하려면 변경사항을 처리한다는 약속을 이행해야 하지만 다른 구성 변경이나 프로세스 종료, 앱 종료로 인한 Activity 재생성으로 인해 여전히 상태가 손실될 수 있습니다. Activity 재생성을 완전히 사용 중지하는 것은 불가능합니다. UI 상태 저장에 설명된 대로 상태를 보존합니다.
  • 구성 변경을 피하지 않기: 구성 변경 및 Activity 재생성을 방지하기 위해 방향이나 가로세로 비율, 크기 조절 가능 여부에 제한을 두지 마세요. 이는 선호하는 방식으로 앱을 이용하려는 사용자에게 부정적인 영향을 미칩니다.

크기 기반 구성 변경 처리

크기 기반 구성 변경은 언제든지 발생할 수 있으며 사용자가 멀티 윈도우 모드로 전환할 수 있는 대형 화면 기기에서 앱이 실행될 때는 그 가능성이 더 높습니다. 사용자는 앱이 이러한 환경에서 잘 작동하기를 기대합니다.

크기 변경에는 일반적인 두 가지 유형이 있습니다. 상당한 변경과 미미한 변경입니다. 상당한 크기 변경은 너비나 높이, 최소 너비와 같은 화면 크기의 차이로 인해 다른 대체 리소스 세트가 새 구성에 적용되는 변경입니다. 이러한 리소스에는 앱이 직접 정의하는 리소스와 라이브러리의 리소스가 포함됩니다.

크기 기반 구성 변경에 관해 활동 재생성 제한

크기 기반 구성 변경에 관해 Activity 재생성을 사용 중지하면 시스템은 Activity를 다시 만들지 않습니다. 대신 Activity.onConfigurationChanged() 호출을 수신합니다. 모든 연결된 뷰는 View.onConfigurationChanged() 호출을 수신합니다.

매니페스트 파일에 android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout"이 있는 경우 Activity 재생성이 크기 기반 구성 변경에 사용 중지됩니다.

크기 기반 구성 변경에 관해 활동 재생성 허용

Android 7.0(API 수준 24) 이상에서는 크기 변경이 상당한 경우에만 Activity 재생성이 크기 기반 구성 변경에 발생합니다. 시스템이 크기가 충분하지 않아 Activity를 다시 만들지 않으면 시스템은 대신 Activity.onConfigurationChanged()View.onConfigurationChanged()를 호출할 수도 있습니다.

ActivityView 콜백과 관련하여 몇 가지 주의사항이 있습니다.

  • Android 11(API 수준 30) 이상에서는 Activity.onConfigurationChanged()가 호출되지 않습니다.
  • Android 12L(API 수준 32) 및 Android 13(API 수준 33)에서는 View.onConfigurationChanged()가 호출되지 않습니다.

크기 기반 구성 변경을 수신 대기하는 데 의존하는 코드의 경우 Activity 재생성 또는 Activity.onConfigurationChanged()에 의존하는 대신 재정의된 View.onConfigurationChanged()가 있는 유틸리티 View를 사용하는 것이 좋습니다.