프래그먼트를 재사용하면 보다 효율적으로 리소스를 관리할 수 있게 된다.
이 프래그먼트를 사용하여 앱을 개발할때, 사용자 이벤트에 적절하게 반응하고 그 상태를 공유하기 위해서는 프래그먼트와 액티비티 간 통신 채널이 필요하다.
안드로이드 공식 문서에서 소개하는 프래그먼트 통신에는 2가지 방법이 있다.
추천 케이스 | ViewModel이 여러 프래그먼트 간 또는 프래그먼트와 액티비티 간에 데이터를 공유해야하는 경우
참고로 ViewModel 객체에는 UI 데이터가 저장되어 있다.
프래그먼트 내의 상호작용에따라 전역 UI 구성요소를 전환해야 할 때와 같이 프래그먼트-액티비티 간 데이터 공유가 필요한 순간이 있다.
아래 ItemViewModel 클래스는 ViewModel 클래스를 확장하여 데이터를 관리하는 뷰모델을 정의한다.
class ItemViewModel : ViewModel() { private val mutableSelectedItem = MutableLiveData<Item>() val selectedItem: LiveData<Item> get() = mutableSelectedItem fun selectItem(item: Item) { mutableSelectedItem.value = item } }
- mutableSelectedItem : 선택된 아이템을 저장하는 변수
- 변경될 수 있는
LiveData
타입
- LiveData : 아키텍처 컴포넌트에서 사용되는 데이터 홀더
- 데이터가 변경될 때마다 UI에 알려주는 역할
- selectedItem : mutableSelectedItem의 값을 읽기 전용 형태로 변환한 변수
- 외부에서 값을 변경할 수 없다.
- UI에서 선택된 아이템을 관찰하기 위한 용도
- selectItem(item: Item) : 선택된 아이템을 설정하는 함수
- 뷰모델에 저장되어 있는 선택된 아이템을 갱신하는 용도
아래의 코드에서는 MainActivity에서 이를 어떻게 활용할 수 있는지 보여준다.
class MainActivity : AppCompatActivity() { private val viewModel: ItemViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.selectedItem.observe(this, Observer { item -> }) } } class ListFragment : Fragment() { private val viewModel: ItemViewModel by activityViewModels() fun onItemClicked(item: Item) { viewModel.selectItem(item) } }
- MainActivity 클래스
- 앱의 시작점
- ItemViewModel 인스턴스를 가져온다.
- onCreate()에서 selectedItem를 관찰하여 데이터가 변경될 때 UI를 업데이트 할 수 있다.
- ListFragment 클래스
- 화면을 나타내는 프래그먼트
- ItemViewModel 인스턴스를 가져온다.
- onItemClicked()에서 사용자가 선택한 항목을 ViewModel에 설정한다.
위 사례처럼, 프래그먼트와 액티비티 간의 연결을 할 때에는 ViewModel을 사용하면된다. 이때 두 항목은 같은 ViewModel을 공유하므로써 데이터를 쉽게 전달하고 관리할 수 있게 된다.
프래그먼트 간 통신에 대한 Activity 범위를 사용하여 viewModel을 공유할 수 있다.
아래 코드는 앱에서 필터링된 목록을 표시하고 관리하는 ViewModel에 대한 예시이다.
class ListViewModel : ViewModel() { val filters = MutableLiveData<Set<Filter>>() private val originalList: LiveData<List<Item>>() = ... val filteredList: LiveData<List<Item>> = ... fun addFilter(filter: Filter) { ... } fun removeFilter(filter: Filter) { ... } } class ListFragment : Fragment() { private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class FilterFragment : Fragment() { private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filters.observe(viewLifecycleOwner, Observer { set -> // Update the selected filters UI. } } fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter) fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter) }
- ListViewModel 클래스
- 필터링된 목록을 관리하는 ViewModel
- filters : 필터의 집합을 저장하는 MutableLiveData
- originalList와 filteredList : 원본 목록과 필터링된 목록을 나타내는 LiveData
- addFilter()와 removeFilter() : 필터를 추가, 제거하는 용도
- ListFragment 클래스
- viewModel에 ListViewModel 인스턴스를 저장한다.
- filteredList로 viewModel을 관찰하여 목록이 변경될 때마다 UI를 업데이트 한다.
- FilterFragment 클래스
- viewModel에 ListViewModel 인스턴스를 저장한다.
- filters로 viewModel을 관찰하여 선택된 필터가 변경될 때마다 UI를 업데이트한다.
- onFilterSelected()와 onFilterDeselected() : 필터를 선택하거나 취소하는 용도
이렇게 목록-필터 프래그먼트 간의 통신이 이루어질 수 있으며, 통신으로부터 UI 업데이트를 해준다거나 데이터를 갱신한다거나 할 수 있다.
class ListFragment: Fragment() { private val viewModel: ListViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class ChildFragment: Fragment() { using the parent fragment's scope private val viewModel: ListViewModel by viewModels({requireParentFragment()}) //이 부분! ... }
만약 통신이 일회성으로 필요하게 되면 어떻게 할까?
예를들어, QR 코드를 읽어 이전 프래그먼트 A로 데이터를 전달해야 하는 프래그먼트 B가 있다고 상상해보자. QR 코드가 사라진 뒤에는 계속해서 통신 채널을 유지할 필요가 없으므로 한 번만 데이터를 넘겨주면 된다.
먼저 데이터를 수신하는 프래그먼트에 결과 리스너(setFragmentResultListener)를 설정해야한다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setFragmentResultListener("requestKey") { requestKey, bundle -> //이 부분 val result = bundle.getString("bundleKey") // Do something with the result. } }
button.setOnClickListener { val result = "result" setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
- B에서는 setFragmentResult 함수를 사용하여 FragmentManager로 값을 넘기고, A는 리스너로 요청하여 해당 값을 받아온다.
- 이 과정은 FragmentManager가 중재한다.
하위 프래그먼트 -> 상위 프래그먼트로의 전달은 childFragmentManager
을 호출하여 처리한다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) childFragmentManager.setFragmentResultListener("requestKey") { key, bundle -> val result = bundle.getString("bundleKey") // Do something with the result. } }
button.setOnClickListener { val result = "result" setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
- 여기서도 마찬가지로 데이터를 넘기는 쪽(하위 프래그먼트)에서는 setFragmentResult 함수를 사용하여 데이터를 넘긴다.
- 상위 프래그먼트도 setFragmentResultListener 함수를 통해 결과를 요청하여 받아온다.
- 이 과정은 ChildFragmentManager가 중재한다.
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager .setFragmentResultListener("requestKey", this) { requestKey, bundle -> val result = bundle.getString("bundleKey") // Do something with the result. } } }
supportFragmentManager
함수를 사용하면 프래그먼트 매니저에게 결과 리스너를 요청하여 받아올 수 있다.
이렇게 프래그먼트의 두 가지 통신 방법에 대해 알아보았다.
개발자가 사용 목적에 따라 취사선택하면 되지만, 공식문서에서는
[TIL-240328]