FragmentManager와 ChildFragmentManager

허니컴(Honeycomb)부터 액티비티나 프레그먼트 안에 프레그먼트를 추가할 수 있다. 이 프레그먼트 객체를 관리하는 것이 FragmentManager다. 프레그먼트 전반에 대한 소개는 공식문서를 참고하고, 여기서는 종종 헷갈려하는 FragmentManager와 일명 ChildFragmentManager의 차이점에 대해서만 설명한다.

먼저 ChildFragmentManager라는 용어부터 짚고 넘어가자. ChildFragmentManager라는 클래스는 따로 없다.  대신, Activity.getFragmentManager()나 Fragment.getFragmentManager()에서 반환하면 FragmentManager(이하 FM), Fragment.getChildFragmentManager() 메소드에서 반환되는 FragmentManager 객체를 ChildFragmentManager(이하 CFM)라고 보통 부른다.

FM과 CFM 모두 FragmentManager 클래스의 객체이니, 동작은 유사하다. 하지만, 실제 사용 시에는 헷갈린다.

  • FM은 액티비티, 액티비티 속 프레그먼트(이하 자식 프레그먼트) 그리고 프레그먼트 속 프레그먼트(이하 손자 프레그먼트)가 모두 사용할 수 있다.
  • CFM은 프레그먼트(자식)와 프레그먼트 속 프레그먼트(손자)들만 사용할 있다.
  • FM에서 관리되는 손자 프레그먼트라면, getFragmentManager()는 액티비티의 getFragmentManager()와 동일한 객체를 반환한다.
  • CFM에서 관리되는 손자 프레그먼트라면, getFragmentManager()는 부모(액티비티의 자식) 프레그먼트의 getChildFragmentManager()와 동일한 객체를 반환한다.

위와 같이 FM과 FCM은 다른 객체이므로 FragmentManager.findFragmentBy…() 메소드의 결과도 다르다. FM이 관리하는 프레그먼트는 FM에서, FCM에서 관리하는 프레그먼트는 FCM에서 찾아야할 것이다.

번거롭게 왜 두개를 유지하는 것일까? API 11에는 getFragmentManager()만 있었다. 그 후, API 17부터 getChildFragmentManager()가 추가된다. 두 개로 유지하는 것의 장점은 remove() 메소드를 통해 프레그먼트를 제거할 때 더 잘 알 수 있다. (여기서 제거란 화면에서 보이는지 여부가 아닌, …onDestroy(), onDetach()로 이어지는 프레그먼트 객체 소멸을 말한다.)

액티비티-자식프레그먼트-손자 프레그먼트의 계층구조를 가진 액티비티가 있다. 여기서 자식 프레그먼트를 제거한다고 해보자. 손자 프레그먼트가 FM에서 관리된다면 자식 프레그먼트만 제거된다.  반면, 자식 프레그먼트의 FCM을 통해 손자 프레그먼트가 관리된다면, 손자프레그먼트가 먼저 제거된 후 자식 프레그먼트가 제거된다. 이는 프레그먼트의 계층구조로 인해 오는 관리의 어려움을 쉽게 만들 수 있다.

마지막으로, 위와 같은 구조가 startActivityForResult()의 결과에도 영향을 미칠까? 그렇지 않다. 어떤 액티비티나 프레그먼트에서 호출하든, startActivityForResult()의 결과를 onActivityResult()를 통해 받을 수 있다.  FM이나 FCM 어디에서 관리하든 상관 없이 startActivityResult()를 호출했던 프레그먼트로 결과가 반환된다. Fragment.onActivityResult()의 동작도 헷갈리는 부분이 있다면 이 글을 참고하자.

그 밖에

  • support library의 FragmentActivity를 상속받아 만든 액티비티의 경우는 getSupportFragmentManager()를 통해 FragmentManager 객체를 얻는다.
  • Activity 클래스의 getFragmentManager()는 P OS부터 deprecated 된다.

참고

java.lang.IllegalStateException: Failure saving state: … has target not in fragment manager: …

프레그먼트를 사용하다보면, 아래와 같은 에러를 만날 수 있다.

Shutting down VM
FATAL EXCEPTION: main
Process: me.sunphiz.android.fragment, PID: 18882
java.lang.IllegalStateException: Failure saving state: CalleeFragment{68fee09 #7 CalleeFragment} has target not in fragment manager: CallerFragment{9c5480e}
    at android.support.v4.app.FragmentManagerImpl.saveAllState(FragmentManager.java:1843)
    at android.support.v4.app.FragmentController.saveAllState(FragmentController.java:134)
    at android.support.v4.app.FragmentActivity.onSaveInstanceState(FragmentActivity.java:566)
    at android.support.v7.app.AppCompatActivity.onSaveInstanceState(AppCompatActivity.java:498)
    at android.app.Activity.performSaveInstanceState(Activity.java:1474)
    at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1317)
    at android.app.ActivityThread.callCallActivityOnSaveInstanceState(ActivityThread.java:5398)
    at android.app.ActivityThread.performStopActivityInner(ActivityThread.java:4700)
    at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4774)
    at android.app.ActivityThread.access$1400(ActivityThread.java:222)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1819)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:158)
    at android.app.ActivityThread.main(ActivityThread.java:7229)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

먼저 로그를 옮기자면, CalleeFragment가 프레그먼트에 없는 프레그먼트를 타겟(target)으로 가지고 있어, 상태를 저장할 수 없다는 뜻이다.

