Android Kotlin 기초 04.2: 복잡한 수명 주기 상황

이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.

소개

이전 Codelab에서는 ActivityFragment 수명 주기에 관해 알아보고 활동 및 프래그먼트의 수명 주기 상태가 변경될 때 호출되는 메서드를 탐색했습니다. 이 Codelab에서는 활동 수명 주기를 자세히 살펴봅니다. 또한 더욱 효과적으로 구성되고 유지 관리하기 쉬운 코드로 수명 주기 이벤트를 관리하는 데 도움이 되는 Android Jetpack의 수명 주기 라이브러리에 관해 알아봅니다.

기본 요건

  • 활동의 정의와 앱에서 활동을 만드는 방법을 알아야 합니다.
  • ActivityFragment 수명 주기 기본사항과 활동이 상태 간에 이동할 때 호출되는 콜백
  • onCreate() 또는 onStop() 수명 주기 콜백 메서드를 재정의하여 활동이나 프래그먼트 수명 주기의 다양한 시점에 작업을 실행하는 방법

학습할 내용

  • 수명 주기 콜백에서 앱의 일부를 설정, 시작, 중지하는 방법
  • Android 수명 주기 라이브러리를 사용하여 수명 주기 관찰자를 만들고 활동과 프래그먼트 수명 주기를 더 쉽게 관리할 수 있도록 하는 방법
  • Android 프로세스 종료가 앱의 데이터에 미치는 영향과 Android에서 앱을 종료할 때 자동으로 데이터를 저장하고 복원하는 방법
  • 기기 회전 및 기타 구성 변경이 수명 주기 상태를 변경하고 앱의 상태에 미치는 영향

실행할 작업

  • DessertClicker 앱을 수정하여 타이머 함수를 포함하고 활동 수명 주기의 다양한 시간에 타이머를 시작하고 중지합니다.
  • Android 수명 주기 라이브러리를 사용하도록 앱을 수정하고 DessertTimer 클래스를 수명 주기 관찰자로 변환합니다.
  • Android 디버그 브리지 (adb)를 설정하고 사용하여 앱의 프로세스 종료 및 그 이후에 발생하는 수명 주기 콜백을 시뮬레이션합니다.
  • onSaveInstanceState() 메서드를 구현하여 앱이 예기치 않게 종료될 때 손실될 수 있는 앱 데이터를 유지합니다. 앱이 다시 시작될 때 이 데이터를 복원하는 코드를 추가합니다.

이 Codelab에서는 이전 Codelab의 DessertClicker 앱을 확장합니다. 백그라운드 타이머를 추가한 다음 Android 수명 주기 라이브러리를 사용하도록 앱을 변환합니다.

이전 Codelab에서는 다양한 수명 주기 콜백을 재정의하고 시스템에서 이러한 콜백을 호출할 때 로깅하여 활동과 프래그먼트 수명 주기를 관찰하는 방법을 알아봤습니다. 이 작업에서는 DessertClicker 앱에서 수명 주기 작업을 관리하는 보다 복잡한 예를 살펴봅니다. 초당 실행 중인 로그 수와 함께 로그 구문을 출력하는 타이머를 사용합니다.

1단계: 디저트 타이머 설정하기

  1. 이전 Codelab의 DessertClicker 앱을 엽니다. 앱이 없는 경우 여기에서 DessertClickerLogs를 다운로드할 수 있습니다.
  2. Project 뷰에서 java > com.example.android.dessertclicker를 펼치고 DessertTimer.kt을 엽니다. 지금은 모든 코드가 주석 처리되어 앱의 일부로 실행되지는 않습니다.
  3. 편집기 창에서 모든 코드를 선택합니다. Code > Comment with Line Comment를 선택하거나 Mac (Control+/의 경우 Command+/)을 누릅니다. 이 명령어는 파일의 모든 코드를 주석 처리합니다. 앱을 다시 빌드할 때까지 Android 스튜디오에서 해결되지 않은 참조 오류를 표시할 수 있습니다.
  4. DessertTimer 클래스에는 타이머를 시작하고 중지하는 startTimer()stopTimer()가 포함됩니다. startTimer()가 실행되고 있으면 타이머는 1초마다 로그 메시지를 출력하며, 시간이 실행된 총 시간(초)이 표시됩니다. 그러면 stopTimer() 메서드는 타이머와 로그 구문을 중지합니다.
  1. MainActivity.kt를 엽니다. 클래스 상단에서 dessertsSold 변수 바로 아래에 타이머 변수(
    )를 추가합니다.
