Android/Theory

Activity LifeCycle (액티비티 생명주기)

sh1mj1 2022. 8. 27. 18:31

Activity LifeCycle(액티비티 생명주기)

작년 2021년에 처음으로 kotlin 언어를 이용한 Android 개발을 시작할 때 액티비티 생명주기를 공부했다. 그리고 올해 상반기에 Swift 언어를 이용한 iOS 개발을 공부하면서 굉장히 많이 잊어버렸다..... 그래서 다시 공부를 해야할 필요성을 느꼈고 이 블로그에 제대로 정리하며 공부할 예정이다. 공부를 이미 한 내용일 것이니까 금방 정리할 수 있을 거라고 기대하면서.... ^^;

아래는 공부하며 참고한 링크이다.

https://bbaktaeho-95.tistory.com/62https://thinkground.studio/android-액티비티-생명주기-activity-lifecycle/https://brunch.co.kr/@mystoryg/80

https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko#oncreate

LifeCycle?

사용자가 앱을 사용하다가, 앱에서 나가고, 앱으로 다시 돌아오면 액티비티 인스턴스는 수명주기 안에서 "상태"가 바뀐다. Activity 클래스에서는 액티비티가 상태 변화를 알아차릴 수 있는 콜백을 제공한다.

<aside> 💡 즉, 안드로이드 앱이 실행된 후 다른 액티비티 화면으로 전환되거나, 화면이 꺼짐, 폰 상태바를 내려서 액티비티가 가려짐, 혹은 앱이 종료 될 때 등을 상태변화가 일어난다. 이러한 상태 변화가 있을 때마다 화면에 보이는 액티비티의 생명주기 메서드를 호출해서 상태 변화를 알려준다.

</aside>

예) 넷플릭스 동영상 플레이어 사용중,

  1. 사용자가 다른 앱으로 전환할 때 동영상 일시중지
  2. 네트워크 연결 종료
  3. 다시 돌아오면 네트워크를 다시 연결,
  4. 일시중지했던 지점에서 다시 동영상 시작

생명 주기 콜백을 잘 구현하면 앱에서 아래 문제가 발생하지 않도록 예방할 수 있다.

  • 사용자가 앱 사용 중 전화가 오거나 다른 앱으로 전환할 때 비정상 종료되는 문제
  • 사용자가 앱을 활발하게 사용하지 않는 경우 귀중한 시스템 리소스가 소비되는 문제
  • 앱에서 나갔다가 나중에 돌아왔을 때 사용자의 진행 상태가 저장되지 않는 문제
  • 화면이 가로 방향과 세로방향 회전할 때 또는 비정상 종료되거나 사용자의 진행 상태가 저장되지 않는 문제

아래는 안드로이드 Activity LifeCycle의 조직도이다.

Activity LifeCycle 조직도

생명주기 메서드 (Preview)

메서드 이름 액티비티 상태 설명

onCreate() 만들어짐 액티비티가 생성될 때 최초로 실행.
onStart() 화면에 나타남 화면에 보여지기 시작
onResume() 현재 실행 중, 화면에 나타남. 화면에 나타나 있고 실행 중임.
onPause() 화면이 가려짐. 액티비티 "화면의 일부"가 다른 액티비티에 가려짐
onStop() 화면이 없어짐. 다른 액티비티의 실행으로 완전히 가려짐
onDestory() 액티비티가 생성될 때 최초로 실행되 액티비티 종료됨.

사용자가 액티비티를 벗어나기 시작하면 시스템은 맥티비티 해체할 메서드를 호출한다. 어떤 경우에는 부분적으로 해체한다. 이 때 액티비티는 여전히 메모리 안에 있으면서 백그라운드로 이동하며(사용자가 다른 앱으로 전환할 경우) 다시 포그라운드(Foreground)로 돌아올 수 있다. 사용자가 해당 액티비티로 다시 돌아오는 경우 종료한 지점에서 다시 액티비티가 시작된다. 몇가지 예외를 제외하고 앱은 백그라운드에서 실행될 때 액티비티를 실행 할 수 없다.시스템은 그 시점의 액티비티 상태에 따라 특정 프로세스와 그 액티비티를 함께 종료할 지를 결정한다.

