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 된다.

참고

Fragment의 onActivityResult로 결과 받기

안드로이드 어플리케이션은 컴포넌트로 구성된다. ActivityManager는 컴포넌트들 사이에 Intent 형태의 데이터를 주고 받을 수 있게 해준다. 액티비티 간 데이터를 주고 받을 때 용이하도록 액티비티와 프레그먼트는 startActivity(), startActivityForResult(), onActivityResult() 메소드를 제공한다. 여기서는 프레그먼트의 onActivityResult()에 대해 살펴보자.

결론부터 말하면, 프레그먼트의 onActivityResult()도 다른 액티비티에서 데이터를 받을 수 있다. 다른 특별한 처리 없이 말이다.

지금 인터넷을 보면 액티비티의 onActivityResult()만 다른 액티비티나 프레그먼트에서 startActivityForResult()통해 요청한 작업의 결과를 받을 수 있다고 한다. 그리고, 프레그먼트의 onActivityResult()에 데이터를 전달하는 다양한 방법들(FragmentManager를 통한 프레그먼트의 onActivityResult() 명시적 호출, 이벤트 버스 사용 등)을 소개한다.  안드로이드가 이상하게 동작한다는 불만도 잊지 않는다.

아마도 이런 오해는 액티비티 간 데이터가 어떻게 전달되는지 이해하지 못해 발생한 것 같다. 액티비티가 데이터를 주고 받는 규칙을 살펴보자.

  • 액티비티의 onActivityResult()는 액티비티나 프레그먼트 둘 중 어느 것의 결과든 받는다.
  • 프레그먼트의 onActivityResult()는 프레그먼트의 startActivityForResult()를 통해 호출한 액티비티에서만 결과를 받는다.
  • 프레그먼트는 액티비티의 onActivityResult()가 호출 된 후 호출 된다. 정확히, 프레그먼트의 onActivityResult()는 액티비티의 onActivityResult() 코드 내부에서 호출된다.

그러므로, 아래와 같이 사용하면 결과를 받을 수 없다.

  • 프레그먼트 안에서 (예를 들면, getActivity() 같은 메소드를 통해 얻은) 액티비티 객체의 startActivityForResult()를 호출한다. 이 때는, 액티비티의 startActivityForResult()를 호출한 것이므로 액티비티로만 결과가 전달된다.
  • 내 액티비티에서 onActivityResult() 메소드를 오버라이드 한 후, 이 메소드에서 super.onActicityResult()로 결과를 전달하지 않고 데이터를 소진한다. 프레그먼트는 데이터를 액티비티의 onActivityResult()를 통해 전달받기 때문에 super.onActivityResult()로 값을 전달하지 않으면 프레그먼트에도 전달되지 않는다.

대신, 아래와 같이 사용해야 결과를 받을 수 있다.

  • Activity.startActivityForResult()가 아닌Fragment.startActivityForResult()를 사용해 호출한다.

프레그먼트 속 onActivityResult()로 결과가 오지 않아 힘들어 했다면, 지금이라도 바르게 사용해 보자.

그 밖에

  • 사실 액티비티에서 프레그먼트에 결과를 전달하는 과정은 인터넷에 알려진 FragmentManager를 이용한 자식 프레그먼트들의 onActivityResult() 명시적 호출과 크게 다르지 않다.
  • 프레그먼트를 추가할 때 건넨 tag를 이용해 프레그먼트를 찾기라도 한다면, 이 태그를 통해 액티비티와 프레그먼트가 강결합하게 되어 더 나쁘다.
  • 액티비티-프레그먼트A-프레그먼트B 순으로 포함하는 계층 구조라도, 프레그먼트 B에서 호출한 액티비티의 결과를 프레그먼트 A가 받지는 않는다. 액티비티에서 각 프레그먼트에 결과 값을 전달한다.
  • Fragment의 공식 문서를 보면 프레그먼트의 onActivityResult()에 대한 설명이 단촐하다. ‘액티비티의 onActivityResult()와 관련이 있다.’라고만 언급이 되어있다.
  • 액티비티의 onActivityResult()는 스코프(scope)가 protected이고, 프래그먼트의 그 것은 public이다.

참조

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

컴포넌트(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;를 해주어야 합니다.

Caused by: java.lang.IllegalStateException: Fragment already active

Caused by: java.lang.IllegalStateException: Fragment already active
at android.app.Fragment.setArguments(Fragment.java:696)

...

위와 같은 에러가 난다면 setArguments()의 주석을 먼저 확인해보자.

/**
 * Supply the construction arguments for this fragment.  This can only
 * be called before the fragment has been attached to its activity; that
 * is, you should call it immediately after constructing the fragment.  The
 * arguments supplied here will be retained across fragment destroy and
 * creation.
 */
public void setArguments(Bundle args) {
    if (mIndex >= 0) {
        throw new IllegalStateException("Fragment already active");
    }
    mArguments = args;
}

프래그먼트를 위한 생성자 아규먼트를 제공하며, 이 프래그먼트가 액티비티에 붙기(attach)전에만 호출된다고 한다. 그러므로, 프래그먼트를 만들자 마자 호출해야 제대로 동작한다는 뜻이다.

그럼, 저 에러를 나는 부분의 코드를 살펴보자. 보통 프래그먼트를 만든 후에 1회 이상 액티비티에 붙인 후일 것이다. 그러므로, setArgument()를 호출하면 이 프래그먼트는 이미 활성화(active)되어있다며 에러가 뜨게된다.

위 소스를 보면 mIndex라는 값이 0과 같거나 큰 경우에 에러를 던지고 있으며, 저 값은 Fragment.initState() 안에서 -1(최초 기본값)로 초기화 된다. 그리고, initState()는 프래그먼트가 remove 된 경우에만 프래그먼트 매니저에 의해 호출된다. 아래를 참고하자.

/**
 * Called by the fragment manager once this fragment has been removed,
 * so that we don't have any left-over state if the application decides
 * to re-use the instance.  This only clears state that the framework
 * internally manages, not things the application sets.
 */
void initState() {
    mIndex = -1;
    mWho = null;
    mAdded = false;
    mRemoving = false;
    mResumed = false;
    mFromLayout = false;
    mInLayout = false;
    mRestored = false;
    mBackStackNesting = 0;
    mFragmentManager = null;
    mActivity = null;
    mFragmentId = 0;
    mContainerId = 0;
    mTag = null;
    mHidden = false;
    mDetached = false;
    mRetaining = false;
    mLoaderManager = null;
    mLoadersStarted = false;
    mCheckedForLoaderManager = false;
}

그러므로, 프래그먼트 매니저를 통해 프래그먼트를 remove 해준 후, 다시 사용할 때 setArgument()를 제대로 호출할 수 있다. 예를 들면, remove() 호출방법은 아래와 같다.

getFragmentManager().beginTransaction().remove(activeFragment).commit();