Developing Myself Everyday
article thumbnail

 

이 게시글은 아래의 공식 문서들을 보고 작성했습니다.

 

UI 상태 저장  |  Android 개발자  |  Android Developers

구성 변경 시 UI 상태를 유지하는 방법을 알아봅니다.

developer.android.com

 

활동 수명 주기에 관한 이해  |  Android 개발자  |  Android Developers

활동은 사용자가 전화 걸기, 사진 찍기, 이메일 보내기 또는 지도 보기와 같은 작업을 하기 위해 상호작용할 수 있는 화면을 제공하는 애플리케이션 구성요소입니다. 각 활동에는 사용자 인터페

developer.android.com


UI 상태를 왜 저장해야 하는가?


이 게시글에서는 UI 상태를 저장하는 방법과 ViewModel로 상태를 저장하는 방법을 자세히 배웁니다. 시스템에서 액티비티가 페기되거나 앱이 소멸된 후에 액티비티의 UI 상태를 저장하고 복원하는 것은 매우 중요합니다.

 

사용자는 다시 돌아왔을 때 UI 상태가 동일하게 유지되기를 기대하지만 시스템이 액티비티의 저장된 상태를 유지하지 않을 수 있습니다. 

 

그러므로 우린 UI 상태를 저장하는 방법을 알아야 합니다. 대표적으로 쓰이는 방법은 아래와 같습니다.

 

 - ViewModel 객체

 - Jetpack Compose: `rememberSaveable`

 - View: `onSaveInstanceState()` API

 - ViewModel: `SavedStateHandle`

 

 

 

사용자의 기대와 시스템 동작


사용자가 취하는 동작에 따라, 그들은 액티비티의 상태가 초기화되거나 상태가 보존될 것으로 기대합니다. 어느 경우에는 기대에 부응하지만, 시스템이 사용자의 기대와는 반대로 작동할 때도 있습니다.

 

 

'사용자' 가 UI 상태를 초기화하는 경우

사용자는 액티비티를 시작할 때 해당 액티비티의 일시적인 UI 상태가 액티비티를 완전히 닫을 때 까지 동일한 상태를 유지할 것으로 기대합니다. 사용자가 액티비티를 완전히 닫는 경우는 아래와 같습니다.

 

 - 최근 사용한 앱 화면에서 액티비티를 지웁니다.

 - 설정 화면에서 앱을 종료합니다.

 - 기기를 다시 시작합니다.

 - `완료` 동작을 수행합니다. (이 행위는 Actvity.finish()로 지원됩니다.)

 

사용자가 위와 같은 상황을 했을 때 액티비티가 깨끗한 상태에서 시작할 것으로 기대합니다. 이러한 상황에 대한 시스템 동작은 사용자의 기대와 일치합니다. 

 

 

'시스템' 이 UI 상태를 초기화하는 경우

사용자는 회전이나 탭을 눌러 다른 앱으로 전환할 때도 액티비티의 UI 상태가 유지될 것으로 기대합니다. 그러나 시스템의 동작은 사용자의 기대와 불일치합니다. 시스템은 이러한 상황에서 액티비티를 파괴하고 액티비티 인스턴스에 저장된 UI 상태를 모두 지웁니다.

 

사용자는 다시 앱에 돌아왔을 때 기대와는 다르게 깨끗한 상태의 액티비티를 마주하게 됩니다. 이는 우리가 바라지 않는 부분입니다. 우리는 사용자의 기대와 시스템의 동작을 일치시켜야 합니다.

 

 

UI 상태를 유지하기 위한 옵션

UI 상태에 대한 사용자의 기대와 시스템의 동작이 일치하지 않기 때문에 우리는 아래와 같은 옵션을 사용합니다.

 

 


위 표의 저장된 인스턴스 상태는 `onSaveInstanceState()``rememberSaveable` API와 `SavedStateHandle`을 포함합니다.

 

 

 

 

 View 시스템으로 상태 저장


`onSaveInstanceState()` API 사용해 상태 저장