private lateinit var dessertTimer : DessertTimer;
  1. setOnClickListener()까지 호출한 직후 onCreate()까지 아래로 스크롤하여 새 DessertTimer 객체를 만듭니다.
dessertTimer = DessertTimer()


이제 디저트 타이머 객체가 있으므로 활동이 화면에 표시될 때에만 실행되도록 타이머를 시작하고 중지해야 하는 위치를 고려합니다. 다음 단계에서 몇 가지 옵션을 살펴봅니다.

2단계: 타이머 시작 및 중지하기

onStart() 메서드는 활동이 표시되기 직전에 호출됩니다. onStop() 메서드는 활동이 표시된 후에 호출됩니다. 이러한 콜백은 타이머를 언제 시작하거나 중지해야 할지를 결정하는 역할을 합니다.

  1. MainActivity 클래스의 onStart() 콜백에서 타이머를 시작합니다.
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. onStop() 타이머 타이머:
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. 앱을 컴파일하고 실행합니다. Android 스튜디오에서 Logcat 창을 클릭합니다. Logcat 검색창에 dessertclicker를 입력하면 MainActivity 클래스와 DessertTimer 클래스로 모두 필터링됩니다. 앱이 시작되면 타이머도 즉시 실행되기 시작합니다.
  2. 뒤로 버튼을 클릭하면 타이머가 다시 중지됩니다. 활동 및 제어하는 타이머가 모두 소멸되어 타이머가 중지됩니다.
  3. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. Logcat의 타이머는 0에서 다시 시작됩니다.
  4. 공유 버튼을 클릭합니다. Logcat에서 타이머가 아직 실행 중임을 알 수 있습니다.

  5. 버튼을 클릭합니다. Logcat에서 타이머 실행이 중지됩니다.
  6. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. Logcat의 타이머는 중단된 부분부터 다시 시작됩니다.
  7. MainActivityonStop() 메서드에서 stopTimer() 호출을 주석 처리합니다. stopTimer()를 주석 처리하면 onStart()에서 작업을 시작하지만 onStop()에서 다시 중지하는 것을 잊지 않는 경우가 표시됩니다.
  8. 앱을 컴파일하고 실행한 후 타이머가 시작된 후 홈 버튼을 클릭합니다. 앱이 백그라운드에 있더라도 타이머가 실행되고 계속해서 시스템 리소스를 사용합니다. 타이머가 계속 실행되도록 하는 것은 앱에 대한 메모리 누수이며 원하는 동작이 아닐 수 있습니다.

    일반적인 패턴은 콜백에서 항목을 설정하거나 시작할 때 상응하는 콜백에서 이를 중지하거나 삭제하는 것입니다. 이렇게 하면 더 이상 필요하지 않은 작업이 실행되지 않도록 할 수 있습니다.
  1. 타이머를 중지하는 onStop()의 줄에서 주석 처리를 삭제합니다.
  2. startTimer() 호출을 잘라 onStart()에서 onCreate()로 붙여넣습니다. 이 변경사항은 onCreate()를 사용하여 리소스를 초기화하는 대신 onStart()을 사용하여 리소스를 초기화하는 대신 onCreate()에서 리소스를 초기화하고 시작하는 경우를 보여줍니다.
  3. 앱을 컴파일하고 실행합니다. 예상대로 타이머가 실행됩니다.
  4. 홈을 클릭하여 앱을 중지합니다. 타이머 실행이 예상대로 중단됩니다.
  5. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. 이 경우 타이머는 앱이 다시 시작될 때만 호출되므로 앱이 포그라운드로 돌아올 때 호출되지 않으므로 타이머가 다시 시작되지 않습니다.

유의사항은 다음과 같습니다.

  • 수명 주기 콜백에서 리소스를 설정할 때 리소스를 분리합니다.
  • 해당 메서드에서 설정 및 해체
  • onStart()에서 항목을 설정한 경우 onStop()에서 중지하거나 다시 해체합니다.

DessertClicker 앱에서 onStart()에 타이머를 시작한 경우 onStop()에서 타이머를 중지해야 하는지 상당히 쉽게 알 수 있습니다. 타이머가 한 개만 있으므로 타이머를 중지하는 것을 기억하기가 어렵습니다.

더 복잡한 Android 앱에서는 onStart()onCreate()에 많은 항목을 설정한 다음 onStop() 또는 onDestroy()에서 모두 삭제할 수 있습니다. 예를 들어, 설정 및 해체, 시작 및 중지해야 하는 애니메이션, 음악, 센서 또는 타이머가 있을 수 있습니다. 비밀번호를 잊으면 버그와 두통이 발생할 수 있습니다.

