본문 바로가기

Programming/Android_Kotlin

SavedInstanceState와 ViewModel

SavedInstanceState

앱을 만들면서 늘 지나치던 아이가 있다….

onCreate에서 인자로 들어오는 savedInstanceState인데, Bundle 자료형으로 들어온다고 한다.

보통 화면을 회전할 때 사용한다고 개념적으로 알고만 있는데 이번에 직접 사용해보려고 한다.

class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }

    var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show()

				setView()
    }

    override fun onDestroy() {
        super.onDestroy()
        Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show()
    }

    private fun setView() {
        binding.count = count

        binding.button.setOnClickListener {
            binding.count = ++count
        }
    }
}

 

위와 같이 버튼을 누르면 화면에 표시되는 카운트가 1씩 증가하는 코드를 짜보자.

count라는 변수를 전역으로 관리하고 DataBinding을 통해 뷰에 count를 표시해준다.

당연하게도! 화면이 회전되면 레이아웃이 재구성되며 Activity 또한 destroy 된 후 가로 레이아웃에 맞춰진 화면으로 다시 create된다.

왼쪽 가로 → 오른쪽 가로처럼 레이아웃의 변경이 없을 경우에는 재생성되지 않는다.

문제는 이 경우 Activity가 재생성되며 멤버변수로 관리하던 count가 0으로 초기화가 된다는 것이다! 이러한 상황을 방지하기 위해 savedInstanceState를 사용한다.

class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }

    var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show()

				savedInstanceState?.let {
            this.count = it.getInt(countKey, 0) // state를 불러온다
        }

				setView()
    }

		/* 
        ** 여기서도 저장된 상태를 복구할 수 있다 ** 
        override fun onRestoreInstanceState(savedInstanceState: Bundle) {
            super.onRestoreInstanceState(savedInstanceState)
            this.count = it.getInt(countKey, 0)
        }
    */

		override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(countKey, count) // state를 저장한다
    }

    override fun onDestroy() {
        super.onDestroy()
        Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show()
    }

    private fun setView() {
        binding.count = count

        binding.button.setOnClickListener {
            binding.count = ++count
        }
    }
}

 

onSaveInstanceState()에서 savedInstanceState에 값을 저장해놓고, onCreate()나 onRestoreInstanceState()에서 불러와 사용한다면 count를 유지한 채로 화면 회전이 가능하다.

다시 말해 savedInstanceState에 저장해둔 값은 Activity가 destroy 돼도 날아가지 않는다!

따라서 뷰의 상태를 유지하기 위해 사용하고, 자세한 생명주기는 아래 도표를 참조하면 된다.


ViewModel

근데 ViewModel이 있는데 이걸 왜 쓰지?

ViewModel의 라이프사이클은 아래와 같다.

그림을 보면 알 수 있다싶이 Destroy가 된다고 해도 ViewModel이 clear되지 않는다.

그렇다면 ViewModel이 있는데 savedInstanceState를 사용할 이유가 있을까?

Android 개발자 가이드에는 아래처럼 기재되어 있다.

다시 말해 ViewModel은 시스템에서 프로세스 종료 시 데이터가 날아가지만 읽기/쓰기가 빠르고 복잡한 객체도 사용할 수 있다는 말.

위에서 봤듯이 savedInstanceState는 Bundle이기 때문에 primitive type밖에 저장할 수가 없다.

“시스템에서 프로세스 종료 시”는 무슨 말일까? 말그대로 프로세스가 종료되는 상황을 재현해보면 알 수 있다.

adb shell am kill {package명}

 

위의 이미지처럼 savedInstanceState에는 5, viewModel에는 10이라는 count가 저장되어있는 상황이라고 가정할 때, 터미널에서 위와 같은 명령어를 입력해보자.

앱을 종료하고 명령어를 입력한 뒤 돌아오면 savedInstanceState에 저장된 count는 살아있지만 ViewModel에 저장된 count는 초기화된 것을 볼 수 있다.

백그라운드에서 프로세스가 종료되었다 다시 실행되면 ViewModel이 새롭게 생성되기 때문이다. 그렇다면 ViewModel의 사용성이 많이 아쉬워진다. 이걸 해결할 방법은 없을까?


SavedStateHandle

Google에서도 이를 문제점으로 인식했는지 2019년 구글 I/O에서 SavedState ViewModel을 발표했다.

관련 영상: https://youtu.be/Qxj2eBmXLHg

사용법은 매우 단순하다. ViewModel의 생성자에 savedStateHandle만 넣어주면 된다.

class MainViewModel constructor(
  savedStateHandle: SavedStateHandle
) : ViewModel()

 

SavedStateHandle에서 지원하는 메소드는 다음과 같다.

특이점은 getLiveData를 사용하면 MutableLiveData로 얻어올 수가 있다.

class MainViewModel constructor(
  savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val _count = savedStateHandle.getLiveData("count", 0)
    val count: LiveData<Int> = _count

    fun countUp() {
        _count.value = _count.value?.plus(1)
    }
}

 

따라서 기존의 코드를 위처럼 SavedStateHandle을 사용하도록 리팩토링하면 savedStateHandle에서 값을 저장하고 읽어오도록 구현할 수 있다.

위처럼 코드를 변경 후 다시 프로세스를 죽여보자.

프로세스를 죽이고 다시 들어와도 ViewModel의 값이 멀쩡히 살아있는 게 보인다!


정리

여기까지 알아봤는데… 더더욱 의문이 생겼다… SavedInstanceState는 결국 왜 쓸까…? ChatGPT한테 물어보니 이렇게 답을 해줬다.

결국 savedInstanceState는 데이터보다는 UI 상태를 임시적으로 저장하는데 사용하면 좋을 것 같다.


참고 자료

https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-i-single-activities-e49fd3d202ab

https://developer.android.com/topic/libraries/architecture/saving-states.html?hl=en#options_for_preserving_ui_state

https://developer.android.com/reference/androidx/lifecycle/SavedStateHandle.html