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

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

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

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

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

설정

Fragment 1.2.0 또는 전이 종속 항목인 Activity 1.1.0부터는 SavedStateHandleViewModel의 생성자 인수로 사용할 수 있습니다.

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에는 키-값 맵과 상호작용할 때 예상되는 다른 메서드도 있습니다.

또한 관측 가능한 데이터 홀더를 사용하여 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 아티팩트는 SavedStateHandle와 Compose의 Saver 간의 상호 운용성을 허용하는 실험용 saveable API를 제공합니다. 따라서 rememberSaveable를 통해 맞춤 Saver와 함께 저장할 수 있는 StateSavedStateHandle로 저장할 수 있습니다.

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부터 SavedStateHandlesetSavedStateProvider() 메서드를 사용해 객체를 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;
    }
}

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

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