예) 게임을 하다가 카톡을 봐야 해서 화면 상단 상태바를 내림. 내리면서 화면의 일부가 가려지고 있을 때 : onPause()상태 바에 와있던 카톡 알림을 눌러 카카오톡 앱으로 이동. 기존 게임 앱은 화면에서 더이상 보이지 않음 : onStop()곧 바로 앱으로 돌아온다면 재시작되어 onStart() {onRestart}장시간 사용하지 않다가 앱을 실행시키면 이미 프로세스가 죽은 상태이므로 onCreate()

아래에서 더 자세히 알아보자.

생명주기 호출

액티비티 인스턴스를 생성하면 그 동시에 생성 관련 생명주기 메서드가 순차적으로 호출된다. 종료 시 소멸과 관련된 생명주기 메서드가 순차적으로 호출된다.

  • 액티비티 생성시

onCreate()

: 생성된 화면 요소를 메모리에 로드

onStart(), onResume()

: 화면의 요소를 나타내고 사용자와 상호작용 시작 (Resumed 상태)

  • 액티비티를 화면에서 제거

onPause(), onStop()

-> 뒤로 가기, finish()를 실행할 때 동시 실행

onDestroy()

-> 최정적으로 액티비티가 메모리에서 제거됨.

  • 액티비티 종료하지 않고 액티비티 실행

액티비티를 종료하지 않거나, 액티비티가 모두 가려지지 않을 때 다른 액티비티 실행

액티비티 종료하지 않고 다시 동일 한 액티비티 실행

onPause() -> 완전히 사라진 것은 아니므로 (Paused 상태)

onResume() -> 정지한 것이 아니므로 onStrat()를 거치지 않고 바로 onResume으로. (Resumed 상태)

액티비티를 종료하지 않거나, 액티비티가 모두 가려지지 않을 때 다른 액티비티 실행

onPause(), onStop() -> 현재 액티비티를 종료하지 않고 새로운 액티비티가 만들어 질때 (Stopped 상태)

onStart(), onResume() -> 두 메서드가 연속적으로 실행되고 Resumed 상태로 변경됨.

액티비티 종료하지 않고 다시 동일 한 액티비티 실행

생명주기 메서드(콜백) Detail

onCreate()

시스템이 먼저 액티비티를 생성할 때 실행됨.

필수적으로 구현해야 한다. 액티비티가 생성되면 상태는 (Crated) 상태.

예를 들어 onCreate() 구현하면

  • 목록에 데이터를 Binding
  • 액티비티를 ViewModel 과 연결
  • 일부 클래스의 클래수 변수를 인스턴스화 할 수 있다

이 메서드는 savedInstaceState 라는 매개변수를 수신하는데 이는 액티비티의 이전 저장 상태가 포함된 Bundle 객체이다. 당연히 처음 생성된 액티비티라면 객체의 값은 null!!

액티비티의 생명주기에 연결된 LifeCycle-aware component가 있으면 ON_CREATE 이벤트를 수신하고 @OnLifeCycleEvent 메서드를 호출한다.

XML파일을 정의하고 setContnetView()에 전달하여 화면에 표시할 수 있다. 처음 Activity을 만들면 우리가 매일 봐왔던 것과 같다!

class MainActivity2 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
    }
}

혹은 액티비티 코드에서 새 View 객체를 생성하고, 그 View를 ViewGroup에 넣어서 뷰 계층 구조를 만들 수 있다. 그 후 ViewGroup을 setContentView()에 전달해서 그 레이아웃을 사용할 수도 있다.

예를 들면) 액티비티 코드에서 View 객체를 시간에 따라 바뀌는 것으로 생성하고 그 View를 LinearLayout에 넣는다. 해당 레이아웃을 setContnetView(해당 LinearLayout) 하여 화면에 표시할 수 있다.

LifeCycle 에서 onCreate() 상태에 머무리지 않고 onCreate() 메소드 후 바로 onStart()와 onResume() 메소드를 실행시킨다.

