Android

Fragment 공부

그란. 2022. 6. 24. 19:33

 

  • configuration changed (화면 회전)이 일어나면 savedInstanceState는 null이 아닌 상태로 onCreate를 탄다 
    • 이런 상황에선 fragment를 add 할 필요가 없다 ( savedInstanceState에 의해 자동 복원되므로 )
    • 액티비티가 다시 restored 됐을때 savedInstanceState 조건을 넣지 않는다면 한번더 commit 하게 되고 그러면 프래그먼트가 2개가 생겨 버그가 발생할 수 있다 ( 예기치 못한 현상 )
  • setReorderingAllowed(true) 이유? 
    • setReorderingAllowed(true)는 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화합니다.
  • fragment-ktx에 commit 람다 함수가 있었음 (항상 beginTransaction().show().commit() 으로 했었는데..) 

 

 

 

 

프래그먼트를 삭제할 때 addToBackStack()을 호출하면 프래그먼트는 STOPPED 상태일 뿐이고 나중에 사용자가 뒤로 탐색할 때 RESUMED 상태가 됩니다. 이 경우 뷰가 소멸됩니다. 


프래그먼트 생성자에는 매개변수를 넣으면 안된다

configuration changed 나 프로세스가 재생성됐을때 시스템에서 초기화를 하는데 

기본 생성자를 호출하기 때문에 NoSuchMethod 같은 런타임 에러가 발생할수있기 때문

 


remove() vs detach() 

 

  • remove : DESTROYED 상태
  • detach : STOPPED 상태 
    • fragmentManager에 의해 관리는 됨 
    • attach() 할수있음
    • recreate 됨
    • detach() 가 프래그먼트의 onDetach()에 대응하는것은 아님
      • onDetach()는 FragmentManager에 의해 관리됨 (물론 attach도 동일) 
        • 무조건 FragmentManager에 의해서만 컨트롤 되는것은 아니고 hostActivity에 의해서도 detach 될 수 있음 
        • (내 부모가 없어지면 나도 없어져야 하니까)

Animate

 

두가지 옵션이 있음 (둘중 하나) 

  1. Animation + animator 
  2. Transition Framework

 

1. Animation 

 

fragment commit 시에 지정( fragmentManager )

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />
<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />
  setCustomAnimations(
        enter = R.anim.slide_in,
        exit = R.anim.fade_out,
        popEnter = R.anim.fade_in,
        popExit = R.anim.slide_out
    )

 

 

2. Transition 

Fragment - onCreate 에서 지정, AndroidX transitions을 사용하길 권장

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />
class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

 

 

Use shared element transitions

 

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, “item_image”)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, “hero_image”)
    }
}

setTransitionName을 지정하고 

 

supportFragmentManager.commit {
    addSharedElement(itemImageView, “hero_image”)
    replace(R.id.fragment_container, fragment)
}

이동할 TransitionName으로 설정 

 

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

받는곳 처리

 

이때 주의할점

만약 FragmentB로 이동할때 이미지가 로드되는게 느려서 어색한 애니메이션이 된다면?

 

1. 그래서 위에서 계속 얘기한 setReorderingAllowed(true)로 해야하고

 setReorderingAllowed(true)

2. 글라이드를 사용하는 경우라면 

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

이미지가 로드된 후에 startPostPonedEnterTransition()으로 Transition하게 하여 자연스러운 애니메이션 처리 가능 

 

-> 그런데 이렇게 하면 데이터바인딩 (Glide를 BindingAdapter)로 사용하지 못 할듯?

 

 

RecyclerView 에서 사용하는 방법 

 

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}
class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Fragment Lifecycle

기본 상식 : view의 라싸와 Fragment의 라싸가 분리되어있으며 view의 라싸를 보는것이 라이브데이터처리등을 할때 유용함

 

 

Caution: Avoid reusing Fragment instances after they are removed from the FragmentManager. While the fragment handles its own internal state cleanup, you might inadvertently carry over your own state into the reused instance.

 

ex) 이런 상황을 구현하자면 

 

액티비티에서 fragment를 lazy로 생성해서 재사용하는 경우  

private val aFragment by lazy { Afragment.newInstance() }

supportFragmentManager.commit{
   replace(R.id.container, aFragment) // MemoryLeak 발생 
}

해당 방법으로 사용하지 말라는 뜻

( aFragment를 재활용하고 싶어서 lazy로 만들어서 매번 생성하게 하지말고 불러오게하고 싶었는데

FragmentMananger에서 처리하게 해야함 ) 

 

-> 이건 외워놔야겠는데 

 

 


Fragment State

 

뷰 상태를 유지하려면 뷰에 ID가 필요합니다. 이 ID는 프래그먼트와 프래그먼트의 뷰 계층 구조 내에서 고유해야 합니다. ID가 없는 뷰는 상태를 유지할 수 없습니다.