'시스템' 이 UI 상태를 초기화하는 경우에는 액티비티의 인스턴스는 사라지더라도 시스템에 존재했다는 정보는 남아 있습니다. 사용자가 액티비티로 다시 돌아가려고 시도하면 시스템은 소멸 당시 액티비티의 상태를 설명하는 `저장된 데이터 세트` 를 사용해 해당 액티비티의 새로운 인스턴스를 생성합니다.

 

 

 `저장된 데이터 세트` Bundle 객체에 저장된 키-값 쌍의 컬렉션 입니다. 기본적으로 시스템은 Bundle 인스턴스 상태를 사용해 액티비티 레이아웃의 갹 View 객체 관련 정보를 저장합니다. 이런 과정을 통해 액티비티의 인스턴스가 소멸되고 재생성된 경우, 레이아웃의 상태는 별도의 코드 요청 없이 이전 상태로 복원됩니다. 

 

예시를 통해 구현해 보겠습니다. 

 

1️⃣

class MainActivity : AppCompatActivity() {

    private var clickCount = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button.setOnClickListener {
            clickCount++
            updateUI()
        }
    }

    private fun updateUI() {
        textView.text = "Button Clicked: $clickCount times"
    }
}

 

위의 코드는 사용자가 버튼을 클릭하면 `clickCount` 변수에 버튼을 클릭한 횟수를 저장하고 업데이트하는 코드입니다.

 

 

 

 

2️⃣

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt(KEY_CLICK_COUNT, clickCount)
}

companion object {
    private const val KEY_CLICK_COUNT = "clickCount"
}

 

액티비티가 정지되기 시작하면 인스턴스 상태 번들에 상태 정보를 저장할 수 있도록 시스템이`onSaveInstanceState()`를 호출합니다.

 

액티비티의 추가적인 인스턴스 상태 정보를 저장하려면 `onSaveInstanceState()` override하고, 액티비티가 예상치 못하게 소멸될 경우 저장되는 Bundle 객체에 키-값 쌍을 추가해야 합니다.

 

`onSaveInstanceState()`를 재정의할 경우 기본 구현에서 뷰 계층 구조의 상태를 저장하고자 한다면 상위 클래스 구현을 호출해야 합니다

 


Bundle의 키는 여러 곳에서 사용됩니다. 만약 이를 매번 문자열로 사용한다면 유지보수성 및 오류 방지를 위해서는 좋지 않습니다. 그렇기 때문에 번들의 키는 `const val` 을 사용해 정의하는 것이 좋습니다. 

 

 

 

저장된 인스턴스 상태를 사용해 액티비티 UI 상태 복원

액티비티가 이전에 소멸된 후 재성성되면, 시스템이 액티비티에 전달하는 Bundle로부터 저장된 인스턴스 상태를 복구할 수 있습니다. 복구는 `onCreate()` 에서 진행하거나 `onRestoreInstanceState()` 콜백 메서드를 사용해서 진행할 수 있습니다. 

 

Android Kotlin Fundamentals Course ~ 04.2: Complex lifecycle situations

 

`onCreate()`

아래의 코드는 `onCreate()` 에서 저장된 인스턴스 상태를 복구한 예시입니다.  

 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button.setOnClickListener {
        clickCount++
        updateUI()
    }

    if (savedInstanceState != null) {
        clickCount = savedInstanceState.getInt(KEY_CLICK_COUNT, 0)
        updateUI()
    }
}

 

`onCreate()` 메서드는 인스턴스를 생성하는 경우나 재생성하는 경우에 상관없이 호출되므로 상태 번들의 정보가 Null 일 수가 있습니다. (재생성하는 경우에만 상태 번들에 삭제되기 전의 액티비티의 인스턴스 상태 정보가 저장됩니다.) 그래서 상태 번들을 읽기 전에는 반드시 Null인지 확인해야 합니다.

 

`onRestoreInstanceState()` 

아래의 코드는 `onRestoreInstanceState()` 콜백 메서드를 구현해서 저장된 인스턴스 상태를 복구한 예시입니다.

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    clickCount = savedInstanceState.getInt(KEY_CLICK_COUNT, 0)
    updateUI()
}

 

`onRestoreInstanceState()` 메서드는 저장된 인스턴스 상태가 있을 경우에만 시스템이 호출하기 때문에 BundleNull인지 확인할 필요는 없습니다.

 

 

 

 

ViewModel의 객체로 상태 저장