아래 예시는 XML 레이아웃 파일에 정의된 user interface 선언, 멤버 변수 정의, 일부 UI 구성 등의 액티비티에 관한 기본 설정이다.

lateinit var textView: TextView

// some transient state for the activity instance
var gameState: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
    // call the super class onCreate to complete the creation of activity like
    // the view hierarchy
    super.onCreate(savedInstanceState)

    // recovering the instance state
    gameState = savedInstanceState?.getString(GAME_STATE_KEY)

    // set the user interface layout for this activity
    // the layout file is defined in the project res/layout/main_activity.xml
    setContentView(R.layout.main_activity)

    // initialize member TextView so we can manipulate it later
    textView = findViewById(R.id.text_view)
}

/* 이 콜백은 saved instance가 이전에 onSavedInstanceState()에서 사용되어 저장된 경우에만 호출된다.
onCreate() 에서 state을 복구하거나 선택적으로 다른 state 을 복구할 수 있다. 
후자는 보통 onStart() 가 완료된 후 사용된다.
savedInstanceState Bundle 은 onCreate() 에서 사용된 것과 동일하다. */
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY)
}

// 액티비티가 일시적으로 종료되었을 때 호출된다. 여기에 instance state을 저장한다.
override fun onSaveInstanceState(outState: Bundle?) {
    outState?.run {
        putString(GAME_STATE_KEY, gameState)
        putString(TEXT_VIEW_KEY, textView.text.toString())
    }
    // call superclass to save any view hierarchy
    super.onSaveInstanceState(outState)
}

onStart()

액티비티가 사용자에게 표시되고 앱의 액티비티를 Foreground에 보내 상호작용하게 한다.

이 액비티비의 생명주기에 연결된 모든 LifeCycle-aware component는 ON_START 이벤트를 수신한다.

마찬가지로 액티비티는 Start 상태에 머무리지 않고 이 콜백이 완료되면 액티비티가 onResume() 메서드 호출, RESUMED 상태에 들어간다.

onResume()

액티비티가 RESUME 상태에 들어가면 Foreground에 표시되고 onResume() 콜백 호출.

이 상태에서 앱은 사용자와 상호작용한다.

특정 이벤트 발생(전화가 오거나, 사용자가 다른 액티비티로 이동, 기기 화면이 꺼짐 등) 전까지 앱은 이 상태에 머문다. 이 액비티비의 생명주기에 연결된 모든 LifeCycle-aware component는 ON_RESUME 이벤트를 수신한다. 개발자는 이 상태에서 생명주기 component가 Foreground에서 사용자에게 보이는 동안 실행해야 하는 모든 기능을 활성화할 수 있다.

예를 들어서 카메라 미리보기 시작 등과 같은 방해되는 이벤트가 발생하면

-> Pasued 상태 ->onPause() 콜백 호출

-> 다시 Resumed 상태로 돌아오면 -> 다시 onResume() 콜백 호출한다..

따라서 onResume()을 구현하여 onPause() 일 때 해제하는 컴포넌트를 초기화하고 액티비티가 Resumed 상태로 전환될 때 마다 필요한 다른 초기화 작업도 수행해야 한다.

class CameraComponent : LifecycleObserver {
    ...

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun initializeCamera() {
        if (camera == null) {
            getCamera()
        }
    }
    ...
}

위 코드는 LifecycleObserver가 ON_RESUME 이벤트를 수신하면 카메라를 초기화하는 것이다.

그런데 멀티 윈도우모드(화면분할)에서는 Paused 상태일 때도 완전히 보일 수 있다. 멀티윈도우 모드일 때 다른 앱을 선택하면 onPause된다. 그러므로 액티비티가 Paused 상태지만 표시되어 있는동안 활성화시키고 싶다면 ON_START 이벤트가 실행된 이후에 카메라를 초기화해야 한다. 액티비티가 Paused 일 때 기능을 활성화하면 멀티 윈도우 모드에서 Resumed 상태인 다른 앱이 이 기능에 접근하지 못할 수도 있다. 그리고 이 경우 전반적인 사용자 환경이 실질적으로 저하될 수 있다.

