ViewModel의 저장된 상태 모듈   Android Jetpack의 구성요소

UI 상태 저장에서 언급했듯이 ViewModel 객체가 구성 변경사항을 처리할 수 있으므로 개발자는 회전 이나 다른 상황에서 상태에 신경 쓸 필요가 없습니다. 그러나 시스템에서 시작된 프로세스 종료를 처리해야 하는 경우 SavedStateHandle API를 백업으로 사용하는 것이 좋습니다.

일반적으로 UI 상태는 ViewModel 객체에 저장되거나 참조됩니다. 따라서 Compose에서 rememberSaveable을 사용하기 위해서는 저장된 상태 모듈이 개발자를 대신해 처리할 수 있는 상용구가 필요합니다.

이 모듈을 사용하면 ViewModel 객체는 생성자를 통해 SavedStateHandle 객체를 수신합니다. 이 객체는 저장된 상태에 객체를 작성하고 저장된 상태에서 객체를 검색할 수 있게 하는 키-값 맵입니다. 이러한 값은 시스템에서 프로세스가 중단된 후에도 유지되며 동일한 객체를 통해 계속 사용할 수 있습니다.

저장된 상태는 작업 스택에 연결됩니다. 작업 스택이 사라지면 저장된 상태도 사라집니다. 이는 앱을 강제 종료하거나 최근 메뉴에서 앱을 삭제하거나 기기를 재부팅할 때 발생할 수 있습니다. 이러한 경우 작업 스택이 사라지고 저장된 상태의 정보를 복원할 수 없습니다. 사용자가 시작한 UI 상태 닫기 시나리오에서는 저장된 상태가 복원되지 않습니다. 시스템에서 시작한 시나리오에서는 복원됩니다.

설정

SavedStateHandle을 사용하려면 ViewModel에 생성자 인수로 허용하세요.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

그러면 추가 구성 없이 컴포저블 내에서 ViewModel의 인스턴스를 가져올 수 있습니다. 기본 ViewModel 팩터리는 ViewModel에 적절한 SavedStateHandle을 제공합니다.

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

맞춤 ViewModelProvider.Factory 인스턴스를 제공할 때 SavedStateHandle 사용을 설정할 수 있습니다.CreationExtrasviewModelFactory

SavedStateHandle을 사용한 작업

SavedStateHandle 클래스는 set() 메서드와 get() 메서드를 통해 저장된 상태에 데이터를 작성하고 저장된 상태에서 데이터를 검색할 수 있게 하는 키-값 맵입니다.

SavedStateHandle을 사용하면 쿼리 값이 프로세스 종료 전반에 유지되어 활동이나 프래그먼트에서 값을 수동으로 저장 및 복원하고 ViewModel에 다시 전달하지 않고도 재생성 전과 후에 동일한 필터링된 데이터 세트가 사용자에게 표시됩니다.

SavedStateHandle 에는 키-값 맵과 상호작용할 때 예상되는 다른 메서드도 있습니다.

또한 관찰 가능한 데이터 홀더를 사용하여 SavedStateHandle에서 값을 가져올 수 있습니다. 지원되는 유형 목록은 다음과 같습니다.

StateFlow

StateFlow 관측 가능 항목으로 래핑된 SavedStateHandle에서 값을 가져올 수 있습니다. 값을 직접 변경해야 하는지 여부에 따라 읽기 전용 스트림 또는 변경 가능한 스트림 중에서 선택할 수 있습니다.

  • getStateFlow(): 상태를 읽기만 해야 하는 경우에 사용합니다. SavedStateHandle의 다른 위치에서 키의 값을 업데이트하면 StateFlow가 새 값을 수신합니다. 읽기 전용 스트림을 노출하고 Flow 연산자를 사용하여 변환하려는 경우에 적합합니다.
  • getMutableStateFlow(): 읽기 및 쓰기 액세스 권한이 모두 필요한 경우에 사용합니다. 반환된 MutableStateFlow.value를 업데이트하면 기본 SavedStateHandle이 자동으로 업데이트되므로 키를 수동으로 설정할 필요가 없습니다.

대부분의 경우 데이터 목록을 필터링하기 위해 쿼리를 입력하는 등의 사용자 상호작용으로 인해 이러한 값이 업데이트됩니다.

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    // Use getMutableStateFlow to read and write the query directly
    private val _query = savedStateHandle.getMutableStateFlow("query", "")
    val query: StateFlow = _query.asStateFlow()

    // Use getStateFlow if you only need a read-only stream to react to changes
    val filteredData: StateFlow<List> =
        query.flatMapLatest {
            repository.getFilteredData(it)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun setQuery(newQuery: String) {
        // Updating the MutableStateFlow automatically updates the SavedStateHandle
        _query.value = newQuery
    }
}

KotlinX 직렬화 지원