Android Jetpack에 포함된 수명 주기 라이브러리는 이 작업을 단순화합니다. 라이브러리는 여러 수명 주기 상태에 있는 다수의 이동 부분을 추적해야 할 때 특히 유용합니다. 라이브러리는 수명 주기가 작동하는 방식을 뒤집습니다. 일반적으로 활동이나 프래그먼트는 수명 주기 콜백이 발생할 때 실행할 작업을 구성요소 (예: DessertTimer)에 알립니다. 하지만 수명 주기 라이브러리를 사용하면 구성요소 자체가 수명 주기 변경사항을 확인한 후 이러한 변경사항이 발생할 때 필요한 작업을 실행합니다.

수명 주기 라이브러리의 세 가지 주요 부분은 다음과 같습니다.

  • 수명 주기 소유자 - 수명 주기가 있는 구성요소입니다. ActivityFragment는 수명 주기 소유자입니다. 수명 주기 소유자는 LifecycleOwner 인터페이스를 구현합니다.
  • 수명 주기 소유자의 실제 상태를 유지하고 수명 주기 변경이 발생할 때 이벤트를 트리거하는 Lifecycle 클래스
  • 수명 주기 상태를 관찰하고 수명 주기가 변경될 때 작업을 실행하는 수명 주기 관찰자 수명 주기 관찰자는 LifecycleObserver 인터페이스를 구현합니다.

이 작업에서는 Android 수명 주기 라이브러리를 사용하도록 DessertClicker 앱을 변환하고, 라이브러리를 통해 Android 활동 및 프래그먼트 수명 주기를 더 쉽게 관리할 수 있는 방법을 알아봅니다.

1단계: Dessert타이머를 LifecycleObserver로 전환

수명 주기 라이브러리의 핵심 부분은 수명 주기 관찰 개념입니다. 관찰을 통해 클래스 (예: DessertTimer)가 활동 또는 프래그먼트 수명 주기를 파악하고 수명 주기 상태 변경에 응답하여 시작 및 중지할 수 있습니다. 수명 주기 관찰자를 사용하면 활동 및 프래그먼트 메서드에서 객체를 시작하고 중지하는 책임을 삭제할 수 있습니다.

  1. DesertTimer.kt 클래스를 엽니다.
  2. DessertTimer 클래스의 클래스 서명을 다음과 같이 변경합니다.
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

이 새로운 클래스 정의는 다음 두 가지 작업을 실행합니다.

  • 생성자는 타이머가 관찰하고 있는 수명 주기인 Lifecycle 객체를 사용합니다.
  • 클래스 정의는 LifecycleObserver 인터페이스를 구현합니다.
  1. runnable 변수 아래에서 클래스 정의에 init 블록을 추가합니다. init 블록에서 addObserver() 메서드를 사용하여 소유자 (활동)에서 전달된 수명 주기 객체 (관찰자)를 연결합니다.
 init {
   lifecycle.addObserver(this)
}
  1. startTimer()@OnLifecycleEvent annotation 주석을 추가하고 ON_START 수명 주기 이벤트를 사용합니다. 수명 주기 관찰자가 관찰할 수 있는 모든 수명 주기 이벤트는 Lifecycle.Event 클래스에 있습니다.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  1. ON_STOP 이벤트를 사용하여 stopTimer()과 동일한 작업을 실행합니다.
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

2단계: MainActivity 수정하기

FragmentActivity 슈퍼클래스에서 LifecycleOwner을 구현하므로 MainActivity 클래스는 상속을 통해 이미 수명 주기 소유자입니다. 따라서 활동 수명 주기를 인식하기 위해 해야 할 작업은 없습니다. 개발자는 Activity의 Lifecycle 객체를 DessertTimer 생성자에 전달하기만 하면 됩니다.

  1. MainActivity를 엽니다. onCreate() 메서드에서 this.lifecycle를 포함하도록 DessertTimer의 초기화를 수정합니다.
dessertTimer = DessertTimer(this.lifecycle)

활동의 lifecycle 속성에는 이 활동이 소유한 Lifecycle 객체가 있습니다.

  1. onCreate()startTimer() 호출과 onStop()stopTimer() 호출을 삭제합니다. 이제 DessertTimer가 수명 주기 자체를 관찰하고 수명 주기 상태가 변경되면 자동으로 알림을 전송하기 때문에 DessertTimer에서 활동에 어떤 작업을 할지 알릴 필요가 없습니다. 이제 이러한 콜백에서 메시지를 작성하기만 하면 됩니다.
  2. 앱을 컴파일하고 실행한 후 Logcat을 엽니다. 타이머가 예상대로 실행되고 있습니다.
  3. 홈 버튼을 클릭하여 앱을 백그라운드로 전환합니다. 예상대로 타이머 실행이 중지되었습니다.