멀티 윈도우 지원과 LifeCycle-aware component로 생명주기 처리하는 방법은 나중에 더 구체적으로 알아보자.

onPause()

  • 시스템은 사용자가 액티비티를 떠날 떄의 첫번째 신호로 이 메서드를 호출한다. 즉, 앱이 Puased 상태가 되어도 Destroyed로 연결되는 것은 아니다.)
  • 액티비티가 Foreground에 없다는 것을 의미한다.(멀티 윈도우 모드 제외)
  • onPause() 메서드를 사용하여 액티비티가 Paused 상태일 때 실행을 멈추고 잠시 후 다시 시작할 작업을 일시중지하거나 조정한다.

액티비티가 Paused 상태에 들어가는 이유

  • 일부 이벤트가 앱 실행을 방해 (가장 일반적)
  • 멀티 윈도우 모드에서 보이는 여러 앱 중 포커스를 가진 앱 제외하고 모두 Paused 됨
  • Dialog 같은 새로운 반투명 액티비티가 열림. (기존 액티비티가 부분적으로 보이지만 포커스 상태 아니면 Paused)

액티비티가 Paused 상태가 되면 생명주기의 연결된 모든 LifeCycle-aware component가 ON_PAUSE 이벤트를 수신한다. 구성요소가 Foreground에 있지 않을 때 실행할 필요가 없는 기능을 모두 정지할 수 있다.

또한 배터리나 데이터를 많이 잡아먹는 기능(GPS 등)의기능 혹은 액티비티가 Paused 상태이고 사용자가 필요로 하지 않을 때의 모든 리소스를 onPause() 메서드를 사용해 해제할 수 있다. 그런데 멀티윈도우 모드일 때는 Paused 상태여도 화면에 보일 수 있으므로 onStop() 상태일 때 해제하는 것으로 하는 것이 바람직하다.

class CameraComponent : LifecycleObserver {
    ...
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun releaseCamera() {
        camera?.release()
        camera = null
    }
    ...
}

위 코드는 LifecycleObserver가 ON_PAUSE 이벤트에 응답하여 ON_RESUME 이벤트가 수신된 후 초기화된 카메라를 해제하는 예시이다.

onPuase() 는 아주 잠깐 실행되므로 앱 사용자 데이터 저장, 네트워크 호출, 데이터베이스 처리를 실행하면 안된다. 대신 부하가 큰 종료 작업은 onStop() 상태일 때 실행되야 한다.

onPause() 메서드의 실행이 완료되더라도 액티비티가 Paused 상태로 남을 수 있다. 액티비티는 다시 시작되거나 완전히 보이지 않게 될 때까지 이 상태에 머문다.

→ 액티비티 Paused 상태.(Activity 인스턴스를 메모리에 남겨둠)

→ 다시 돌아와서 Resumed 상태(남겨두었던 인스턴스를 다시 호출). 이 때는 최상위 상태가 Resumed 상태인 콜백 메서드 중에 생성된 component는 다시 초기화할 필요가 없다.

→ 액티비티가 완전히 보이지 않게 되면 onStop() 호출한다.

onStop()

액티비티가 사용자에게 더이상 표시되지 않으면 Stop 상태이다.

예를 들어 새로 실행된 액티비티가 화면 전체를 차지할 경우에 onStop() 호출한다.

액티비티가 Stop 상태로 전환하면 생명주기와 연결된 모든 LifeCycle-aware component는 화면에 보이지 않을 때 실행할 필요가 없는 기능을 모두 정지할 수 있다.

onStop() 메서드에서는 앱이 사용자에게 보이지 않는 동안 앱은 불필요한 리소스를 해제하거나 조정해야 한다.(애니메이션을 일시중지 | UI 세밀한 위치 업데이트에서 대략적인 업데이트로 전환 등)

onPause() 대신 onStop()을 사용하면 사용자가 멀티 윈도우 모드에서 액티비티가 보여도 UI 관련 작업이 계속 진행됨.

또한 onStop()을 사용하면 CPU를 많이 사용하는 종료 작업 이 때 해야 한다. (정보를 데이터베이스에 저장할 시기가 없었다면 onStop() 중에 실행!)

