Android Fragment 간의 ViewModel 공유하기



개인 광고 영역

안드로이드는 AAC-ViewModel을 제공하는데 기본 3가지를 제공한다.

  • Activity에서 만 사용하는 경우
  • Fragment에서 만 사용하는 경우
  • Activity를 기준으로 Fragment에서 공유해서 사용해야 할 경우

1번과 2번 케이스는 그냥 자기 자신만 사용하는 경우에 흔하게 사용한다.

3번의 케이스는 Activity는 하나이고, Fragment가 여러 개인데 데이터 관리는 Activity만을 기준으로 처리하는 경우가 있다.

이 경우 3번 케이스를 사용하는데, Fragment-KTX를 활용하면 매우 쉽게 접근할 수 있다.


이 글에서는

  • Activity/Fragment에서 자신의 ViewModel을 생성해 사용하는 방법 알아보기
  • Activity ViewModel을 활용하여 Fragment 들에서 사용하는 방법 알아보기
  • Fragment의 ChildFragment 간에 ViewModel을 공유하는 방법(Activity ViewModel을 활용하지 않고)

이렇게 3가지 방법을 알아보는 글이다.

그전에 AAC-ViewModel의 내부 구조가 궁금하다면 > Android Architecture Components ViewModel을 간단하게 초기화 하려면? 살펴보면 좋다.


Activity/Fragment에서 자신의 ViewModel을 생성해 사용하는 방법 알아보기

가장 일반적으로 사용하는 AAC-ViewModel 사용 방법인데, Activity/Fragment 구분 없이 각자가 자기 자신의 ViewModel을 생성하여, 자기 자신의 Lifecycle을 따르는 방법이다.

MainViewModel이 아래와 같이 정의되어 있고, 이를 Activity에서 초기화한다.

class MainViewModel : ViewModel() {

    private val _updateActivityCount = MutableLiveData<Int>(0)
    val updateActivityCount: LiveData<Int> get() = _updateActivityCount

    fun insertCountTwo() {
        _updateActivityCount.value = (updateActivityCount.value ?: 0) + 2
    }
}

Activity에서는 아래와 같이 ViewModel을 초기화할 수 있는데 종류별로 다 기입해본다.

KTX ViewModel을 활용하지 않는 경우에는 아래와 같이 lazy 하게 정의할 수 있다.

또는 lateinit으로 초기화할 수 있으나, lateinit은 언제든 갈아치울 수 있는 위험성(?)이 있기에 그냥 lazy로 처리한다.

private val viewModel: MainViewModel by lazy {
    ViewModelProvider(this@MainActivity, object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return MainViewModel() as T
        }
    }).get(MainViewModel::class.java)
}

viewModel-ktx를 활용하는 경우는 아래와 같다.

초기화 코드에 아무것도 존재하지 않으면 첫 줄의 viewModels()로 간단하게 처리가 가능하고, 초기화 코드가 들어가야 한다면 2번째 방법을 선택해야 한다.

private val viewModel: MainViewModel by viewModels()

private val viewModel: MainViewModel by viewModels {
    object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            MainViewModel() as T
    }
}

Fragment도 ViewModel 사용하는 방법은 큰 차이가 없어 링크로 대체한다.

Fragment ViewModel 초기화 방법


Activity ViewModel과 Fragment ViewModel의 관계

각각을 따로 생성했기 때문에 ViewModel은 Activity와 Fragment 각각 따로따로 생성되고, 서로 의존성은 존재하지 않는다.

sample_default


Activity ViewModel을 활용하여 Fragment 들에서 사용하는 방법 알아보기

Activity ViewModel을 공유해서 사용하는 경우는 어떤 경우가 있을까?

  • Fragment가 N 개인데, Data의 공유가 필요한 경우
  • Fragment에서 처리한 데이터에 따라 Activity의 데이터 갱신이 필요한 경우

보통 위와 같은 이유로 sharedViewModels를 사용하는 케이스가 있다.

ktx를 사용하지 않을 경우라면 다음과 같이 초기화할 수 있다.

액티비티에서 초기화하고, Fragment에서 당겨다 쓰려면 어떻게 해야 할까?

아래의 코드에서는 requireActivity()가 가장 중요하다. Fragment이기 때문에 Fragment ViewModelStoreOwner를 활용하지 않고, Activity의 ViewModelStoreOwner를 활용하여 이미 생성된 Activity의 ViewModel을 활용할 수 있다.

private val activityViewModel: MainViewModel by lazy {
    ViewModelProvider(requireActivity(), object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            MainViewModel() as T
    }).get(MainViewModel::class.java)
}

sharedViewModels의 사용은 매우 간단한데 fragment-ktx 활용하는 경우에는 activityViewModels를 사용하여 간단하게 사용하는 게 가능하다.

private val activityViewModel: MainViewModel by activityViewModels {
    object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            MainViewModel() as T
    }
}


좀 더 알아보자

activityViewModels는 무조건 Activity에서 초기화가 이루어진다는 전제는 없고, 이런 경우도 있다.

  • Activity에서는 초기화가 불필요하고, 그냥 Fragment에서 만 필요로 한다.

결국 ChildFragment 간의 데이터 공유가 필요한 케이스가 생긴다는 것이다. 이 경우는 ViewPager를 활용할 경우 ViewPager 간의 데이터 교환이 필요할 수 있다.