복잡한 UI 상태의 경우 KotlinX 직렬화와 함께 saved 속성 위임을 사용할 수 있습니다. 이 위임을 사용하면 맞춤 @Serializable 데이터 클래스를 SavedStateHandle에 직접 유지할 수 있습니다. 이렇게 하면 프로세스 종료 전반에서 ViewModel의 상태가 유지되므로 Compose UI가 재생성 시 상태를 원활하게 복원할 수 있습니다.

사용하려면 데이터 클래스에 @Serializable을 주석 처리하고 ViewModel에서 saved 위임을 사용하세요.

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
// Ensure you have the savedstate-ktx dependency
import androidx.savedstate.serialization.saved
import kotlinx.serialization.Serializable

@Serializable
data class UserFilterState(
    val searchQuery: String,
    val minAge: Int,
    val includeInactive: Boolean
)

class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {

    // The state is automatically serialized to a Bundle on process death,
    // and deserialized upon recreation.
    var filterState by savedStateHandle.saved {
        UserFilterState(searchQuery = "", minAge = 18, includeInactive = false)
    }

    fun updateQuery(newQuery: String) {
        // Mutating the property automatically updates the underlying SavedStateHandle
        filterState = filterState.copy(searchQuery = newQuery)
    }
}

Compose 상태 지원

상태가 KotlinX 직렬화 대신 Compose's Saver API에 의존하는 경우 lifecycle-viewmodel-compose 아티팩트가 saveable 위임을 제공합니다. 이렇게 하면 SavedStateHandle과 Compose의 Saver 간의 상호 운용성이 허용되므로 맞춤 SaverrememberSaveable을 통해 저장할 수 있는 모든 StateSavedStateHandle을 사용해서도 저장할 수 있습니다.

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

지원되는 유형

SavedStateHandle 내에 보관된 데이터는 앱의 나머지 savedInstanceState와 함께 Bundle, 로 저장되고 복원됩니다.

직접 지원되는 유형

기본적으로 다음과 같이 Bundle과 동일한 데이터 유형에 관해 SavedStateHandle에서 set()get()을 호출할 수 있습니다.

유형/클래스 지원 배열 지원
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

클래스가 위의 목록에 있는 클래스 중 하나를 확장하지 않는 경우 클래스를 parcelable로 만들려면 @Parcelize Kotlin 주석을 추가하거나 Parcelable을 직접 구현하면 됩니다.

비 parcelable 클래스 저장

클래스가 Parcelable 또는 Serializable을 구현하지 않으며 이러한 인터페이스 중 하나를 구현하기 위해 수정될 수도 없는 경우, 이 클래스의 인스턴스를 SavedStateHandle에 직접 저장할 수 없습니다.

Lifecycle 2.3.0-alpha03부터 SavedStateHandleBundle로 객체를 저장하고 복원하는 자체 로직을 제공하여 모든 객체를 저장할 수 있습니다.setSavedStateProvider() SavedStateRegistry.SavedStateProvider는 저장할 상태가 포함된 Bundle을 반환하는 단일 saveState() 메서드를 정의하는 인터페이스입니다. SavedStateHandle은 상태를 저장할 준비가 되면 saveState()를 호출하여 SavedStateProvider에서 Bundle을 검색하고 연결된 키용으로 Bundle을 저장합니다.

`ACTION_IMAGE_CAPTURE` 인텐트를 통해 카메라 앱에서 이미지를 요청하고 카메라가 이미지를 저장해야 하는 임시 파일에 전달하는 앱의 예를 생각해 보세요.ACTION_IMAGE_CAPTURE TempFileViewModel은 임시 파일을 만드는 로직을 캡슐화합니다.

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

활동의 프로세스가 중단되고 나중에 복원되는 경우 임시 파일이 손실되지 않도록 TempFileViewModelSavedStateHandle을 사용하여 데이터를 유지할 수 있습니다. TempFileViewModel이 데이터를 저장할 수 있게 하려면 SavedStateProvider을 구현하고 ViewModelSavedStateHandle에 관한 제공자로 설정하세요.

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

사용자가 돌아올 때 File 데이터를 복원하려면 SavedStateHandle에서 temp_file Bundle을 검색합니다. 절대 경로가 포함된 saveTempFile()에서 제공하는 것과 동일한 Bundle입니다. 그런 다음 이 절대 경로를 사용하여 새 File을 인스턴스화할 수 있습니다.

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

테스트의 SavedStateHandle

SavedStateHandle을 종속 항목으로 사용하는 ViewModel을 테스트하려면 필요한 테스트 값으로 SavedStateHandle의 새 인스턴스를 만들어 테스트 중인 ViewModel 인스턴스에 전달합니다.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

추가 리소스

ViewModel의 저장된 상태 모듈에 관한 자세한 내용은 다음 리소스를 참고하세요.

Codelab

콘텐츠 보기