Android가 백그라운드에 있는 동안 앱을 종료하면 앱과 앱의 데이터는 어떻게 되나요? 이 까다로운 극단적인 사례는 반드시 이해해야 합니다.

앱은 백그라운드로 전환될 때 폐기되지 않고 앱이 중지되었다가 사용자가 돌아올 때까지 기다립니다. 하지만 Android OS의 주요 관심사 중 하나는 포그라운드에서 원활한 활동을 실행하는 것입니다. 예를 들어, 사용자가 버스 앱을 포착하기 위해 GPS 앱을 사용하고 있다면 GPS 앱을 빠르게 렌더링하고 경로를 계속 표시하는 것이 중요합니다. 사용자가 며칠 동안 확인하지 않았을 수 있는 DessertClicker 앱을 백그라운드에서 원활하게 실행하는 것은 덜 중요합니다.

Android는 포그라운드 앱이 문제없이 실행될 수 있도록 백그라운드 앱을 조절합니다. 예를 들어 Android는 백그라운드에서 실행 중인 앱이 수행할 수 있는 처리량을 제한합니다.

Android가 앱 전체 프로세스를 종료하는 경우도 있는데 이 프로세스에는 앱과 관련된 모든 활동이 포함됩니다. Android는 시스템이 스트레스를 받고 시각적 지연의 위험이 있을 때 이와 같이 종료하므로 콜백이나 코드가 이 시점에서 추가로 실행되지 않습니다. 앱 프로세스는 백그라운드에서 자동으로 간단히 종료됩니다. 그러나 사용자에게는 앱이 닫힌 것처럼 보이지 않습니다. 사용자가 Android OS가 종료된 앱으로 다시 이동하면 Android가 앱을 다시 시작합니다.

이 작업에서는 Android 프로세스 종료를 시뮬레이션하고 앱이 다시 시작될 때 어떻게 되는지 검토합니다.

1단계: adb를 사용하여 프로세스 종료 시뮬레이션하기