아래 예는 초안 내용을 영구 저장소에 저장하는 onStop() 구현한 것.

override fun onStop() {
    // 부모클래스 메소드를 먼저 부르기
    super.onStop()

    // 액티비티가 멈추고 있으므로노트의 초안을 저장한다.
    // 그리고 현재 노트 진행을 잃지 않도록 하고 싶다.
    val values = ContentValues().apply {
        put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText())
        put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle())
    }

    // 이 업데이트를 AsyncQueryHandler 또는 같은 곳의 **백그라운드**에서 수행합니다
    asyncQueryHandler.startUpdate(
            token,     // call들의 상관관계를 보여주는 int token
            null,      // cookie (여기서는 사용되지 않음)
            uri,       // 업데이트할 노트의 URI
            values,    // 열 이름과 여기에 적용할 새 값의 map
            null,      // 사용될 기준 (선택되지 않음)
            null       // 열이 사용될 장소 (선택되지 않음)
    )
}

위 코드는 SQLite을 직접 사용한 것이다. 실제로는 SQLite을 통해 추상 계층을 제공하는 짖속성 라이브러리 Room을 사용해야 한다.

액티비티가 Stop 상태에 들어가면 Activity 객체는 메모리 안에 머문다. 이 객체가 모든 상태 및 멤버 정보를 관리하지만 window manager와 연결되어 있지 않다.

액티비티가 다시 시작되면 이 정보를 다시 호출한다. 최상위 상태가 Resumed 상태인 콜백 메서드일 때 생성된 컴포넌트는 다시 초기화할 필요는 없다.

또 시스템은 레이아웃에 있는 각 View 객체의 현재 상태도 기록한다**.** 만약 사용자가 EditText 위젯에 텍스트를 입력하면 해당 내용이 저장되기 때문에 이를 따로 저장하거나 복원할 필요없다.

액티비티가 Stopped 되면 시스템이 메모리를 복구할 필요가 있을 때 액티비티를 포함한 프로세스를 destroy 한다. 액티비티가 stopped 일 때 destroy 되더라도 시스템은 여전히 View 객체의 상태를 Bundle(key-value 쌍으로)에 보유하고 사용자가 액티비티로 돌아오면 그것들을 복구한다.

(Saving and restoring transient UI state 참고)

이 액티비티가 다시 시작되면 onRestart() 호출, 액티비티가 실행을 종료하면 onDestroy() 호출한다.

onDestroy()

onDestroy()는 액티비티가 소멸될 떄 호출된다.

  • 사용자가 액티비티를 완전히 닫거나 finish()가 호출되어 액티비티 종료될 때
  • 구성변경(기기회전 | 멀티 윈도우 모드)로 인해 시스템이 일시적으로 액티비티 Destroy.

액티비티가 destroyed 상태가 될 때 이 액티비티의 생명주기와 연결된 모든 LifeCycle-aware component는 ON_DESTROY 이벤트 수신한다. 이 때 생명주기 컴포넌트는 액티비티 destroy 전에 필요한 것들을 정리할 수 있다.

액티비티 Destroy 이유를 결정하는 로직을 입력하는 대신 ViewModel 객체를 사용해 액티비티의 관련 뷰 데이터를 포함해야 한다.

액티비티가 구성변경으로 인해 Restart하면 ViewModel은 그대로 보존되어 다음 액티비티 instance에 전달돼서 추가 작업이 필요하지 않다. 액티비티가 Restart하지 않으면 ViewModel은 onCleared() 메서드를 호출하여 액티비티 Destroy 전에 모든 데이터를 정리해야 한다.

위 두 상황은 isFinishing() 메서드로 구분할 수 있다.

액티비티 Stop 시 onDestroy() 는 액티비티가 수신하는 마지막 생명주기 콜백이 된다. 구성변경으로 onDestroy() 호출될 시 시스템이 바로 새 액티비티 인스턴스 생성한 후, 새로운 구성에서 새로운 인스턴스에 관한 onCreate()을 호출한다..