“Failure saving state”로그를 보자. CalleeFragment는 라이프사이클에 따라 onSaveInstanceState()가 호출되었다. onSaveInstanceState()는 프래그먼트가 포그라운드(foreground)에서 물러날 때, onPause() ~ onDestroy() 사이에 호출되는 메소드로, 개발자에게 프레그먼트의 상태나 데이터를 저장할 수 있는 기회를 주는 메소드다. 이 때, 문제가 생겨 위와 같은 에러가 발생했다.

“CalleeFragment{68fee09 #7 CalleeFragment} has target not in fragment manager: CallerFragment{9c5480e}”로그를 살펴보자. CalleeFragment의 setTargetFragment()를 호출할 때, 프레그먼트의 인스턴스를 건넸기 때문에 타겟 프레그먼트가 처음부터 생성되지 않았을리는 없다. 그보다는, FragmentManager.replace()나 remove() 메소드로 제거되었을 것이다.

이에 대한 해결책으로는 크게 두 가지가 있다. 하나는, CalleeFragment의 onSaveInstanceState()가 호출 될 때, 이 메소드 안에서 setTargetFragment( null, -1 ) 줄을 추가하여 타겟을 날려버리는 것이다. 다른 하나는, 프레그먼트의 흐름을 파악하여, 적절한 곳에서 remove(), replace() 등을 해주는 것이다.

앞에서 설명한 두 가지 방법 중 좋은 방법은 당연히 후자이다. onSaveInstanceState()는 프레그먼트의 라이프 사이클 중 일부로 당연히 호출되는 것이다. 이를, 감안하여 개발해야지 암의로 초기화 하는 미봉책으로 수정하는 것은 대부분의 경우 다른 사이드 이펙트를 발생시킨다. 예를 들어 targetFragment를 초기화 해버리면, getTargetFragment().onActivityResult()와 같은 메소드를 더이상 호출할 수 없게 된다.

참조

알듯 말듯한 안드로이드의 컴포넌트들

컴포넌트(Component)

  • 액티비티, 서비스, 브로드캐스트 리시버, 컨텐츠 프로바이더를 안드로이드 4대 컴포넌트라 합니다.
  • 4대 컴포넌트 중 컨텐츠 프로바이더는 인텐트로 실행하지 않습니다.
  • 4대 컴포넌트 중 브로드캐스트 리시버는 AndroidManifest.xml에 등록하지 않고도 사용할 수 있으며, 이를 동적 브로드캐스트 리시버라 부릅니다.
  • 동적 브로드캐스트 리시버는 선택이 아닌 필수로, 동적 브로드캐스트 리시버를 통해서만 수신할 수 있는 브로드캐스트가 있습니다.
  • 4대 컴포넌트 중 가장 많이 쓰이는 것은 액티비티지만, 중요한 기능들은 모두 서비스에 기반합니다.
  • 액티비티는 포그라운드 작업(주로, UI), 서비스는 백그라운드 작업에 적합하다 설명하지만, 액티비티와 마찬가지로 서비스 메인 스레드(일명, UI 스레드)에서 동작하기 때문에 시간이 오래 걸리는 작업은 스레드를 따로 만들어서 처리하지 않으면 ANR을 발생시킵니다. 액티비티와 서비스는 라이프 사이클이 다르고, UI의 유무 정도의 차이가 있다고 보는 것이 옳습니다.

프레그먼트(Fragment)

  • 라이프사이클에 따라 올바르게 돌아가는 프레그먼트를 최소한의 코드로 구현하겠다면 onCreate(), onCreateView(), onPause()만 사용하면 됩니다.
  • 복구할 필요가 없다면, onCreate()와 onCreateView()만 만들어도 됩니다.
  • 프레그먼트가 꼭 UI를 가질 필요는 없습니다. 그러므로, 프레그먼트를 화면이 필요하지 않은 액티비티의 동작을 처리하는데 쓸 수 있습니다. 프레그먼트를 add()할 때 컨테이너를 지정하지 않으면 프레그먼트의 라이프 사이클 메소드 중 onCreateView()에 container가 전달되지 않습니다.
  • Fragment의 라이프 사이클을 체크하는 메소드들이 많습니다. 그 중에서 isAdded()는 onCreate()가 호출된 후, isVisible()은 onCreateView() 호출 후 true가 반환됩니다. 그러므로, 반대로 생각하면 UI를 가지지 않은 프레그먼트는 isVisible()이 항상 false가 됩니다.
  • 프레그먼트의 onCreate()메소드에서 setHasOptionsMenu()에 true를 전달하지 않으면 onCreateOptionsMenu()메소드가 호출되지 않습니다.
    액티비티처럼 프레그먼트도 액티비티의 상단 우측에 메뉴를 추가할 수 있습니다. 단, 액티비티와 달리 onCreate()에서 setHasOptionsMenu(true)를 호출해주어야 onCreateOptionsMenu()메소드가 호출되어 메뉴를 추가 할 수 있게됩니다.
  • 메뉴의 이벤트는 액티비티가 가장 먼저받은 후 프레그먼트에 전달됩니다. 만약, 액티비티에서 메뉴 이벤트를 소비해버리면 프레그먼트는 이벤트를 받지 않습니다.
    사용자가 메뉴를 눌렀을 때 이벤트는 액티비티 – 프레그먼트 순으로 전달됩니다. 만약, 액티비티에서 이벤트 처리 후, return true;를 해버리면 프레그먼트의 onOptionsItemSelected() 메소드로 이벤트가 들어오지 않습니다. 그러므로, 프레그먼트에서 소비할 수 있는 기회를 주려면 액티비티에서 return false;를 해주어야 합니다.