이런 경우 activityViewModels를 활용할 수 있으나 activity에서는 ViewModel 초기화가 필요치 않다.

이럴 경우에는 1개 이상의 Fragment에서 activityViewModels만을 진행한다.

이렇게도 가능하다. 그럼 어떻게 동작할까?

핵심은 Activity의 ViewModelStoreOwner를 활용하기 때문에 Fragment만 초기화 코드가 존재하더라도, Activity가 관리하는 ViewModel이 생성되며, Lifecycle에 따라 onDestroy를 처리한다.

Activity에서는 사용하지 않는 불필요한 코드를 가질 필요가 없다.

class MainActivity : AppCompatActivity() {
  // View만 초기화

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = MainActivityBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 생략
  }
}

class MainFragmentOne : Fragment() {
  private val sharedViewModel: SharedViewModel by activityViewModels()
  // View 초기화
}

class MainFragmentTwo : Fragment() {
  private val sharedViewModel: SharedViewModel by activityViewModels()
  // View 초기화
}

class SharedViewModel : ViewModel() {
  // todo...
}


Activity Shared ViewModel의 관계는

간단하게 그렸는데 복잡할 수 있으니 참고만…

activity_shared_view_model


Fragment의 ChildFragment 간에 ViewModel을 공유하는 방법(Activity ViewModel을 활용하지 않고)

이번엔 Activity와 무관하게 ChildFragment 간에 데이터 공유를 해보려고 하는데 어떤 케이스가 있을까?

  • ChildFragment를 기준으로 child 간의 데이터 공유가 필요한 경우
  • Child에서 만 데이터 공유가 필요한 경우
  • Activity와는 무관하게 처리하고 싶을 경우

이번엔 조금 복잡한 케이스에 해당하는데, Activity는 무관하게 동작해야 하는 경우도 있을것 같다.

예를 들면 Fragment의 ViewModel에서 데이터를 가져오고, 이를 Activity와 무관하게 child의 ViewPager에서만 활용해야 한다.

lifecycle은 Fragment를 유지해야 한다. 생각해보면 이런 케이스는 대부분 One Activity에서 많이 활용될 수 있어 보인다.

하나의 Activity에서 모든 하위의 ViewModel을 관리한다면 앱을 사용하는 동안 초기화 케이스가 없고, 사용시간이 길어지면 길어질수록 더 많은 ViewModel을 관리해야 할 것이다.

이 경우는 그냥 Fragment 안에서 처리하는 게 좋다.

다행히 AAC-ViewModel 샘플에는 이러한 주석이 포함되어 있다.

// Default scope may be overridden with parameter [ownerProducer]:
class MyFragment : Fragment() {
  val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
}

requireParentFragment을 이용하여 내 상위 Fragment의 ViewModel을 공유해서 사용하고 싶은 케이스에 위 코드처럼 사용할 수 있다.


샘플로 알아보자

복잡하게 구성했지만 핵심은 그냥 ParentFragment를 공유하는 것이다. 그래서 아래와 같이 표현해봤다.

ParentFragment_shared_02

설명으로 적으면

  • Activity는 MainViewModel만을 사용한다.
  • Fragment는 MainViewModel과 자신의 ViewModel을 활용한다.
  • ChildFragmnet를 만들고, ParentFragmentViewModel과 자신의 ViewModel을 활용한다.

복잡해지는 부분은 Activity에 붙어있는 Fragment가 Activity와 ChildFragment 모두를 알고 있어서 복잡해진다.

그래서 이렇게 구성할 수 있다.

ParentFragment_shared

순서대로 정리했기 때문에 ChildFragment인 Fragment는 바로 ParentFragment의 ViewModel을 공유해서 사용할 수 있다. 코드상으로 아래가 다이다.

private val parentViewModel: ParentFragmentSharedViewModelSampleViewModel by viewModels(
    ownerProducer = { requireParentFragment() }
)

ownerProducer을 나의 상위 ParentFragment로 지정해 줘야 한다는 점이다.

그리고 ParentFragment는 replace 할 때 parentFragmentManager를 보통 활용하는데, 이게 아닌 childFragmentManager를 활용해야 한다.

그래야 ParentFragment가 생성한 ViewModel을 child가 접근해서 가져다 쓸 수 있다.

요 부분이 조금 복잡할 수 있으나, 아래와 같이 정리할 수 있다.

private val viewModel: ParentFragmentSharedViewModelSampleViewModel by viewModels<ParentFragmentSharedViewModelSampleViewModel>()

private val pagerAdapter: ParentFragmentSharedViewModelSampleViewPager by lazy {
    ParentFragmentSharedViewModelSampleViewPager(
        fragmentManager = childFragmentManager, // 요 부분이 childFragmentManager 여야 한다.
        lifecycle = lifecycle
    )
}

샘플 코드는 아래 링크를 통해 확인할 수 있다.


마무리

일반적인 Activity SharedViewModel과 ParentFragment를 추가해 Fragment 간에서 발생할 수 있는 데이터 처리에 대해서 알아보았다.

Fragment와 Fragment 간의 데이터 처리는 보통 activity shared viewModel만으로도 충분하나, One Activity 구조를 생각한다면 ParentFragment의 활용성도 고민해야 할 것 같다.

꼭 이 경우가 아니더라도 정말 간혹 ParentFragment 간의 데이터 교환도 필요한 경우가 있을 수 있어 정리해둔다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments