[안드로이드]부모,자식 프레그먼트와 프레그먼트 매니저 활용

Lee Yongin·2022년 6월 17일
0

1.서론


private const val NUM_PAGES = 4

class ScreenSlidePagerAdapter(fa: FragmentActivity): FragmentStateAdapter(fa){
    val fragmentList = mutableListOf(HomeFragment(),
        MyBanguRootFragment(),FeedFragment(),ProfileFragment())
    override fun createFragment(position: Int): Fragment {
        return fragmentList[position]
    }

    override fun getItemCount(): Int = NUM_PAGES
}

뷰페이저를 활용해서 페이지 속 하위프레그먼트를 교체하는 방법을 포스팅한다. 간단하게 어댑터에는 빈 프레그먼트를 넣고 이 프레그먼트의 프레그먼트매니저를 부모로 삼아 하위 프레그먼트들을 조정하는 방법을 소개한다.

1.본론

Single Activity Architecture방식에서는 페이지를 프레그먼트로 구성하기 때문에 프레그먼트매니저 활용을 잘 해야한다. 토이프로젝트를 하다가 개념이해에 좋은 예시를 만든 것 같아서 포스팅한다.

위의 그림은 최종적으로 ViewPager2의 여러 페이지가 프레그먼트로 구성된 상황이다. 특정 페이지의 하위페이지를 바꾸고, 상하위 프레그먼트 간 정보를 교환해야 하는 2가지 경우를 알아보자.

1.ViewPager2의 하위 프레그먼트 교체

1.뷰페이저 안의 상하위 프레그먼트 구조 만들기

ScreenSlidePagerAdapter에 빈 상위프레그먼트를 삽입하고 그 안에 하위프레그먼트를 넣으면 아래 그림같은 프레그먼트매니저 관계가 형성된다.

1.childFragmentManager란?

각 프레그먼트는 본인의 프레그먼트매니저가 존재한다. 이걸 childFragmentManager이라고 한다.
childFragmentManager는 본 프레그먼트가 품고있는 하위프레그먼트를 호출하고, 그 하위프레그먼트 간의 정보를 송수신할 수 있다.

2.parentFragmentManager란?

하위 프레그먼트라면 그 바깥에 상위 프레그먼트도 존재한다. 그 상위 프레그먼트의 childFragmentManager가 하위 프레그먼트의 parentFragmentManager이다. 할아버지가 A를 자식이라 부르면 나는 당연히 A를 부모라고 부르는 상황과 같다.

*프레그먼트중 가장 상위라고 하면 그 프레그먼트를 호출할 수 있는건 Activity 뿐일 것이다. 안드로이드 API level28까지는 아래와 같이 getFragmentManager()메소드를 사용할 수 있었지만 이제는 child또는 parentFragmentManager만을 사용한다.

Activity.getFragmentManager()
Fragment.getFragmentManager()

2.하위 프레그먼트 교체 코드

뷰페이저 초기화에 사용되는 Frament2을 아래와 같이 FrameLayout으로 지정하고 이 안에
FragmentA, FragmentB 등의 하위 프레그먼트를 replace해주면 된다.

<?fragment2.xml?>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mybangu_root_frag"
    android:background="@color/canvas"
    tools:context=".main.mybangu.ui.MyBanguRootFragment">

</FrameLayout>
//Fragment2.kt
*root프레그먼트의 디폴트프레그먼트를 FragmentA로 지정함*/
        childFragmentManager.beginTransaction().apply {
            replace(R.id.mybangu_root_frag,FragmentA())
            commit()
        }
//FragmentA.kt
/*페이지로 가기*/
        binding.backCursor.setOnClickListener {
            parentFragmentManager.beginTransaction(). replace(R.id.search_popup_frame, FragmentA()).commit()
        }
//FragmentB.kt
/*리뷰 작성 기능*/
        binding.mybanguWritebtn.setOnClickListener{
            //ReviewFragment로 이동
            parentFragmentManager.beginTransaction().apply {
                replace(R.id.mybangu_root_frag, FragmentA())
                setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) //Fragment is being added onto the stack
                addToBackStack(null)
                commit()
            }
        }

2.상하위 프레그먼트 간의 정보교환