`onSaveInstanceState()`를 사용해서 상태를 저장하고 복원하는 방법을 알았습니다. 다만 구글에서는 말합니다.

 

사용할 API는 상태가 유지된 위치와 필요한 로직에 따라 다릅니다. 비즈니스 로직에 사용되는 상태의 경우 ViewModel에 유지하고 SavedStateHandle을 사용하여 저장합니다. UI 로직에 사용되는 상태의 경우에는 뷰 시스템에서 onSaveInstanceState API를 사용하거나 Compose에서 rememberSaveable을 사용합니다.

 

비즈니스 로직을 위한 데이터를 처리할 때 `onSaveInstanceState()`를 사용하는 것은 좋지 않은 방법이라고 구글은 말하고 있습니다. 

 

구글은 그에 대한 대안으로 ViewModel을 사용하라고 말합니다.

 

 

액티비티가 `onCreate()` 될 때 ViewModel이 생성되며, `onCreate()`가 여러 번 호출되어도 생성된 ViewModel은 계속해서 상태를 유지합니다. 

 

아래의 코드의 ViewModel은 상태를 저장할 변수를 선언합니다.

import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {
    // 상태를 저장할 변수들을 선언합니다.
    var count: Int = 0
    var text: String = ""
    
    // 기타 필요한 함수들을 추가할 수 있습니다.
}

 

 

아래의 코드에서 ViewModelProvider(this).get(MyViewModel::class.java)를 사용하여 액티비티의 라이프사이클에 맞게 ViewModel 인스턴스를 생성 및 관리합니다. ViewModel을 사용하여 counttext 값을 변경하고 액티비티의 UI에 반영합니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        // 저장된 상태를 가져와 UI에 반영합니다.
        countTextView.text = viewModel.count.toString()
        textTextView.text = viewModel.text

        incrementButton.setOnClickListener {
            viewModel.count++
            countTextView.text = viewModel.count.toString()
        }

        changeTextButton.setOnClickListener {
            viewModel.text = "New Text"
            textTextView.text = viewModel.text
        }
    }
}

 

 

다만 시스템이 UI 상태를 초기화하는 경우에는 ViewModel 또한 파괴됩니다. 그렇기 때문에 프로세스에 중단된 경우에 복원이 필요하다면 `onSaveInstanceState()`를 ViewModel과 함께 사용해야 합니다.

 

 

 

 

ViewModel의 `SavedStateHandle`로 상태 저장


ViewModel의 생성자로 넘어오는 `SavedStateHandle`은 Key-Value 형태인 Map 구조입니다. 

ViewModel에 `SavedStateHandle`를 전달하는 방법은 `ViewModelFactory`를 사용하는 것입니다.

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val factory = SavedStateViewModelFactory(this.application, this)
        viewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)

        // 저장된 상태를 가져와 UI에 반영합니다.
        countTextView.text = viewModel.count.toString()
        textTextView.text = viewModel.text

        incrementButton.setOnClickListener {
            viewModel.count++
            countTextView.text = viewModel.count.toString()
        }

        changeTextButton.setOnClickListener {
            viewModel.text = "New Text"
            textTextView.text = viewModel.text
        }
    }
}

 

 

아래 코드에서  `SavedStateHandle`을 사용하여 counttext 값을 저장하고 가져오는 방식을 보여줍니다.  `SavedStateHandle`을 통해 상태를 저장하고 복원할 수 있습니다.

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel

class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    // 상태를 저장할 변수들을 선언합니다.
    var count: Int
        get() = savedStateHandle.get("count") ?: 0
        set(value) {
            savedStateHandle.set("count", value)
        }

    var text: String
        get() = savedStateHandle.get("text") ?: ""
        set(value) {
            savedStateHandle.set("text", value)
        }
    
    // 기타 필요한 함수들을 추가할 수 있습니다.
}

'Android' 카테고리의 다른 글

안드로이드의 부팅과 애플리케이션의 실행  (0) 2023.08.20
Android의 Build Process  (0) 2023.08.20
작업 및 백 스택 이해  (0) 2023.08.04
Android의 Intent (인텐트)  (0) 2023.07.23
안드로이드의 Context  (0) 2023.07.11
profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!