일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 서비스 vs 쓰레드
- 안드로이드 os 구조
- 안드로이드 라이선스 종류
- 클래스
- jvm 작동 원리
- 자바 다형성
- 안드로이드 라이선스
- rxjava disposable
- android ar 개발
- 플러터 설치 2022
- ANR이란
- 스택 큐 차이
- 서비스 쓰레드 차이
- android retrofit login
- 안드로이드 유닛테스트란
- ar vr 차이
- 객체
- 스택 자바 코드
- 큐 자바 코드
- 안드로이드 유닛 테스트
- 안드로이드 유닛 테스트 예시
- 안드로이드 레트로핏 crud
- 2022 플러터 안드로이드 스튜디오
- 안드로이드 레트로핏 사용법
- jvm이란
- rxjava cold observable
- 멤버변수
- 2022 플러터 설치
- Rxjava Observable
- rxjava hot observable
- Today
- Total
나만을 위한 블로그
[Android] 액티비티 UI 상태 저장 (onSaveInstanceState, onRestoreInstanceState) 본문
[Android] 액티비티 UI 상태 저장 (onSaveInstanceState, onRestoreInstanceState)
참깨빵위에참깨빵_ 2025. 4. 2. 00:30UI는 언제든 예기치 못한 에러나 유저의 기똥찬 행동 앞에 크래시를 일으킬 수 있다. 이걸 원천적으로 봉쇄하는 것은 불가능하지만 개발 단계에서 데이터를 저장하고 이를 복원하는 처리를 구현함으로써 최대한 사용자 경험을 보완해줘야 한다.
이와 관련된 안드로이드 문서를 확인한다.
https://developer.android.com/topic/libraries/architecture/saving-states?hl=ko
UI 상태 저장 | Android Developers
구성 변경 시 UI 상태를 유지하는 방법을 알아봅니다.
developer.android.com
시스템에서 액티비티가 폐기되거나 앱 소멸 후에 신속하게 액티비티의 UI 상태를 저장, 복원하는 건 우수한 사용자 환경에 필수다. 유저는 UI 상태가 동일하게 유지되길 기대하지만 시스템이 액티비티와 저장된 상태를 폐기할 수도 있다. 유저 기대치와 시스템 동작 간의 간극을 메우려면 아래 방법들을 조합해서 사용하라
- 뷰모델 객체
- 컨텍스트 안에 저장된 인스턴스 상태(컴포즈의 rememberSaveable, 뷰 시스템의 onSaveInstanceState, 뷰모델의 SavedStateHandle)
- 앱, 액티비티 전환 중 UI 상태 유지를 위한 로컬 저장소
최적의 솔루션은 UI 데이터의 복잡성, 앱의 사용 사례, 데이터 접근 속도, 메모리 사용량 간의 균형 찾는 방법에 따라 달라진다. 앱은 유저 기대치를 충족하고 빠르고 반응성 높은 인터페이스를 제공해야 한다. 특히 회전 같은 일반적인 구성 변경 후에는 UI에 데이터를 로드할 때 지연되지 않게 한다
< 유저 기대치 및 시스템 동작 >
- 유저가 시작한 UI 상태 닫기
유저는 액티비티 시작 시 그 액티비티를 완전히 닫을 때까지 액티비티의 일시적인 UI 상태가 그대로 유지될 것으로 기대한다. 유저는 아래 동작으로 액티비티를 완전히 닫을 수 있다
- 개요(최근 사용) 화면에서 액티비티를 스와이프해서 닫기
- 설정 화면에서 앱 종료 또는 강제 종료
- 기기 재부팅
- Activity.finish()로 지원되는 일종의 마무리 작업 진행
액티비티를 완전히 닫은 유저는 영구적으로 액티비티에서 벗어났다고 가정하며 액티비티를 다시 열면 완전히 새로 시작될 것으로 예상한다. 이런 닫기 시나리오의 기본적인 시스템 동작은 유저 기대치와 일치한다. 액티비티 인스턴스는 내부에 저장된 상태, 액티비티 관련해서 저장된 인스턴스 상태 기록과 함께 메모리에서 폐기되고 삭제된다
완전한 닫기 규칙에는 예외가 몇 개 있다. 예를 들어 유저는 브라우저에서 뒤로가기 버튼을 써서 브라우저를 종료하기 전에 보고 있던 정확한 웹페이지로 되돌아간다고 기대할 수 있다
- 시스템에서 시작된 UI 상태 닫기
유저는 회전 or 멀티 윈도우 모드 전환 같은 구성 변경이 발생해도 액티비티의 UI 상태가 동일하게 유지될 것으로 기대한다. 그러나 기본적으로 시스템은 구성 변경 발생 시 액티비티를 폐기하고 액티비티 인스턴스에 저장된 UI 상태를 완전 삭제한다. 구성 변경의 기본 동작을 재정의하는 건 권장되지 않지만 가능은 하다
또한 유저는 일시적으로 다른 앱으로 전환한 후 나중에 다시 돌아오면 액티비티 UI 상태가 동일하게 유지될 것으로 기대한다. 검색 후 홈 버튼을 누르거나 전화를 받은 후 검색 액티비티로 돌아오면 유저는 검색 키워드, 결과가 이전과 같이 유지되기를 기대한다
이 시나리오에서 앱은 시스템이 앱 프로세스를 메모리에 유지하기 위해 작동하는 동안 백그라운드에 배치된다. 하지만 유저가 다른 앱과 상호작용하는 동안 시스템은 앱 프로세스를 폐기할 수 있다. 이 경우 액티비티 인스턴스는 내부에 저장된 상태와 함께 폐기된다. 유저가 앱을 재실행하면 액티비티는 깨끗한 상태다
< UI 상태를 유지하기 위한 옵션 >
UI 상태를 유지하기 위한 각각의 옵션은 유저 환경에 영향을 주는 측정기준에 따라 다르다
뷰모델 | onSaveInstanceState | 영구 스토리지 | |
저장소 위치 | 메모리 | 메모리 | 디스크 or 네트워크 |
구성 변경 시에도 유지? | 예 | 예 | 예 |
시스템에서 시작된 프로세스 종료 시에도 유지? | 아니오 | 예 | 예 |
유저의 완전한 액티비티 닫기 / onFinish() 시에도 유지? | 아니오 | 아니오 | 예 |
데이터 제한 | 복잡한 객체도 괜찮지만 가용 메모리에 의해 공간이 제한됨 | primitive 타입, 문자열 등 단순하고 작은 객체만 | 디스크 공간, 비용, 네트워크 리소스에서 검색하는 시간에 의해서만 제한됨 |
읽기 / 쓰기 시간 | 빠름(메모리 접근만) | 느림(직렬화 / 역직렬화 필요) | 느림(디스크 접근 or 네트워크 트랜잭션 필요) |
< 뷰모델을 써서 구성 변경사항 처리 >
뷰모델은 유저가 앱을 적극 사용하는 동안 UI 관련 데이터 저장, 관리하기에 이상적이다. 뷰모델을 쓰면 UI 데이터에 빠르게 접근할 수 있고 회전, 창 크기 조절, 일반적으로 발생하는 구성 변경 시 네트워크 or 디스크에서 데이터를 다시 가져오는 걸 피할 수 있다. 뷰모델은 메모리에 데이터를 보관하므로 디스크 or 네트워크에서 데이터를 검색하는 것보다 비용이 낮다. 구성 변경 중에는 메모리에 남아 있으며 시스템이 구성 변경으로 인한 새 액티비티 인스턴스와 뷰모델을 자동 연결한다
유저가 액티비티 or 프래그먼트를 종료할 때 or 개발자가 finish()를 호출할 때 뷰모델은 시스템에 의해 자동 폐기된다. 이는 사용자가 이런 시나리오에서 기대한 대로 상태가 삭제됨을 의미한다
onSaveInstanceState와 달리 뷰모델은 시스템에서 시작된 프로세스 종료 중에 폐기된다. 뷰모델에서 시스템이 시작한 프로세스가 종료된 후 데이터를 다시 로드하려면 SavedStateHandle을 사용하라. 또는 데이터가 UI와 관련 있고 뷰모델에 유지할 필요가 없다면 onSaveInstanceState 또는 컴포즈의 rememberSaveable을 사용하라. 데이터가 앱 데이터면 디스크에 유지하는 게 나을 수 있다. 구성 변경 시 UI 상태를 저장하기 위해 준비된 메모리 내 솔루션이 있다면 뷰모델을 안 써도 된다
< 저장된 인스턴스 상태를 백업으로 써서 시스템에서 시작된 프로세스 종료 처리 >
onSaveInstanceState, rememberSaveable, SavedStateHandle은 시스템이 액티비티 / 프래그먼트 같은 UI 컨트롤러를 폐기하고 나중에 재생성할 때 컨트롤러 상태를 다시 로드하는 데 필요한 데이터를 저장한다. 저장된 인스턴스 상태 번들은 구성 변경, 프로세스 종료 시에도 유지되지만 여러 API가 데이터를 직렬화하기 때문에 저장용량과 속도 제한이 있다. 직렬화될 객체가 복잡하면 직렬화에 많은 메모리가 소비될 수 있다. 직렬화 프로세스는 구성 변경 시 메인 쓰레드에서 발생하기 때문에 직렬화가 장기적으로 실행되면 프레임 하락, 시각적 끊김 현상이 발생할 수 있다
onSaveInstanceState는 액티비티가 중지된 경우에만 쓰여진 데이터를 저장한다. 이 생명주기 상태 간에 데이터를 기록하면 다음 생명주기 이벤트가 중지될 때까지 저장 작업이 지연된다
비트맵 같은 대량의 데이터, 길이가 긴 직렬화 / 역직렬화가 필요한 복잡한 데이터 구조를 저장하는 데 onSaveInstanceState를 쓰면 안 된다. 원시 타입과 String 등 단순하고 작은 객체만 저장해야 한다. 따라서 다른 지속성 매커니즘이 실패할 때 UI를 이전 상태로 복원하는 데 필요한 데이터를 재생성할 수 있게 onSaveInstanceState를 써서 id 등 필수적인 최소한의 데이터를 저장해야 한다. 대부분의 앱은 onSaveInstanceState를 구현해서 시스템에서 시작된 프로세스 종료를 처리해야 한다
번들 대신 PersistableBundle을 쓰면 시스템이 잠재적으로 데이터를 디스크에 저장할 수 있다. 하지만 프래그먼트, 컴포즈, 기타 androidx 라이브러리는 이를 지원하지 않기 때문에 번들을 대신 사용하라...(중략)...구성 변경 동안 DB에서 데이터를 다시 불러오느라 주기를 낭비하지 않으려면 뷰모델을 계속 써야 한다. 보존할 UI 데이터가 단순하고 가벼우면 onSaveInstanceState만 단독으로 쓸 수 있다...(중략)
< UI 상태 관리 : 분할 및 정복 >
여러 유형의 지속성 매커니즘으로 작업을 분할해서 UI 상태를 효율적으로 저장, 복원할 수 있다. 각 매커니즘은 데이터 복잡도, 접근 속도, 전체 기간의 균형에 따라 액티비티에 쓰이는 여러 유형의 데이터를 저장해야 한다
- 로컬 지속성 : 액티비티를 열고 닫을 때 손실하지 않으려는 모든 앱 데이터를 저장(오디오 파일, 메타데이터가 포함된 노래 전체 컬렉션)
- 뷰모델 : 관련 UI 컨트롤러를 표시하는 데 필요한 모든 데이터(화면 UI 상태)를 메모리에 저장(최근 검색한 노래, 최근 검색어)
- onSaveInstanceState : 시스템에서 UI를 중지한 후 재생성 시 UI 상태를 다시 로드하는 데 필요한 소량의 데이터를 저장한다. 복잡한 객체는 여기 저장하지 않고 로컬 스토리지에 보존하며 onSaveInstanceState에 이런 객체의 고유 id를 저장한다(최근 검색어 저장)...(중략)
쉽게 말해 UI 데이터를 저장하는 방법은 크게 뷰모델 내부 필드에 담기, onSaveInstanceState / rememberSaveable / 뷰모델의 SavedStateHandle, Room 등의 로컬 DB가 있다. 이 중 자신의 앱에서 저장하고자 하는 데이터가 뭐냐에 따라서 뭘 사용할지 정하면 된다.
이 포스팅에선 뷰 시스템에서 onSaveInstanceState로 저장하고 onRestoreInstanceState를 사용해서 UI 데이터를 저장, 복원하는 예시를 확인한다.
간단하게 editText를 하나 선언한다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.MainActivity">
<EditText
android:id="@+id/etMemo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:hint="메모를 입력하세요" />
</androidx.constraintlayout.widget.ConstraintLayout>
그리고 액티비티는 아래처럼 작성한다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = this::class.simpleName
private lateinit var binding: ActivityMainBinding
companion object {
private const val MEMO = "memo"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
if (savedInstanceState != null) {
val text = savedInstanceState.getString(MEMO, "")
Log.d(TAG, "## [save] onCreate > 복원된 텍스트 : $text")
binding.etMemo.setText(text)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val text = binding.etMemo.text.toString()
outState.putString(MEMO, text)
Log.d(TAG, "## [save] 저장된 텍스트 : ${outState.getString(MEMO, "")}")
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val restoredText = savedInstanceState.getString(MEMO, "")
Log.d(TAG, "## [save] 복원된 텍스트 : $restoredText")
binding.etMemo.setText(restoredText)
}
}
그리고 앱을 실행한다. 이후 아무 글자나 입력한 후 화면 회전 모드를 설정한 다음 기기를 가로, 세로로 돌리면 onSaveInstanceState -> onCreate -> onRestoreInstanceState 순으로 호출되며 editText에 내가 입력한 글자가 유지되는 게 보일 것이다.
그리고 라이트 모드 / 다크 모드로 전환한 다음 앱으로 돌아오면 이 때도 동일한 순서로 함수들이 호출되는 걸 볼 수 있다.
또한 개발자 옵션에서 스크롤을 밑으로 내리다 보면 앱 탭에서 활동 유지 안함 토글이 있다. 이것은 홈 버튼을 눌러서 앱을 백그라운드로 보내면 안드로이드 시스템이 강제로 onDestroy()를 호출해서 앱에 할당된 메모리를 제거시키는 기능이다. OS가 메모리 부족 같은 이유로 프로세스를 죽인 후 복원하는 상황을 테스트할 때 써먹을 수 있는 방법으로, onSaveInstanceState를 테스트할 때 자주 사용되는 기능이다. 디버그할 때만 켜놓는 걸 추천한다.
이것을 on으로 설정한 다음 앱으로 돌아와서 텍스트를 입력한 후, 홈으로 이동하면 onSaveInstanceState가 호출되면서 텍스트가 저장됐다는 로그가 표시된다.이후 앱 사용 목록에서 다시 앱을 포그라운드로 이동시키면 onCreate -> onRestoreInstanceState 순으로 함수가 호출되면서 editText에 입력했던 글자가 유지되는 걸 볼 수 있다.
실행 로그를 보면 onCreate에서 한 차례 데이터를 복원하는 걸 볼 수 있다. 그럼 onRestoreStateInstance는 필요없는 게 아닌가?
https://medium.com/jaesung-dev/%EB%86%93%EC%B9%98%EA%B8%B0-%EC%89%AC%EC%9A%B4-lifecycle-daf5b293f5e
(Android) 놓치기 쉬운 Lifecycle
Activity Lifecycle 관리
medium.com
(중략)...onRestoreInstanceState는 저장된 상태가 있고 액티비티가 다시 초기화됐을 때 onStart 이후에 호출되는 콜백이다. Bundle로 상태를 복원할 수 있고 onCreate와 다르게 non-null한 Bundle을 쓴다. 이 콜백은 항상 호출되는 생명주기 콜백이 아닌 반드시 비정상적인 종료에 의해 호출되는 콜백이기 때문에 어떤 데이터든 무조건 포함돼 있을 것이기 때문이다
onCreate에서도 저장된 상태를 복원할 수 있어서 굳이 필요한 콜백인가 하는 의문도 든다. 실제로 onSaveInstanceState로 저장한 데이터는 onCreate, onRestoreInstanceState에서 같은 Bundle 객체로 복원된다. onRestoreInstanceState가 상태 복원에서 갖는 이점은 하위 클래스의 유연한 확장성이다. 상태를 복원하고 일부 초기화가 필요하다면 onCreate보다 onRestoreInstanceState를 재정의하면 더 많은 유연성을 제공할 것이다
https://www.reddit.com/r/androiddev/comments/2afx13/onrestoreinstancestate_vs_oncreate/
From the androiddev community on Reddit
Explore this post and more from the androiddev community
www.reddit.com
onCreate, onRestoreInstanceState 중 원하는 걸 써도 된다. 난 서브 클래싱을 편하게 하는 것에 대한 논거가 잘 보이지 않는다. 가장 큰 차이점은 onCreate는 항상 호출되므로 처음 생성되는지 복원되는지에 따라 초기화 결정을 내릴 수 있고 onRestoreInstanceState는 복원되는 경우에만 호출되므로 처음 생성될 때 초기화할 수 없다는 것이다. 필요에 맞게 사용하라
https://stackoverflow.com/a/14676555
Are onCreate and onRestoreInstanceState mutually exclusive?
I have a couple of question regarding onRestoreInstanceState and onSaveInstanceState. 1) where do these methods fit the activity lifecycle? I have read a lot of documentation but there is no clea...
stackoverflow.com
(중략)...가장 좋은 사용법은 onCreate에 뷰 계층 구조를 배치하고 onRestoreInstanceState에서 이전 상태를 복원하는 것이다. 이렇게 하면 액티비티를 서브 클래싱하는 모든 사람이 onRestoreInstanceState를 재정의해서 복원된 상태 로직을 보강하거나 대체하도록 선택할 수 있다. 공식문서는 onRestoreInstanceState가 템플릿 메서드 역할을 한다는 걸 장황하게 설명한 것이다
활동 수명 주기 | App architecture | Android Developers
활동은 사용자가 전화 걸기, 사진 찍기, 이메일 보내기 또는 지도 보기와 같은 작업을 하기 위해 상호작용할 수 있는 화면을 제공하는 애플리케이션 구성요소입니다. 각 활동에는 사용자 인터페
developer.android.com
액티비티가 이전에 파괴된 후 재생성되면 시스템이 액티비티로 전달하는 번들에서 저장된 인스턴스 상태를 복구할 수 있다. onCreate와 onRestoreInstanceState 콜백 모두 인스턴스 상태 정보가 포함된 같은 번들을 받는다. 시스템이 액티비티의 새 인스턴스를 만들거나 이전 인스턴스를 재생성할 때 onCreate가 호출되므로 읽기 전에 state bundle이 null인지 확인해야 한다. null이면 시스템이 파괴된 이전 인스턴스를 복원하는 대신 액티비티의 새 인스턴스를 만드는 것이다
onRestoreInstanceState는 onSaveInstanceState가 호출된 경우에 한해 onCreate 후 onStart가 실행된 후에 실행되는데 이 때는 UI가 모두 그려진 상태기 때문에 뷰에 안전하게 접근할 수 있는 시점이다. 그래서 onRestoreInstanceState에서 리사이클러뷰 스크롤 위치 복원 등 기타 데이터 복원 로직들을 담아두고 onCreate에선 뷰 초기화, 기타 여러 로직들을 배치해서 onCreate는 액티비티 설정, onRestoreInstanceState는 상태 복원을 담당하게 할 수 있다. 역할 분리를 꾀할 수 있다는 것이다. 그러나 무조건 이렇게 할 필요는 없고, 내게 필요한 상태 복원 로직 중 onCreate에서 savedInstanceState의 null 체크가 필요하다면 onCreate에 복원 로직을 담으면 된다. 필요에 맞게 선택해서 사용하면 된다.
'Android' 카테고리의 다른 글
[Android] 일반적인 UseCase 패턴 실수 (0) | 2025.03.26 |
---|---|
[Android] Result와 Result.fold() 알아보기 (0) | 2025.03.24 |
[Android] @JvmOverloads란? (0) | 2025.03.03 |
[Android] ListAdapter란? ListAdapter 사용법 (0) | 2025.02.19 |
[Android] Unable to delete directory build 에러 해결 (0) | 2025.02.18 |