아래의 그림은 그 반대의 경우인데, 부모 프레그먼트의 자식에 해당하면서, 자식 프레그먼트의 부모에 해당하는 프레그먼트매니저 1개가 정보를 매개하는 모습니다. setFragmentResultListener()로 정보를 부모 프레그먼트에게서 받은뒤 자식 프레그먼트가 프레그먼트매니저에게 setFragmentResult()로 정보를 요청하면 그 정보를 전달해주는 것이다.

이 글의 예시코드와 설명은 위의 상황과 반대로, 자식(하위) FragmentC의 데이터를 부모(상위) FragmentB로 전달하는 경우이다. 아래 그림처럼 FragmentB와 FragmentC를 매개하는 FragmentManagerB는 FragmentB에겐 child로, FragmentC에겐 parent로 불리게 된다.

내가 구현한 코드의 경우 FragmentC 안의 리사이클뷰의 아이템 데이터를 FragmentB로 보내야했다. Communicator라는 인터페이스의 passData()라는 추상메소드를 통해 리사이클뷰 어댑터에서 FragmentC로 데이터를 전달한 다음 상하위 프레그먼트 간의 정보교환을 구현했다. (코드볼 때 오잉 할까봐 설명)

//FragmentC.kt
val SpRvadapter = SearchPuAdapter(object:Communicator{
            override fun passData(title: String, imageUrl: String) {
                val bundle_title = title
                val bundle_imageUrl = imageUrl
                parentFragmentManager.setFragmentResult("requestKey", bundleOf("title" to bundle_title,"imageUrl" to bundle_imageUrl))
                parentFragmentManager.beginTransaction().remove(this@FragmentB).commit()//현재 프레그먼트 닫기
            }
        })

위의 코드는 리사이클뷰의 아이템의 데이터 2가지, bundle_title과 bundle_imageUrl을 bundle안의 pair객체로 넣어서 부모프래그먼트매니저에 전달한다.
key-value 쌍의 pair 2개가 bundle안에 넣었으니 아래의 코드처럼 FragmentB도 bundle_title와 bundle_imageUrl을 얻고 싶으면 그에 맞는 key값을 넣어야 한다.

//FragmentB.kt
childFragmentManager.setFragmentResultListener("requestKey",viewLifecycleOwner,
            FragmentResultListener{ key,bundle->
                var result_title : String ?= null
                var result_imageUrl : String ?= null
                result_title = bundle.getString("title")
                result_imageUrl = bundle.getString("imageUrl")

                /*수신받은 영화 이미지, 작품명 바인딩*/
                Glide.with(binding.root).load(result_imageUrl).override(28, 28)
                    .into(binding.mybanguImage) //이미지
                binding.resultMovietitle.text = result_title //작품명
            })

3.결론

1.더 좋은 방법


private const val NUM_PAGES = 4

class ScreenSlidePagerAdapter(fa: FragmentActivity): FragmentStateAdapter(fa){
    val fragmentList = mutableListOf(HomeFragment(),
        MyBanguRootFragment(),FeedFragment(),ProfileFragment())
    override fun createFragment(position: Int): Fragment {
        return fragmentList[position]
    }

    override fun getItemCount(): Int = NUM_PAGES
}

이제껏 설명한 방식은 위의 ScreenSlidePageAdapter에 빈 프레그먼트를 두고 하위프레그먼트 1개를 디폴트로 지정한 것이었다. 아무리 onCreateView 시점에서 하위프래그먼트를 호출한다고 해도 낭비는 낭비인 것 같고, 무엇보다 FragmentStateAdapter가 ecyclerView.Adapter를 상속받았다는 장점을 살리지 못한다. notifyItemChanged(), notifyDataSetChanged()를 활용하면 상하위관계 없이 구현가능할 것 같다.

3.참고자료
프래그먼트매니저 관련: http://sunphiz.me/wp/archives/2395
참고한 깃허브: https://github.com/danilao/fragments-viewpager-example
프래그먼트 간 데이터 전달: https://developer.android.com/training/basics/fragments/pass-data-between?hl=ko

profile
개발공부를 미식칼럼 읽듯이 하고싶다구요

0개의 댓글