Android 디버그 브리지(adb)는 컴퓨터에 연결된 에뮬레이터 및 기기로 안내를 전송할 수 있는 명령줄 도구입니다. 이 단계에서는 adb를 사용하여 앱 프로세스를 닫고 Android가 앱을 종료하면 어떻게 되는지 확인합니다.

  1. 앱을 컴파일하고 실행합니다. 컵케이크를 몇 번 클릭합니다.
  2. 홈 버튼을 눌러 앱을 백그라운드로 전환합니다. 이제 앱이 중지되었으며 Android에서 앱에서 사용하는 리소스가 필요한 경우 앱이 종료될 수 있습니다.
  3. Android 스튜디오에서 Terminal 탭을 클릭하여 명령줄 터미널을 엽니다.
  4. adb를 입력하고 Return 키를 누릅니다.

    Android Debug Bridge version X.XX.X로 시작하고 tags to be used by logcat (see logcat —help로 끝나는 많은 결과가 표시되면 모두 정상입니다. 대신 adb: command not found가 표시되면 실행 경로에서 adb 명령어를 사용할 수 있는지 확인합니다. 자세한 내용은 유틸리티 챕터의 "실행 경로에 adb 추가"를 참조하세요.
  5. 이 주석을 복사하여 명령줄에 붙여넣고 Return 키를 누릅니다.
adb shell am kill com.example.android.dessertclicker

이 명령어는 앱이 백그라운드에 있는 경우에만 연결된 기기 또는 에뮬레이터에 dessertclicker 패키지 이름으로 프로세스를 중단하도록 지시합니다. 앱이 백그라운드에 있었으므로 프로세스가 중지되었음을 나타내는 아무것도 기기나 에뮬레이터 화면에 표시되지 않습니다. Android 스튜디오에서 Run 탭을 클릭하여 '애플리케이션 종료됨'이라는 메시지를 확인합니다. Logcat 탭을 클릭하여 onDestroy() 콜백이 실행되지 않았음을 확인합니다. 활동이 간단히 종료되었습니다.

  1. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. 앱이 백그라운드로 전환되었는지 또는 완전히 중지되었는지와 관련해 최근 화면에 표시됩니다. 최근 화면을 사용하여 앱으로 돌아가면 활동이 다시 시작됩니다. 활동은 onCreate()를 포함하여 시작 수명 주기 콜백의 전체 집합을 거칩니다.
  2. 앱이 다시 시작되면 quot;score&quot(판매된 디저트 수와 총 달러)가 기본값 (0)으로 재설정됩니다. Android가 앱을 종료했다면 왜 그 상태를 저장하지 않았나요?

    OS가 앱을 다시 시작할 때 Android는 이전 상태로 앱을 재설정하려고 노력합니다. Android는 일부 뷰의 상태를 가져와 활동에서 벗어날 때마다 번들에 저장합니다. 자동으로 저장되는 데이터의 예로는 EditText의 텍스트 (레이아웃에 ID가 설정되어 있는 경우)와 활동의 백 스택이 있습니다.

    하지만 Android OS에서 모든 데이터를 알지는 못하는 경우도 있습니다. 예를 들어 DessertClicker 앱에 revenue와 같은 맞춤 변수가 있다면 Android OS는 이 데이터 또는 그 중요성을 알지 못합니다. 번들에 이 데이터를 직접 추가해야 합니다.

2단계: onSaveInstanceState()를 사용하여 번들 데이터 저장

onSaveInstanceState() 메서드는 Android OS에서 앱을 삭제하는 경우 필요할 수 있는 데이터를 저장하는 데 사용하는 콜백입니다. 수명 주기 콜백 다이어그램에서 onSaveInstanceState()는 활동이 중지된 후 호출됩니다. 또한 앱이 백그라운드로 전환될 때마다 호출됩니다.

onSaveInstanceState() 호출을 안전 조치라고 생각하세요. 활동이 포그라운드를 벗어날 때 소량의 정보를 번들에 저장할 수 있습니다. 이제 시스템은 이 데이터를 저장합니다. 앱이 종료될 때까지 기다리면 OS가 리소스 압력을 받을 수 있기 때문입니다. 매번 데이터를 저장하면 번들의 업데이트 데이터를 필요에 따라 복원할 수 있습니다.

  1. MainActivity에서 onSaveInstanceState() 콜백을 재정의하고 Timber 로그 문을 추가합니다.
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}
  1. 앱을 컴파일하고 실행한 다음 버튼을 클릭하여 앱을 백그라운드로 전환합니다. onSaveInstanceState() 콜백은 onPause()onStop() 바로 다음에 발생합니다.
  2. 파일 상단에서 클래스 정의 바로 앞에 다음 상수를 추가합니다.
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

이러한 키는 인스턴스 상태 번들에서 데이터를 저장하고 검색하는 데 사용합니다.

  1. onSaveInstanceState()까지 아래로 스크롤하면 outState 매개변수가 Bundle 유형입니다.

    번들은 키가 항상 문자열인 키-값 쌍 모음입니다. intboolean 값과 같은 기본 값을 번들에 넣을 수 있습니다.
    시스템에서 이 번들을 RAM에 보관하므로 번들의 데이터를 작게 유지하는 것이 좋습니다. 이 번들의 크기도 제한되지만 기기마다 크기는 다릅니다. 일반적으로 100k 미만을 저장해야 하며 그러지 않으면 TransactionTooLargeException 오류로 앱이 비정상 종료될 수 있습니다.
  2. onSaveInstanceState()에서 revenue 값 (정수)을 putInt() 메서드를 사용하여 번들에 넣습니다.
outState.putInt(KEY_REVENUE, revenue)

putInt() 메서드와 putFloat()putString()과 같은 Bundle 클래스의 유사한 메서드는 두 가지 인수를 사용합니다. 키 문자열(KEY_REVENUE 상수)과 저장할 실제 값입니다.

  1. 판매된 디저트 수와 타이머 상태를 포함하여 동일한 프로세스를 반복합니다.
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

3단계: onCreate()를 사용하여 번들 데이터 복원

  1. onCreate()까지 위로 스크롤하여 메서드 서명을 확인합니다.
