본문 바로가기

안드로이드

[Android] Fragment 잘 사용하기

개요

Android의 Fragment는 유용한 만큼 매우 복잡한 컴포넌트이다. 단순히 Fragment를 사용하는 것은 디버그하기 어려운 많은 문제를 마주칠 수 있다. 이런 문제를 피하기 위한 다양한 솔루션을 알아보자.

 

1. 새로운 Fragment를 onCreate에서 savedStateInstance의 체크 없이 생성하는 것

흔히 다음과 같은 코드를 자주 확인할 수 있다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    supportFragmentManager.beginTransaction()
        .replace(R.id.container, MyFragment())
        .commit()
}

위 코드의 문제점은 Activity가 시스템에 의해 제거되고 복원될 때, Fragment가 중복 생성된다는 점이다

 

이를 방지하기 위해 다음과 같이 작성하는 것을 권고한다.

if (savedInstanceState == null) {
    supportFragmentManager.beginTransaction()
        .replace(R.id.container, MyFragment())
        .commit()
}

 

2. Fragment가 소유한 객체를 onCreateView에서 생성하는 것

Fragment가 존재하는 동안 필요한 데이터 객체를 onCreateView에서 초기화하는 것을 자주 확인할 수 있다. 해당 라이프사이클 이벤트는 Fragment가 생성되거나 killed state에서 복원될 때 호출된다.

private var presenter: MyPresenter? = null
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?): View? {
    presenter = MyPresenter()
    return inflater.inflate(R.layout.frag_layout, container, false)
}

만약, 해당 Fragment가 또 다른 Fragment로 대체될 때 문제가 발생한다. 해당 Fragment는 killed가 아니고, 데이터 객체도 여전히 존재한다. 이때, 해당 Fragment가 다시 복원되는 상황이 올 경우 불필요하게 다시 데이터 객체(eg. presenter)를 다시 생성하게 되는 것이다.

 

따라서, 다음과 같이 onCreate에서 작업을 처리하면 문제를 방지할 수 있다.

private var presenter: MyPresenter? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    presenter = MyPresenter()
}

 

3. Activity에서 Fragment에 대한 참조를 유지하는 것

종종 편의상의 이유로 Activity에서 자식 Fragment의 참조를 갖고 있을 수 있다.

private var myFragment: MyFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (savedInstanceState == null) {  
        myFragment = NewFragment()
        supportFragmentManager.beginTransaction()
            .replace(R.id.container, myFragment)
            .commit()
    }
}
private fun anotherFunction() {
    myFragemnt?.doSomething() 
}

문제점은 Fragment는 고유한 라이프 사이클이 존재한다. Killed/Restore와 같은 고유한 라이프 사이클들은 Android System에서 관리된다. 이 과정에서 참조로 들고 있는 Fragment가 진짜 개발자가 원하는 Fragment가 아니게 될 수 있다. 따라서, 개발자는 이 참조를 계속 관리해주어야 하는데 이는 오류를 범하기 쉬운 부분이다.

 

이를 해결하기 위해서 다음과 같이 transaction 시에 Tag를 활용하고, Tag를 통해 Fragment에 접근하는게 올바른 방법이다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (savedInstanceState == null) {            
        supportFragmentManager.beginTransaction()
            .replace(R.id.container, NewFragment(), FragmentTag)
            .commit()
    }
}
private fun anotherFunction() {
    (supportFragmentManager.findFragmentByTag(FragmentTag) as? 
        NewFragment)?.doSomething()
}

 

4. Tag로 Simple Name을 사용하는 것

위 처럼 Tag를 통해 Fragment를 검색할 때, simpleName을 사용하는 경우가 종종 있다.

supportFragmentManager.beginTransaction()
    .replace(
        R.id.container, 
        fragment, 
        fragment.javaClass.simpleName)
    .commit()

 

위 경우 proguard에 의해 난독화 처리될 경우, 운이 나쁘면 다른 클래스 이름과 충돌할 수 있다. 이를 방지하기 위해 다음과 같이 사용하는 것을 추천한다.

supportFragmentManager.beginTransaction()
    .replace(
        R.id.container, 
        fragment, 
        fragment.javaClass.canonicalName)
    .commit()

 

5. Fragment에서 View, ViewAdapter 등에 대한 참조를 관리하지 않는 것.

Fragment에서 onDestroyView가 호출되는 상황에서 View에 대한 참조를 유지하고 있을 경우 Memory Leak이 발생할 수 있다. 해당 문제는 Fragment가 Back Stack에 들어갔을 때 발생한다. Fragment가 BackStack에 들어가면 View는 파괴되지만, Fragment는 계속 존재한다. 하지만 null로 명시하지 않는다면 GC는 View에 대한 참조를 지울 수 없을 것이다.

 

 

View나 ViewAdapter 등에 대해 다음과 같이 처리해야 한다.

override fun onDestroyView() {
    recyclerView.setAdapter(null)
    myView = null
}

 

ViewAdapter는 왜 문제가 될까?

정답은 RecyclerView Adapter가 다시 직 간접적으로 RecyclerView를 hold하고 있기 때문에, 결론적으로 View가 할당해제되지 않는게 문제이다.

 

6. Fragment 사용시 replace 보다 add를 선호하는 것

Fragment에 대한 트랜잭션 시 replace와 add라는 두 가지 옵션이 존재한다.

 

supportFragmentManager.beginTransaction()
    .add(R.id.container, myFragment)
    .commit()

 

add는 하위 Fragment가 파괴되지 않고 상위 Fragment가 나타날 때 재생성되지 않을 때 유용하다. 다음과 같은 시나리오가 존재한다.

  • 다음 Fragment가 상위 Fragment 위에 추가되면 둘다 보이는 상태이고 stack 처럼 쌓인다. 만약, 상단 Fragment가 투명한 속성이 있다면 하위 Fragment가 보일 수 있다.
  • 만약 하위에 추가된 Fragment가 로딩이 오래걸리는 경우 상위 Fragment가 제거될 때, 다시 로드되는 것을 피하고 싶을 때 유용하다.

 

하지만 위와 같은 두 가지 케이스를 제외하고는 add는 썩 좋지 않다.

  • add는 하위 Fragment가 visible한 상태로 남아있다. 더 많은 메모리를 사용하게 된다.
  • Stack에 하나 이상의 fragment를 가지고 있는 것은 복잡한 상태 복원 문제를 초래하기도 한다.
 

The Crazy Android Fragment Bug I’ve Investigated

Google, it is really a bug and not intended behavior. Please fix it.

medium.com