프로젝트 내에서 사용하고 있는 프래그먼트 구조를 먼저 보여드리자면 다음과 같습니다.
위와 같이 프래그먼트에서 결과값을 받아오는 구조로 만들었습니다.
Fragment 1 코드
private fun observeSelectionFragmentResult(navController: NavController) {
viewLifecycleOwner.repeatOnLifecycleExtension {
navController.currentBackStackEntry?.savedStateHandle?.getStateFlow<List<ExerciseSelection>>(
"key", listOf()
)?.onEach {
viewModel.addAdditionalExercise(it)
}?.onCompletion {
Log.d("PlannerFragment", "done : for the flow")
}?.collect()
}
}
ResultFragment 코드
private fun setResultToPlannerFragment(navController: NavController) {
viewLifecycleOwner.repeatOnLifecycleExtension {
viewModel.selectedExerciseList.collect { list ->
binding.btnExerciseSelect.setOnClickListener {
navController.previousBackStackEntry?.savedStateHandle?.set("key", list)
navController.popBackStack()
}
}
}
}
이렇게 작성하다 보니. savedStateHandle은 무엇인지 궁금해 졌고 이에 관련된 지식을 찾아보기로 하게 되었습니다.
SavedStateHandle 이란 무엇일까?
간단하게 말하면 앱이 포커싱이 되지 않더라도 데이터를 잠깐 동안 묶어둘 수 있는 저장 형태
SavedStateHandle은 왜 사용되는걸까?
이 물음에 답하기 위해서는 Configuration Change가 될때 사용되는 방법 2가지를 알아보아야 합니다.
- onSaveInstanceState 를 활용해서 configuration change가 발생했을때 데이터를 잠시 저장해둔다
- viewmodel 을 이용해서 activity나 fragment의 생명주기 보다 훨씬 길게 데이터를 보관한다.
하지만, 이런 좋은 방법들이 있지만, 문제점이 있다. 바로, 다른앱을 갔다가 돌아오면 UI상태가 초기화 되는 것입니다. 바로 아래 링크들을 통해 확인할 수 있는데요.
UI가 초기화 되는 모습
UI가 저장되는 모습 (+ saveStateHandle 사용)
Activity가 종료 되는 케이스
사용자가 명시적으로 Activity를 종료한 케이스는 다음과 같습니다.
- Back 버튼 누르기
- Recents(최근앱) 화면에서 앱을 밀어서 종료시키기
- 상위 액티비티로 이동하기
- 설정화면에서 앱을 강제로 종료하기
- finish() 호출에 의한 Activity 종료하기
ViewModel은 Configuration 변경의 경우 사용됩니다. 하지만 시스템에 의해서 Activity가 종료되는 경우 ViewModel도 같이 메모리에서 제거 되기 때문에 UI 상태를 보존할 수 없습니다.
SavedStateHandle 사용방법
savedStateHandle + ViewModel 사용방법 은 다음과 같습니다.
- 모듈 추가
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
2. Activity
class SavedStateViewModelCounterActivity : AppCompatActivity() {
private lateinit var counterViewModel: SavedStateCounterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityCounterBinding.inflate(layoutInflater)
setContentView(binding.root)
counterViewModel = ViewModelProvider(
this,
SavedStateViewModelFactory(application, this)
).get(SavedStateCounterViewModel::class.java)
Timber.d("ViewModel = ${counterViewModel.hashCode()}")
counterViewModel.countState.observe(this, Observer {
binding.counter.text = it.toString()
})
binding.fab.setOnClickListener {
counterViewModel.incCounter()
}
}
}
뷰모델 프로바이더를 통해서 데이터를 가져오는것은 기존 뷰모델 생성과 동일하지만, 두번째 파라미터로 SavedStateViewModelFactory 를 넣어줍니다.
3. Viewmodel 코드
class SavedStateCounterViewModel(
private val handle: SavedStateHandle
) : ViewModel() {
// Get value of SavedStateHandle
private var counter = handle.get<Int>("counter") ?: 0
set(value) {
// Set value of SavedStateHandle
handle.set("counter", value)
field = value
}
private val _countForm = MutableLiveData<Int>(counter)
val countState: LiveData<Int> = _countForm
// Get LiveData of SavedStateHandle
val countLiveData: LiveData<Int> = handle.getLiveData("count", 0)
fun incCounter() {
++counter
Timber.d("Inc Counter => $counter")
_countForm.value = counter
}
}
뷰모델은 간단하게 생성자로 savedstatehandle 을 넣어주어 구성하면 됩니다.
SavedStateHandle 이 처리 가능한 형태는?
Key-value 로 이루어진 Map 형태로 저장이 되는데 여기에 들어갈 수 있는 형은, Bundle 에 저장하므로 동일하게 처리가능한 형만 저장이 가능합니다.
유의사항
- SavedStateHandle에 저장되는 데이터는 단순하고 가벼워야 한다. 복잡하거나 큰 데이터의 경우 데이터베이스를 사용하도록 하자.
- SavedStateHandle로 부터 복원된 상태값을 이용하여 다시 재쿼리를 하려는 경우, ViewModel에 캐시된 결과가 있는지 확인하자. 이미 ViewModel이 필요한 데이터를 로드 했으면 새로운 데이터를 불러올 필요가 없다
Reference
- https://charlezz.medium.com/ui-상태-저장-및-복원의-필요성-a00297e7a20b
- http://pluu.github.io/blog/android/2020/02/20/savedstatehandle/
최근댓글