onDestroy() 콜백은 이전 콜백(onStop())에서 아직 해제되지 않은 모든 리소스를 해제해야 한다.

Activity 상태 및 메모리에서 killed

시스템은 RAM에 여유공간이 필요하면 프로세스를 kill 한다. 이 가능성은 프로세스의 상태에 따라 다르다.

kill 가능성 프로세스 상태 Activity 상태

최소 Foreground (포커스가 있거나 포커스를 가져올 예정) Created, Started, Resumed
중간 Background (포커스 상실) Paused
최대 Background (보이지 않음) Empty Stopped, Destroyed

시스템은 메모리 공간 확보를 위해 액티비티를 직접 종료하지 않고 프로세스를 kill한다. 그러면 프로세스가 담당하는 모든 액티비티가 kill된다.

Saving and Restoring transient UI state

Activity의 UI 상태는 화면 회전, 멀티 원도우 모드로 전환처럼 구성 변경이 발생해도 동일하게 유지되어야 한다. 하지만 시스템은 이런 상황에서 기본적으로 액티비티를 destroy 하여 액티비티 인스턴스에 있는 모든 UI 상태를 제거한다.

마찬 가지로 앱에서 다른 앱으로 잠시 전환되었다가 다시 돌아와도 UI 상태가 동일하게 유지되어야 한다. 하지만 액티비티가 stop 되면 시스템은 앱의 프로세스를 destroy 할 수도 있다.

액티비티가 destroy되면 ViewModel, onSaveInstanceState() 및 로컬 저장소를 결합해서 사용자의 임시 UI 상태를 보존해야 한다.

그래서 InstanceState가 도당체 뭔지, onSaveInstance() 메서드를 어떻게 구현하는지를 알아보자.

간단한 객체처럼 UI 데이터가 가볍다면 onSaveInstanceState()만으로도 UI 상태를 보존할 수 있다. 하지만 onSaveInstanceState() 는 직렬화/역직렬화 cost가 들어 대부분은 ViewModel 과 onSaveInstanceState() 을 함께 사용한다.

https://developer.android.com/topic/libraries/architecture/saving-states

Instance state

액티비티가 destroy 되는 시나리오는 크게 두가지이다. 정상적인 경우, 시스템 제약의 경우로 분리해서 보자.

  • 정상적인 경우
    • 사용자가 뒤로 버튼을 눌러 종료
    • finish() 메서드 호출하여 자체 소멸 신호.
      • 즉, 말 그래도 정상적인 경우이다.
  • 시스템 제약의 경우
    • 구성 변경으로 인한 종료
    • 메모리 부족으로 종료

시스템 제약의 경우 액티비티 인스턴스는 사라져도 시스템에 존재했다는 정보는 남는다. 사용자가 다시 액티비티로 돌아가려고 하면 시스템은 destroy 당시 액티비티의 상태를 저장한 data set을 사용해 액티비티의 새 instance을 생성한다.

<aside> 💡 시스템이 이전 상태를 복원하기 위해 사용하는 저장된 데이터를 Instance State 라고 한다. 이는 Bundle 객체이며 key-value 로 저장된다.

</aside>

시스템은 Bundle 인스턴스 상태를 사용하여 액티비티 레이아웃의 각 View 객체 정보를 저장한다. 즉, 위의 경우에서 레이아웃의 상태는 별도의 코드없이 이전 상태로 복원된다.

하지만 액티비티에서 사용자 진행 상태를 추적하는 멤버 변수처럼 액티비티에 복원하고자 하는 상태 정보가 더 많이 있을 때도 있다. 이 때는 Bundle 객체가 부족하여 ViewModel 클래스도 사용해야 한다.

Save simple, lightweight UI state using onSaveInstanceState()

액티비티가 정지될 때 시스템이 onSaveInstanceState() 메서드를 호출한다. 이 때 EditText 내 텍스트 혹은 ListView 위젯의 스크롤 위치 같은 임시 정보를 저장한다.

<aside> 💡 만약 추가적인 인스턴스 상태 정보를 저장하려면 onSaveInstanceState() 을 override 하여 Bundle 객체에 key-value 쌍을 추가해야 한다!!

