UI 상태 저장 및 복원의 필요성
안드로이드의 Activity는 시스템의 요청에 의해 언제든지 소멸될 수 있다. 사용자는 UI상태가 동일하게 유지되길 기대하기 때문에 Activity의 상태를 적시에 보존하고 복원하는 작업이 필요하다.
화면 회전 또는 멀티 윈도우 모드로 전환하는 것과 같이 Configuration이 변경되어도 사용자는 Activity의 UI상태가 그대로 유지하기를 기대한다. Activity는 Configuration이 변경되면 기존 Activity를 소멸시키고 새로운 Activity 인스턴스를 생성하기 때문에 이전 UI상태가 모두 날아가버린다.
이를 방지하기 위해서는 Activity가 가지고 있는 내부 콜백 메서드인 onSavedInstanceState() 와 onRestoreInstanceState() 를 통해 상태를 저장하고 복구할 수 있다. 그러나 Configuration변경의 경우 ViewModel을 사용하는 것 만으로도 UI상태를 유지할 수도 있다. 대부분의 경우에는 ViewModel과 저장된 인스턴스 상태(savedInstanceState)를 모두 사용해야한다.
아래 영상을 통해 UI 상태를 유지하지 못하는 앱을 확인하자.
https://www.charlezz.com/wordpress/wp-content/uploads/2020/06/ViewModel-Basic.mp4
https://www.charlezz.com/wordpress/wp-content/uploads/2020/06/ViewModel-Basic
Activity의 종료 그리고 상태 저장 및 복원 시기
사용자가 명시적으로 Activity를 종료하는 경우는 UI상태를 저장할 필요가 없기 때문에 시스템에서는 Activity와 관련된 ViewModel 및 저장된 인스턴스 상태를 메모리에서 삭제시킨다.
사용자가 명시적으로 Activity를 종료한 케이스는 다음과 같다.
- Back 버튼 누르기
- Recents(최근앱) 화면에서 앱을 밀어서 종료시키기
- 상위 액티비티로 이동하기
- 설정화면에서 앱을 강제로 종료하기
- finish() 호출에 의한 Activity 종료하기
사용자가 Activity를 명시적으로 종료하지 않았는데 Activity가 종료되는 케이스는 Configuration변경과 시스템에 의한 종료가 있다.
Configuration 변경은 런타임에 발생할 수 있는 Activity의 회전, 멀티 윈도우, 키보드 가용성 등을 말하며 이 경우 Activity를 다시 시작하게 된다. Configuration 변경의 경우 ViewModel을 사용하는 것만으로도 UI상태를 보존할 수 있다. 하지만 시스템에 의해서 Activity가 종료되는 경우 ViewModel도 같이 메모리에서 제거 되기 때문에 UI 상태를 보존할 수 없다.
시스템은 RAM에 여유 공간이 필요할 때 프로세스를 종료시킨다. 시스템이 특정 프로세스를 종료할 가능성은 그 시점의 프로세스 상태에 따라 달라진다. 그리고 프로세스 상태는 프로세스에서 실행되는 Activity 상태에 따라 달라진다. 다음은 프로세스 상태, Activity상태, 시스템이 프로세스를 종료할 가능성 사이의 상관관계를 나타낸다.
시스템에 의해서 Activity가 종료되는 경우 저장된 인스턴스 상태 또는 데이터베이스에 UI상태를 저장하여 복원하는 방법이 있다. 다음의 표는 UI 상태를 유지하기 위해 고려할만한 사항들을 정리하고 있다.
SavedStateHandle을 이용한 상태 저장/복원 구현하기
ViewModel을 위한 saved state 모듈은 lifecycle 버전 2.2.0에서 추가되었다.
build.gradle에 다음과 같은 아티팩트의 종속성을 추가하자. (최신 버전 확인)
dependencies {
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
}
SavedStateHandle을 ViewModel에서 다루기 위해서는 다음과 같이 SavedStateHandle을 받는 생성자를 ViewModel에 포함해야 한다.
class MainViewModel(val savedStateHandle: SavedStateHandle) : ViewModel() {
...
}
SavedStateHandle을 받도록 ViewModel 인스턴스를 생성하려면 AbstractSavedStateViewModelFactory를 확장하는 Factory 클래스를 만들어야 한다.
만약 ViewModel 생성자 파라미터 시그니쳐가 (Application, SavedStateHandle) 또는 (SavedStateHandle) 이라면 이미 정의된 SavedStateViewModelFactory 클래스를 이용하여 AbstractSavedStateViewModelFactory 구현을 대신할 수도 있다.
SavedStateViewModelFactory를 이용한 ViewModel 인스턴스화를 하는 코드는 다음과 같을 수 있다.
class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this)).get(MainViewModel::class.java) }
}
이제 ViewModel의 SavedStateHandle을 이용하여 간단한 데이터를 저장하고 복원하는 코드를 구현할 수 있다. 다음은 정수형 count 변수의 상태를 저장하고 복원하는 코드다.
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var count = 0
set(value) {
savedStateHandle.set("count",value)
field = value
} init {
savedStateHandle.get<Int>("count")?.run {
count = this
}
}}
키 — 값 형식으로 저장이 가능하고 불러오는 것이 가능하다.
SavedStateHandle 클래스에는 다음과 같이 키-값 맵에 필요한 메서드가 있다.
- get(String key)
- contains(String key)
- remove(String key)
- set(String key, T Value)
- keys()
또한 식별 가능한 LiveData 항목으로 래핑된 값을 반환하는 특별한 메서드 getLiveData(String key)도 있다.
SavedStateHandle로 부터 상태가 복원된 ViewModel을 참조하는 Activity는 다음과 같이 상태가 보존된 것을 확인할 수 있다.
[video width=”360" height=”736" mp4=”https://www.charlezz.com/wordpress/wp-content/uploads/2020/06/ViewModel-with-SavedStateHandle.mp4"][/video]
예제코드는 github에서 확인할 수 있다.
유의사항
- SavedStateHandle에 저장되는 데이터는 단순하고 가벼워야 한다. 복잡하거나 큰 데이터의 경우 데이터베이스를 사용하도록 하자.
- SavedStateHandle로 부터 복원된 상태값을 이용하여 다시 재쿼리를 하려는 경우, ViewModel에 캐시된 결과가 있는지 확인하자. 이미 ViewModel이 필요한 데이터를 로드 했으면 새로운 데이터를 불러올 필요가 없다.
- 시스템에 의한 Activity소멸 케이스를 테스트 하고 싶다면, 개발자 모드 > 액티비티 유지안함 옵션을 활성화 하자. 백그라운드로 진입하는 순간 바로 Activity가 소멸되고, 포어그라운드로 재진입했을 때 새로운 Activity가 생성되는 것을 확인할 수 있다. 또한 개발자 모드에서 백그라운드 프로세스 개수에 제한을 거는 방법도 유효하다.