SaveStateHandle

IT/안드로이드 / / 2022. 10. 24. 22:38

프로젝트 내에서 사용하고 있는 프래그먼트 구조를 먼저 보여드리자면 다음과 같습니다.

위와 같이 프래그먼트에서 결과값을 받아오는 구조로 만들었습니다.

 

 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 은 저장된 인스턴스 상태 부분에 속한다.

 

SavedStateHandle은 왜 사용되는걸까?

이 물음에 답하기 위해서는 Configuration Change가 될때 사용되는 방법 2가지를 알아보아야 합니다.

  1. onSaveInstanceState 를 활용해서 configuration change가 발생했을때 데이터를 잠시 저장해둔다
  2. viewmodel 을 이용해서 activity나 fragment의 생명주기 보다 훨씬 길게 데이터를 보관한다.

하지만, 이런 좋은 방법들이 있지만, 문제점이 있다. 바로, 다른앱을 갔다가 돌아오면 UI상태가 초기화 되는 것입니다. 바로 아래 링크들을 통해 확인할 수 있는데요.

 

UI가 초기화 되는 모습

https://youtu.be/NdQwHwV0Vuo

UI가 저장되는 모습 (+ saveStateHandle 사용)

https://youtu.be/MXfWkQ_wnYk

 

Activity가 종료 되는 케이스

사용자가 명시적으로 Activity를 종료한 케이스는 다음과 같습니다.

  • Back 버튼 누르기
  • Recents(최근앱) 화면에서 앱을 밀어서 종료시키기
  • 상위 액티비티로 이동하기
  • 설정화면에서 앱을 강제로 종료하기
  • finish() 호출에 의한 Activity 종료하기

ViewModel은 Configuration 변경의 경우 사용됩니다. 하지만 시스템에 의해서 Activity가 종료되는 경우 ViewModel도 같이 메모리에서 제거 되기 때문에 UI 상태를 보존할 수 없습니다.

 

 

SavedStateHandle 사용방법

savedStateHandle + ViewModel 사용방법 은 다음과 같습니다.

  1. 모듈 추가
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

 

 

 

728x90
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기