</aside>

override fun onSaveInstanceState(outState: Bundle?) {
    // Save the user's current game state
    outState?.run {
        putInt(STATE_SCORE, currentScore)
        putInt(STATE_LEVEL, currentLevel)
    }

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(outState)
}

companion object {
    val STATE_SCORE = "playerScore"
    val STATE_LEVEL = "playerLevel"
}

주의할 점은 사용자가 정상적으로 액티비티를 destroy 하면 onSaveInstanceState() 가 호출되지 않는다.

영구 데이터를 저장하려면 액티비티가 Foreground에 있을 때 적절한 타이밍에 하거나 그 타이밍이 없다면 onStop() 메서드가 호출되었을 때 해당 데이터를 저장해야 한다.

Restore activity UI state using saved instance state

위에서 outState 라는 이름의 Bundle 객체에 저장한 인스턴스 상태는 Restarted 일 때 복구할 수 있다.

onCreate() 와 onRestoreInstanceState() 메서드 둘 다 동일한 Bundle 을 수신한다.

  • onCreate() 는 액티비티가 새 인스턴스를 생성하던 이전 인스턴스를 생성하던 무조건 호출된다. 즉, null 체크를 해주어야 한다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState) // Always call the superclass first

    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
        with(savedInstanceState) {
            // Restore value of members from saved state
            currentScore = getInt(STATE_SCORE)
            currentLevel = getInt(STATE_LEVEL)
        }
    } else {
        // Probably initialize members with default values for a new instance
    }
    // ...
}
  • onRestoreInstanceState() 을 구현하는 방법을 선택할 수도 있다. 이는 onStart() 후에 호출된다. 이 때 시스템은 복원할 저장 상태가 있을 때만 onRestoreInstanceState()를 호출한다. 그래서 null 체크가 필요없다.
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState)

    // Restore state members from saved instance
    savedInstanceState?.run {
        currentScore = getInt(STATE_SCORE)
        currentLevel = getInt(STATE_LEVEL)
    }
}

Navigating between activities

성공적인 액티비티 전환 구현을 위한

  • 액티비티 시작
  • 액티비티 상태 저장
  • 액티비티 상태 복원

을 알아보자.

Starting one activity from another

액티비티 간 전환을 할 때 startActivity(), startActivityForResult() 을 사용한다. 두 경우 모두 Intent 객체를 전달한다.

Intent 객체는 시작할 액티비티를 나타내거나 실행할 작업 유형을 설명한다. Intent 객체는 시작된 액티비티에서 사용할 소량의 데이터를 포함할 수 있다.

인텐트에 관해서는 다시 자세히 알아보자.

https://developer.android.com/guide/components/intents-filters?hl=ko

startActivity()

새로 시작된 액티비티가 결과를 반환할 필요가 없을 때 사용한다.

예시는 SignInActivity 라는 액티비티를 시작하는 것으로!! 레고

val intent = Intent(this, SignInActivity::class.java)
startActivity(intent)

Intent의 인수로는 순서대로 Context, 와 액티비티이다. 굉장히 쉽게 구현할 수 있다. 액티비티로 이동한 후 뒤로 가기를 통해 원래 액티비티로 돌아올 수 있다.

예를 들어서 이메일 or SMS 보내기, 상태 업데이트 등의 작업을 액티비티의 데이터를 사용해 수행할 수 있다. 이 때 기기에 있는 다른 앱이 제공하는 액티비티를 대신 활용하여 실행할 수 있다. 이 때 Intent 가 또 큰 역할을 한다.

실행하고자 하는 동작을 설명하는 인텐트를 생성하면 다른 앱에서 액티비티가 시작된다. 이 때 인텐트를 처리할 수 있는 액티비티가 여러 개인 경우, 사용자는 직접 선택할 수 있다. 아래는 다른 앱으로 이메일을 보내는 예시이다.

val intent = Intent(Intent.ACTION_SEND).apply {
    putExtra(Intent.EXTRA_EMAIL, recipientArray)
} // 여기서 EXTRA_EMAIL 은 이메일이 전송되어야 할 이메일 주소의 문자열 배열.
startActivity(intent)

