ViewModel의 저장된 상태 모듈 Android Jetpack의 구성요소
UI 상태 저장에서 언급했듯이 ViewModel
객체가 구성 변경사항을 처리할 수 있으므로 개발자는 회전이나 다른 상황에서 상태에 신경 쓸 필요가 없습니다. 그러나 시스템에서 시작된 프로세스 종료를 처리해야 하는 경우 SavedStateHandle
API를 백업으로 사용하는 것이 좋습니다.
일반적으로 UI 상태는 활동이 아닌 ViewModel
객체에 저장되거나 참조됩니다. 따라서 onSaveInstanceState()
또는 rememberSaveable
을 사용하기 위해서는 저장된 상태 모듈이 개발자를 대신해 처리할 수 있는 상용구가 필요합니다.
이 모듈을 사용하면 ViewModel
객체는 생성자를 통해 SavedStateHandle
객체를 수신합니다. 이 객체는 저장된 상태에 객체를 작성하고 저장된 상태에서 객체를 검색할 수 있게 하는 키-값 맵입니다. 이러한 값은 시스템에서 프로세스가 중단된 후에도 유지되며 동일한 객체를 통해 계속 사용할 수 있습니다.
저장된 상태는 작업 스택에 연결됩니다. 작업 스택이 사라지면 저장된 상태도 사라집니다. 이는 앱을 강제 종료하거나 최근 메뉴에서 앱을 삭제하거나 기기를 재부팅할 때 발생할 수 있습니다. 이러한 경우 작업 스택이 사라지고 저장된 상태의 정보를 복원할 수 없습니다. 사용자가 시작한 UI 상태 닫기 시나리오에서는 저장된 상태가 복원되지 않습니다. 시스템에서 시작된 시나리오에서는 복원됩니다.
설정
Fragment 1.2.0 또는 전이 종속 항목인 Activity 1.1.0부터는 SavedStateHandle
을 ViewModel
의 생성자 인수로 사용할 수 있습니다.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
그런 다음 추가 구성 없이 ViewModel
인스턴스를 가져올 수 있습니다. 기본 ViewModel
팩토리는 ViewModel
에 적절한 SavedStateHandle
을 제공합니다.
Kotlin
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Java
class MainFragment extends Fragment { private SavedStateViewModel vm; public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { vm = new ViewModelProvider(this).get(SavedStateViewModel.class); ... } ... }
맞춤 ViewModelProvider.Factory
인스턴스를 제공할 때 AbstractSavedStateViewModelFactory
를 확장하여 SavedStateHandle
사용을 설정할 수 있습니다.
SavedStateHandle을 사용한 작업
SavedStateHandle
클래스는 set()
메서드와 get()
메서드를 통해 저장된 상태에 데이터를 작성하고 저장된 상태에서 데이터를 검색할 수 있게 하는 키-값 맵입니다.
SavedStateHandle
을 사용하면 쿼리 값이 프로세스 종료 전반에 유지되어 활동이나 프래그먼트에서 값을 수동으로 저장 및 복원하고 ViewModel
에 다시 전달하지 않고도 재생성 전과 후에 동일한 필터링된 데이터 세트가 사용자에게 표시됩니다.
SavedStateHandle
에는 키-값 맵과 상호작용할 때 예상되는 다른 메서드도 있습니다.
contains(String key)
- 지정된 키의 값이 있는지 확인합니다.remove(String key)
- 지정된 키의 값을 삭제합니다.keys()
-SavedStateHandle
내에 포함된 모든 키를 반환합니다.
또한 관측 가능한 데이터 홀더를 사용하여 SavedStateHandle
에서 값을 가져올 수 있습니다. 지원되는 유형 목록은 다음과 같습니다.
LiveData
getLiveData()
를 사용하여 관측 가능한 LiveData
에 래핑된 값을 SavedStateHandle
에서 가져옵니다.
키의 값이 업데이트되면 LiveData
는 새 값을 수신합니다. 대부분의 경우 데이터 목록을 필터링하기 위해 쿼리를 입력하는 등의 사용자 상호작용으로 인해 값이 설정됩니다. 그런 다음 업데이트된 값을 사용하여 LiveData
를 변환할 수 있습니다.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveData<List<String>> = savedStateHandle.getLiveData<String>("query").switchMap { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle savedStateHandle; public LiveData<List<String>> filteredData; public SavedStateViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; LiveData<String> queryLiveData = savedStateHandle.getLiveData("query"); filteredData = Transformations.switchMap(queryLiveData, query -> { return repository.getFilteredData(query); }); } public void setQuery(String query) { savedStateHandle.set("query", query); } }
StateFlow
getStateFlow()
를 사용하여 관측 가능한 StateFlow
에 래핑된 값을 SavedStateHandle
에서 가져옵니다.
키의 값을 업데이트하면 StateFlow
가 새 값을 수신합니다. 대부분의 경우 데이터 목록을 필터링하기 위해 쿼리를 입력하는 등의 사용자 상호작용으로 인해 값을 설정할 수도 있습니다. 그런 다음 다른 Flow 연산자를 사용하여 업데이트된 값을 변환할 수 있습니다.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
실험용 Compose 상태 지원
lifecycle-viewmodel-compose
아티팩트는
saveable
SavedStateHandle
와 Compose 간의 상호 운용성을 허용하는 API
Saver
를 하면 State
에서
rememberSaveable
를 통해 저장 가능
맞춤 Saver
또한 SavedStateHandle
로 저장할 수 있습니다.
Kotlin
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+) |
클래스가 위의 목록에 있는 클래스 중 하나를 확장하지 않는 경우 @Parcelize
Kotlin 주석을 추가하거나 Parcelable
을 직접 구현하여 parcelable 클래스로 만들 수 있습니다.
parcelable이 아닌 클래스 저장
클래스가 Parcelable
또는 Serializable
을 구현하지 않으며 이러한 인터페이스 중 하나를 구현하기 위해 수정될 수도 없는 경우, 이 클래스의 인스턴스를 SavedStateHandle
에 직접 저장할 수 없습니다.
Lifecycle 2.3.0-alpha03부터 SavedStateHandle
은 setSavedStateProvider()
메서드를 사용해 객체를 Bundle
로 저장하고 복원하는 자체 로직을 제공하여 모든 객체를 저장할 수 있습니다. SavedStateRegistry.SavedStateProvider
는 저장할 상태가 포함된 Bundle
을 반환하는 단일 saveState()
메서드를 정의하는 인터페이스입니다. SavedStateHandle
은 상태를 저장할 준비가 되면 saveState()
를 호출하여 SavedStateProvider
에서 Bundle
을 검색하고 연결된 키용으로 Bundle
을 저장합니다.
ACTION_IMAGE_CAPTURE
인텐트를 통해 카메라 앱에서 이미지를 요청하고 카메라가 이미지를 저장해야 하는 임시 파일에 전달하는 앱의 예를 생각해 보세요. TempFileViewModel
은 임시 파일을 만드는 로직을 캡슐화합니다.
Kotlin
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel() { } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } }
활동의 프로세스가 중단되고 나중에 복원되는 경우 임시 파일이 손실되지 않도록 TempFileViewModel
은 SavedStateHandle
을 사용하여 데이터를 유지할 수 있습니다. TempFileViewModel
이 데이터를 저장할 수 있게 하려면 SavedStateProvider
를 구현하고 ViewModel
의 SavedStateHandle
에 관한 제공자로 설정하세요.
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } } }
사용자가 돌아올 때 File
데이터를 복원하려면 SavedStateHandle
에서 temp_file
Bundle
을 검색합니다. 절대 경로가 포함된 saveTempFile()
에서 제공하는 것과 동일한 Bundle
입니다. 그런 다음 이 절대 경로를 사용하여 새 File
을 인스턴스화할 수 있습니다.
Kotlin
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 } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { Bundle tempFileBundle = savedStateHandle.get("temp_file"); if (tempFileBundle != null) { tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle); } savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } @Nullable private static File restoreTempFile(Bundle bundle) { if (bundle.containsKey("path") { return File(bundle.getString("path")); } return null; } } }
테스트의 SavedStateHandle
SavedStateHandle
을 종속 항목으로 사용하는 ViewModel
을 테스트하려면 필요한 테스트 값으로 SavedStateHandle
의 새 인스턴스를 만들어 테스트 중인 ViewModel
인스턴스에 전달합니다.
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
추가 리소스
ViewModel
의 저장된 상태 모듈에 관한 자세한 내용은 다음 리소스를 참고하세요.
Codelab
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- UI 상태 저장
- 관찰 가능한 데이터 객체로 작업
- 종속 항목이 있는 ViewModel 만들기