override fun onCreate(savedInstanceState: Bundle) {

onCreate()는 호출될 때마다 Bundle을 가져옵니다. 프로세스 종료로 인해 활동이 다시 시작되면 저장한 번들이 onCreate()에 전달됩니다. 활동이 새로 시작되었다면 onCreate()의 번들은 null입니다. 따라서 번들이 null이 아니면 이전에 알려진 지점에서의 활동을 알고 있는 것입니다.

  1. DessertTimer 설정 후 onCreate()에 다음 코드를 추가합니다.
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

null 테스트는 번들에 데이터가 있는지 또는 번들이 null인지 확인하므로 결과적으로 앱이 새로 시작되었는지 또는 종료 후 다시 만들어졌는지 알려줍니다. 이 테스트는 번들에서 데이터를 복원하는 일반적인 패턴입니다.

여기에서 사용한 키(KEY_REVENUE)는 putInt()에 사용한 키와 같습니다. 매번 같은 키를 사용하도록 하려면 이러한 키를 상수로 정의하는 것이 좋습니다. putInt()를 사용하여 데이터를 번들에 넣은 것처럼 getInt()를 사용하여 번들에서 데이터를 가져옵니다. getInt() 메서드는 두 가지 인수를 사용합니다.

  • 키 역할을 하는 문자열(예: 수익 값의 "key_revenue")
  • 번들의 키에 값이 없는 경우를 위한 기본값

그러면 번들에서 가져온 정수가 revenue 변수에 할당되고 UI에서 이 값을 사용합니다.

  1. 판매된 디저트 수와 타이머 값을 복원하는 getInt() 메서드를 추가합니다.
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. 앱을 컴파일하고 실행합니다. 컵케이크가 도넛으로 바뀔 때까지 다섯 번 이상 누릅니다. 홈을 클릭하여 앱을 백그라운드로 전환합니다.
  2. Android 스튜디오 Terminal 탭에서 adb를 실행하여 앱의 프로세스를 종료합니다.
adb shell am kill com.example.android.dessertclicker
  1. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. 이번에는 앱이 정확한 수익으로 반환되며 번들에서 판매된 값을 디저트합니다. 그러나 디저트가 컵케이크로 돌아왔습니다. 앱이 정확히 종료된 상태 그대로 종료에서 돌아오도록 하기 위해 한 가지 작업이 더 남았습니다.
  2. MainActivity에서 showCurrentDessert() 메서드를 확인합니다. 이 메서드는 현재 판매된 디저트 수와 allDesserts 변수의 디저트 목록에 기반하여 어떤 디저트 이미지를 활동에 표시할지 결정합니다.
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

이 메서드는 판매된 디저트 수를 사용하여 올바른 이미지를 선택합니다. 따라서 onSaveInstanceState()의 번들에 이미지 참조를 저장하려고 별다른 작업을 하지 않아도 됩니다. 번들에 이미 판매된 디저트 수가 저장되고 있습니다.

  1. onCreate() 내 번들에서 상태를 복원하는 블록에서 showCurrentDessert()를 호출합니다.
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. 앱을 컴파일하고 실행한 후 백그라운드로 배치합니다. adb를 사용하여 프로세스를 종료합니다. 최근 항목 화면을 사용하여 앱으로 돌아갑니다. 이제 디저트 값, 총 수익 값, 디저트 이미지가 모두 올바르게 복원되었습니다.

구성 프래그먼트가 수명 주기와 프래그먼트의 수명 주기에 미치는 영향을 이해해야 하는 중요한 중요한 활동 및 프래그먼트 수명 주기를 관리하는 마지막 특수한 경우입니다.

구성 변경은 기기 상태가 매우 급격하게 변경되어 시스템이 변경사항을 확인하는 가장 쉬운 방법이 활동을 완전히 종료하고 다시 빌드하는 것일 때 발생합니다. 예를 들어 사용자가 기기 언어를 변경하면 다른 텍스트 방향을 수용하도록 전체 레이아웃을 변경해야 할 수 있습니다. 사용자가 기기를 도크에 연결하거나 물리적 키보드를 추가하면 앱 레이아웃은 다른 디스플레이 크기나 레이아웃을 활용해야 할 수 있습니다. 기기 방향이 변경되면(기기가 세로 모드에서 가로 모드로 또는 이와 반대로 회전하는 경우) 새 방향에 맞게 레이아웃을 변경해야 할 수 있습니다.

1단계: 기기 회전 및 수명 주기 콜백 살펴보기

  1. 앱을 컴파일하고 실행한 후 Logcat을 엽니다.
  2. 기기나 에뮬레이터를 가로 모드로 회전합니다. 회전 버튼을 사용하거나 Control과 화살표 키 (Mac은 Command와 화살표 키)를 사용하여 에뮬레이터를 왼쪽이나 오른쪽으로 회전할 수 있습니다.
  3. Logcat에서 출력을 확인합니다. MainActivity에서 출력을 필터링합니다.
    기기 또는 에뮬레이터가 화면을 회전하면 시스템은 모든 수명 주기 콜백을 호출하여 활동을 종료합니다. 그런 다음 활동이 다시 만들어질 때 시스템은 모든 수명 주기 콜백을 호출하여 활동을 시작합니다.
  4. MainActivity에서 전체 onSaveInstanceState() 메서드를 주석 처리합니다.
  5. 앱을 컴파일하고 다시 실행합니다. 컵케이크를 몇 번 클릭하고 기기나 에뮬레이터를 회전합니다. 이번에는 기기가 회전되어 활동이 종료되고 다시 만들어지면 활동이 기본값으로 시작됩니다.

    구성 변경이 발생하면 Android는 이전 작업에서 학습한 것과 동일한 인스턴스 상태 번들을 사용하여 앱의 상태를 저장하고 복원합니다. 프로세스 종료와 마찬가지로 onSaveInstanceState()를 사용하여 앱의 데이터를 번들에 넣습니다. 그런 다음 기기가 회전해도 활동 상태 데이터가 손실되지 않도록 onCreate()에서 데이터를 복원합니다.
  6. MainActivity에서 onSaveInstanceState() 메서드의 주석 처리를 삭제하고 앱을 실행한 후 컵케이크를 클릭한 다음 앱이나 기기를 회전합니다. 이번에는 활동 회전 전반에 걸쳐 디저트 데이터가 보관됩니다.

Android 스튜디오 프로젝트: DessertClickerFinal

수명 주기 도움말

  • 수명 주기 콜백에서 무언가를 설정하거나 시작한 경우 해당 콜백에서 무언가를 중지하거나 삭제합니다. 중단하는 일이 더 이상 필요하지 않을 때 계속 실행되지 않도록 하기 위해서입니다. 예를 들어 onStart()에 타이머를 설정했다면 onStop()에서 타이머를 일시중지하거나 중지해야 합니다.
  • onCreate()는 앱이 처음 시작될 때 앱의 한 부분을 실행하는 데만 사용합니다. onStart()를 사용하여 앱이 시작될 때와 앱이 포그라운드로 돌아올 때마다 실행되는 앱 부분을 시작합니다.

Lifecycle 라이브러리

  • Android 수명 주기 라이브러리를 사용하여 활동 수명 주기 제어를 활동 또는 프래그먼트에서 수명 주기를 인식해야 하는 실제 구성요소로 이동합니다.
  • 수명 주기 소유자ActivityFragment를 비롯하여 수명 주기가 있는 구성요소 (즉, 자체 키)입니다. 수명 주기 소유자는 LifecycleOwner 인터페이스를 구현합니다.
  • 수명 주기 관찰자는 현재 수명 주기 상태에 주의를 기울이고 수명 주기가 변경될 때 작업을 실행합니다. 수명 주기 관찰자는 LifecycleObserver 인터페이스를 구현합니다.
  • Lifecycle 객체에는 실제 수명 주기 상태가 포함되어 있으며 수명 주기가 변경될 때 이벤트를 트리거합니다.

수명 주기 인식 클래스를 만들려면 다음 안내를 따르세요.

  • 수명 주기를 인식해야 하는 클래스에 LifecycleObserver 인터페이스를 구현합니다.
  • 활동이나 프래그먼트의 수명 주기 객체를 사용하여 수명 주기 관찰자 클래스를 초기화합니다.
  • 수명 주기 관찰자 클래스에서 수명 주기 인식 메서드에 관심 있는 수명 주기 상태 변경에 주석을 추가합니다.

    예를 들어 @OnLifecycleEvent(Lifecycle.Event.ON_START) 주석은 메서드가 onStart 수명 주기 이벤트를 보고 있음을 나타냅니다.

프로세스 종료 및 활동 상태 저장

  • Android는 포그라운드에서 앱이 문제없이 실행될 수 있도록 백그라운드에서 실행되는 앱을 조절합니다. 이 규정으로 인해 앱이 백그라운드에서 처리할 수 있는 처리량을 제한할 수 있으며 경우에 따라 전체 앱 프로세스를 종료할 수도 있습니다.
  • 사용자는 시스템이 백그라운드에서 앱을 종료했는지 확인할 수 없습니다. 앱이 여전히 최근 화면에 표시되며 사용자가 앱을 종료했던 상태와 같은 상태로 다시 시작해야 합니다.
  • Android 디버그 브리지(adb)는 컴퓨터에 연결된 에뮬레이터 및 기기로 안내를 전송할 수 있는 명령줄 도구입니다. adb를 사용하여 앱에서 프로세스 종료를 시뮬레이션할 수 있습니다.
  • Android가 앱 프로세스를 종료할 때 onDestroy() 수명 주기 메서드가 호출되지 않습니다. 앱이 중지됩니다.

활동 및 프래그먼트 상태 보존

  • 앱이 백그라운드로 전환되면 onStop()이 호출된 직후 앱 데이터가 번들에 저장됩니다. EditText의 콘텐츠와 같은 일부 앱 데이터는 자동으로 저장됩니다.
  • 번들은 키와 값의 모음인 Bundle의 인스턴스입니다. 키는 항상 문자열입니다.
  • onSaveInstanceState() 콜백을 사용하여 앱이 자동으로 종료된 경우에도 유지하려는 번들에 기타 데이터를 저장합니다. 번들에 데이터를 넣으려면 put으로 시작하는 번들 메서드(예: putInt())를 사용합니다.
  • onRestoreInstanceState() 메서드 또는 더 일반적인 onCreate()의 번들에서 데이터를 다시 가져올 수 있습니다. onCreate() 메서드에는 번들을 보유하는 savedInstanceState 매개변수가 있습니다.
  • savedInstanceState 변수에 null가 포함된 경우 활동이 상태 번들 없이 시작되었으며 검색할 상태 데이터가 없는 것입니다.
  • 키를 사용하여 번들에서 데이터를 검색하려면 get으로 시작하는 Bundle 메서드(예: getInt())를 사용합니다.

구성 변경

  • 구성 변경은 기기 상태가 매우 급격하게 변경되어 시스템이 변경사항을 확인하는 가장 쉬운 방법이 활동을 종료하고 다시 빌드하는 것일 때 발생합니다.
  • 구성 변경은 사용자가 기기를 세로 모드에서 가로 모드로 또는 가로 모드에서 세로 모드로 회전할 때 가장 흔하게 발생합니다. 기기 언어가 변경되거나 하드웨어 키보드가 연결될 때도 구성 변경이 발생할 수 있습니다.
  • 구성 변경이 발생하면 Android는 모든 활동 수명 주기의 종료 콜백을 호출합니다. 그런 다음 Android는 처음부터 활동을 다시 시작하여 모든 수명 주기 시작 콜백을 실행합니다.
  • Android는 구성 변경으로 인해 앱을 종료할 때 onCreate()에서 사용할 수 있는 상태 번들로 활동을 다시 시작합니다.
  • 프로세스 종료와 마찬가지로 앱 상태를 onSaveInstanceState()의 번들에 저장합니다.

Udacity 과정:

Android 개발자 문서:

기타:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.

  • 필요한 경우 과제를 할당합니다.
  • 학생에게 과제 과제를 제출하는 방법을 알려주세요.
  • 과제 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.

이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.

앱 변경하기

강의 1에서 DiceRoller 앱을 엽니다. 앱이 없다면 여기에서 다운로드하세요. 앱을 컴파일하고 실행합니다. 기기를 회전하면 주사위의 현재 값이 손실됩니다. onSaveInstanceState()을 구현하여 번들에 이 값을 유지하고 onCreate()에 이 값을 복원합니다.

답변

질문 1

앱에 물리 시뮬레이션이 포함되어 있는데 이를 표시하려면 과도한 컴퓨팅이 필요합니다. 그런데 사용자가 전화를 받습니다. 이 상황과 관련하여 다음 설명 중 참인 것은 무엇인가요?

  • 전화 통화 중에도 물리 시뮬레이션에서는 계속해서 객체의 위치를 계산해야 합니다.
  • 전화 통화 중에는 물리 시뮬레이션에서 객체 위치 계산을 중지해야 합니다.

질문 2

앱이 화면에 없을 때 시뮬레이션을 일시중지하려면 어떤 수명 주기 메서드를 재정의해야 하나요?

  • onDestroy()
  • onStop()
  • onPause()
  • onSaveInstanceState()

질문 3

Android 수명 주기 라이브러리를 통해 클래스가 수명 주기를 인식하도록 하려면 클래스에서 어떤 인터페이스를 구현해야 하나요?

  • Lifecycle
  • LifecycleOwner
  • Lifecycle.Event
  • LifecycleObserver

질문 4

활동의 onCreate() 메서드가 데이터가 포함된 Bundle (즉, Bundlenull가 아님)을 수신하나요? 답은 2개 이상 적용될 수 있습니다.

  • 활동은 기기가 회전된 후에 다시 시작됩니다.
  • 활동은 처음부터 시작됩니다.
  • 활동은 백그라운드에서 돌아온 후 다시 시작됩니다.
  • 기기가 재부팅되었습니다.

다음 강의 시작: 5.1: ViewModel 및 ViewModelFactory

이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.