이메일 앱이 이 인텐트에 응답하면 앱은 추가로 제공된 문자열 배열을 읽은 후 이메일 작성 양식의 수신 필드에 배치한다. 이 상황에서 이메일 앱의 액티비티가 시작되고 사용자가 이메일 앱의 작업을 끝내면 내 액티비티가 Restart 된다!

startActivityForResult()

액티비티가 종료될 때 내 액티비티에서 결과를 받을 수 있다.

예를 들어서 카메라 앱을 실행시킨 후 찍은 사진을 바로 가져오거나, 연락처 목록에서 어떤 사람을 선택하고 그 결과를 받는 액티비티를 시작할 수 있다.

이 때 startActivityForResult(Intent, int) 메서드를 사용한다. 이 때 int 값은 일종의 확인 코드이다. 동일한 액티비티에서 여러 개의 startActivityForResult(Intent, int) 을 구분하는 것이다. 이 확인코드는 global 변수가 아니다.

그리고 startActivityForResult(Intent, int) 의 결과는 onActivityResult(int, int, Intent) 메서드로 반환된다.

하위 액티비티가 있다면 setResult(int) 을 호출하여 상위 액티비티로 데이터를 가져올 수 있다. 이 때는 항상 result code을 제공한다. result code는 RESULT_CANCELED, RESULT_OK 혹은 RESULT_FIRST_USER_~~~ 인 맞춤 값이 될 수도 있다. 상위 액티비티는 확인코드와 함께 onActivityResult(int, int, Intent) 메서드를 사용하여 정보를 수신한다.

만약 하위 액티비티가 실행되지 않으면 상위 액티비티는 RESULT_CANCELED을 받는다.

class MyActivity : Activity() {
    // ...

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
            // When the user center presses, let them pick a contact.
            startActivityForResult(
                    Intent(Intent.ACTION_PICK,Uri.parse("content://contacts")),
                    PICK_CONTACT_REQUEST)
            return true
        }
        return false
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        when (requestCode) {
            PICK_CONTACT_REQUEST ->
                if (resultCode == RESULT_OK) {
                    startActivity(Intent(Intent.ACTION_VIEW, intent?.data))
                }
        }
    }

    companion object {
        internal val PICK_CONTACT_REQUEST = 0
    }
}

위 코드는 중앙버튼(KEYCODE_DPAD_CENTER)이 눌렸을 때 인텐트에 사용자가 누른 연락처에 대한 정보를 가져오도록 onKeyDown 을 override 하고,

onActivityResult 을 통해 intent.data 을 가져오는 코드 예시이다.

https://developer.android.com/guide/components/intents-common?hl=ko

위 링크에서 여러 앱을 실행하여 데이터를 인텐트로 가져오는 예시를 확인할 수 있다.

Coordinating activities

하나의 액티비티가 다른 액티비티를 실행하면 두 액티비티는 LifeCycle이 전환된다.

이와 같은 액티비티가 디스크 혹은 다른 곳에 저장된 데이터를 공유하는 경우, 첫번째 액티비티는 두번째 액티비티가 생성되기 전에 완전히 Stop 되지 않는다. 오히려 두번째 액티비티의 Start 과정이 첫번째 액티비티의 Stop 과정과 겹쳐서 일어난다.

액티비티 A 가 액티비티 B을 시작할 때 발생하는 순서는

  1. A 의 onPause 실행.
  2. B 의 onCreate(), onStart(), onResume() 메서드가 순차적 실행. 포커스는 B로 이동함.
  3. A 가 더 이상 화면에 표시되지 않으면 onStop()

'Android > Theory' 카테고리의 다른 글

MVC, MVP, MVVM 봐도 봐도 조금씩 헷갈리면 모르는 거임  (1) 2023.10.02
Android Room, SQLite 기본  (0) 2022.09.06
Android SharedPreference  (0) 2022.09.05
Android Thread, Handler, Runnable  (0) 2022.09.03
Intent는 택배 상자!  (1) 2022.08.28