Fragment와 생성자

Activity

안드로이드를 공부하면서 예제들을 따라하다보면 새 Activity를 시작할 때 Intent 객체를 만들어서 startActivity()의 인자로 준다. 우리가 작성하는 코드에서는 Activity 객체를 만들지는 않는다. 안드로이드에 의해 새 Activity에 해당하는 객체가 생성된다.

val intent = Intent(this, SubActivity::class.java)
startActivity(intent)

Fragment

그러나 Fragment 관련 예제의 경우 다르다. 일단 Fragment 객체를 우리가 작성하는 코드에서 직접 생성한다. 그리고 supportFragmentManager를 통해 Transaction을 열고 add, replace, addToBackStack 등의 함수로 Fragment를 Transaction에 삽입한다.
마지막으로 Transaction을 commit하여 Fragment의 Activity에 삽입을 완료한다.

val myFragment = MyFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.WhateverLayout, myFragment)
transaction.commit()

물론 많은 예제들에서 Fragment 생성시에 값을 전달 할 때, Bundle을 이용해서 값을 넘기라고 한다. 그외에도 잘 다룰 수 만 있다면 적당히 setter를 만들어서 할 수도 있을 것이다. 그렇지만 이쯤되면 이런 의문이 들 수있다.

이젠 내가 Fragment를 만들 수 있으니 생성자를 이용해 값을 넘기면 되는거 아닌가!?

그리고 실제로 생성자를 이용해서 값을 전달하면 몸속에 폭탄을 숨기고 아무 문제 없이 동작 한다.

문제점

class MyFragment(private val id: Int): Fragment() { ... }

인 Fragment를 위의 fragment 예제처럼 만들었다고 치고 화면의 orientation을 바꿔보자.
그러면 아래와 같은 에러 메시지를 뿜으면서 앱이 죽을 것이다.

Unable to instantiate fragment 경로: could not find Fragment constructor

에러메시지를 보아하니 생성자를 찾을 수 없다고 한다.
즉, 안드로이드는 우리가 설정하지 않은 다른 생성자를 사용하려고 하는 것이다.
아무것도 받지 않는 생성자를 별도로 만들면 런타임 에러는 발생하지 않겠지만, 결국 Fragment 재생성 때 원하지 않는 결과를 얻게 될 것이다.

Fragment Factory

물론 생성자를 사용하는 방법을 버리면 모든게 쉽게 해결된다. 하지만 그래도 나는 생성자가 좋다! 하면 FragmentFactory를 써보자. FragmentFactory의 instantiate()를 오버라이드하면 안드로이드에서 Fragment를 재생성 하는 방법을 커스텀하게 지정할 수 있다. 아래는 instantiate()의 원형이다. (Java)

@NonNull
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
    try {
        Class<? extends Fragment> cls = loadFragmentClass(classLoader, className);
        return cls.getConstructor().newInstance();
    } catch ...
}

그러면 위의 MyFragment를 위해 커스텀 FragmentFactory를 만들어보자.

class FragmentFactoryImpl(private val id: Int) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        return when (className) {
            MyFragment::class.java.name -> MyFragment(id)
            else -> super.instantiate(classLoader, className)
        }
    }
}

이제 Activity에서 우리가 만든 FragmentFactoryImpl을 등록하고 사용하자.

private val fragmentFactory = FragmentFactoryImpl(33)

override fun onCreate(savedInstanceState: Bundle?) {
    supportFragmentManager.fragmentFactory = fragmentFactory
    super.onCreate(savedInstanceState)
    
    val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, MyFragment::class.java.name)
    // Fragment add, addToBackstack...
}

이제 안드로이드는 화면 회전, 메모리 부족 등의 이유로 Fragment 재생성을 하면 오버라이드 된 instantiate()를 사용하게 된다. 그래서 더이상 Fragment의 생성자를 찾을 수 없다는 이유로 앱이 죽지 않는다.
이를 잘 활용하면 Fragment에 Dependency Injection을 할 때 생성자 주입도 할 수 있다.
참고글 1
참고글 2

viewLifecycleOwner

문제점

lifecycle에 맞춰 LiveData, Databinding 등으로 옵저버 패턴을 구현한다면, lifecycleOwner를 지정하게 된다. Fragment또한 Activity에 종속되는 면은 있지만 자신만의 lifecycle이 있기에 예를 들면 아래와 같이 LiveData를 구독할 것이다.

// this는 Fragment다.
whateverVM.livedata.observe(this, Observer{ ... })

하지만 이런 경우 예상과 다르게 Observer가 중복으로 등록되는 경우가 생긴다. (화면을 돌리면 이런 상황을 맞이할 것이다. 화면회전을 죽입시다.)

원인

이는 Fragment의 Lifecycle은 화면이 회전할 때 아래 그림과 같이 onDestroy를 거치지 않기 때문이다.
UI가 없는 Fragment를 사용할 수 있는 것과 연관이 있는 것인지 (Fragment 그 자체)와 (Fragment에 포함되는 View들)은 약간 다른 lifecycle을 가진다고 볼 수 있다.

그렇기 때문에 화면 회전이 발생하면, onDestroy를 거치지 않은 Fragment의 lifecycle은 죽지 않는다.
그에 따라 회전 전에 있던 기존 구독또한 해제되지 않는다.
이후 onCreateView에서 새로운 구독이 발생하고 Observer로 주었던 action들이 중복되어 실행된다.

해결책

이런 문제점을 해결하기 위해 Fragment에는 viewLifecycleOwner가 있다. viewLifecycleOwner는 onCreateView부터 onDestroyView까지의 생명주기를 가진다. 즉, onDestroyView에서 생명주기가 끝나므로 위에서 발생한 문제를 해결할 수 있다.

whateverVM.livedata.observe(viewLifecycleOwner, Observer{ ... })

참고

Fragment를 상속한 DialogFragment를 쓸 경우 하나 주의할 점이 있다.
onCreateDialog에서 Dialog 관련 로직을 작성하는 경우가 많은데, 이때 viewLifecycleOwner를 사용하면 crash가 난다.
viewLifecycleOwner의 생명주기는 onCreateView ~ onDestroyView인데 onCreateDialog는 onCreateView 이전에 불린다.
그러므로 onCreateDialog에서 viewLifecycleOwner를 접근하면 null이고 Exception이 발생한다.
아래 getViewLifecycleOwner()Java 코드를 참조하자.

@MainThread
@NonNull
public LifecycleOwner getViewLifecycleOwner() {
    if (mViewLifecycleOwner == null) {
        throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when "
                + "getView() is null i.e., before onCreateView() or after onDestroyView()");
    }
    return mViewLifecycleOwner;
}

참고글 1
참고글 2

profile
Android Developer

0개의 댓글