(원문: http://developer.android.com/guide/components/fragments.html)

(위치: Develop > API Guides > App Components > Activities 
> Fragments)

(전체 번역 글 목록으로 이동하기)


프레그먼트 (Fragments)


프레그먼트는 액티비티가 담당하는 전체 사용자 인터페이스 중 독립적인 일부분을 맡습니다. 여러개의 화면이 하나의 액티비티에 표현되도록 하기 위해, 하나의 액티비티는 여러개의 프레그먼트를 가질 수 있습니다. 프레그먼트는 액티비티처럼 생명주기를 갖으며 사용자와 상호작용할 수 있습니다. 즉, 액티비티의 한 모듈이라고 볼 수 있습니다. 프레그먼트는 액티비티가 실행중일 때 액티비티에 추가 또는 제거될 수 있으며, 다른 액티비티들에 재사용될 수도 있습니다.

프레그먼트는 액티비티 안에 들어가며, 프레그먼트의 생명주기는 그 액티비티의 생명주기에 직접적으로 영향을 받습니다. 예를 들어, 액티비티가 paused상태가 되면 그 안에 있는 모든 프레그먼트들도 paused상태가 되며, 액티비티가 종료되면 프레그먼트들도 종료됩니다. 하지만 액티비티가 resumed상태(액티비티 생명주기 참고)로 있는 동안에, 그 안에 있는 각각의 프레그먼트는 독립적으로 추가되거나 제거될 수 있습니다. 액티비티에 추가될 때, 액티비티에 의해 관리되는 백스택에 추가될 수도 있습니다. 프레그먼트에서 다른 프레그먼트를 실행했다가, 뒤로버튼을 누르면 다시 이전 프레그먼트가 보이게 하고 싶다면, 백스택을 사용합니다.

프레그먼트가 액티비티 레이아웃의 일부가 되길 원한다면, 액티비티의 뷰 계층구조 안에서 ViewGroup 안에 해당 프레그먼트가 정의한 뷰 레이아웃이 들어가게 됩니다. 액티비티의 레이아웃 xml파일 안에 <fragment>요소로 추가할 수도 있고, 소스코드에서 동적으로 프레그먼트 객체를 생성하여 액티비티의 레이아웃에 있는 ViewGroup 객체 안에 추가할 수도 있습니다. 하지만 프레그먼트가 반드시 액티비티 레이아웃의 일부가 되어야 하는 것은 아닙니다. 프레그먼트는 눈에 보이는 UI 없이 액티비티가 필요로 하는 작업을 수행할 수도 있습니다. 

본 문서에서는 프레그먼트를 이용하여 어떻게 앱을 개발할 수 있는지에 대해 설명할 것입니다. 여기에는, 프레그먼트를 액티비티의 백스택에 추가할 때 상태를 어떻게 유지할 수 있는지, UI에서 발생한 이벤트를 어떻게 액티비티 및 다른 프레그먼트에 전달할 수 있는지, 액티비티의 액션바와 어떻게 상호작용할 수 있는지 등이 포함됩니다.


디자인 철학


프레그먼트는 태블릿과 같은 큰 화면 디바이스에서 동적이고 유연한 UI를 지원하기 위해 안드로이드 3.0(API레벨 11, 허니컴)부터 추가되었습니다. 태블릿의 화면은 스마트폰보다 훨씬 크기 때문에, UI 컴포넌트가 들어갈 공간이 상대적으로 더 많습니다. 따라서 스마트폰용으로 개발된 앱이 태블릿을 적절히 지원하기 위해서는 레이아웃의 수정이 필요한데, 프레그먼트를 이용하면 레이아웃의 수정 없이 액티비티의 전체 화면을 분할하여 프레그먼트들을 배치할 수 있는 것입니다. 

뉴스앱을 예로 들어 보겠습니다. 기사 목록을 보여주는 프레그먼트가 왼쪽에 있고, 기사 내용을 보여주는 프레그먼트가 오른쪽에 있다면, 화면의 전환 없이 한 화면에서 기사를 선택하여 읽을 수 있을 것입니다. 프레그먼트들은 각각 생명주기 콜백 메소드들을 구현할 수 있고, 각각이 따로 사용자와 상호작용할 수 있습니다. 따라서 기사 목록과 기사 내용을 각각의 화면에 보여주는 대신에 아래 그림1의 태블릿 레이아웃에서 보이는 바와 같이 한 화면에 보여줄 수 있는 것입니다.

프레그먼트는 모듈화가 되어 액티비티에서 재사용 가능하도록 개발되어야 합니다. 프레그먼트는 독립적인 존재로서 레이아웃을 정의하고, 생명주기를 갖기 때문에, 여러개의 액티비티에 재사용될 수 있습니다. 따라서, 재사용 가능하도록 설계되어야 하고, 다른 프레그먼트를 직접 참조하는 등의 의존성을 없애야 합니다. 이것은 다양한 크기의 스크린에서 그에 맞게 프레그먼트들을 조합할 수 있도록 하기 위해 중요합니다. 태블릿과 스마트폰을 동시에 지원하는 앱을 설계할 때, 각각의 레이아웃에 프레그먼트들을 적절히 배치할 수 있습니다. 예를 들어, 스마트폰에서는 태블릿과는 다르게 화면이 작기 때문에 두개의 프레그먼트를 한꺼번에 보여주기 어려울 것이며, 각각의 프레그먼트를 각각의 액티비티에 담아 별도의 화면에서 보여줄 수 있을 것입니다.

그림 1. 태블릿과 스마트폰에서, 프레그먼트로 정의된 2개의 UI모듈이 보여지는 예.


그림1을 보면
, 태블릿에서는 액티비티A에 두개의 프레그먼트를 모두 보여주고 있고, 스마트폰에서는 크기가 작기 때문에 액티비티A에서 프레그먼트A를, 액티비티B에서 프레그먼트B를 보여주고 있습니다. 프레그먼트를 재사용함으로써 태블릿과 스마트폰을 모두 지원하는 앱을 만들 수 있는 것입니다.

다양한 스크린을 지원하기 위해 프레그먼트들을 어떻게 설계하고 조합할 것인지에 대한 자세한 내용은 태블릿과 스마트폰 지원하기에서 학습하실 수 있습니다. 


프레그먼트 만들기


프레그먼트를 만들기 위해서는 Fragment 클래스나 이미 만들어진 Fragment의 서브클래스를 상속받아서, 생명주기 콜백 메소드나 그 외 필요한 기능들을 구현하면 됩니다. 프레그먼트의 콜백 메소드는 onCreate(), onStart(), onPause(), onStop() 등으로서 액티비티의 콜백 메소드들과 유사합니다. 그래서 액티비티 기반으로 이미 개발된 앱을 프레그먼트 기반으로 바꾸려고 한다면, 액티비티의 콜백 메소드에 구현된 내용들을 거의 그대로 프레그먼트의 콜백 메소드로 옮기기만 하면 됩니다.

보통 아래 몇가지 콜백 메소드들을 구현합니다.

onCreate()
프레그먼트가 생성될 때 호출됩니다. 여기서는 프레그먼트가 살아있는 동안 사용하게 될 중요한 구성요소들을 셋팅하며, 이는 프레그먼트가 paused나 stopped상태가 되었다가 다시 resumed상태가 될 때 유지가 되길 원하는 것들입니다.

onCreateView()
프레그먼트의 사용자 UI가 화면에 보여지는 첫번째 시점에 호출됩니다. 여기서는 화면에 보여줄 뷰, 즉 프레그먼트의 레이아웃을 생성하여 리턴해줘야 합니다. 만약 프레그먼트가 UI를 제공하지 않는다면, null을 리턴하면 됩니다.

onPause()
사용자가 해당 프레그먼트에서 벗어날 때 가장 먼저 호출되는 콜백 메소드입니다. 이후에 다시 resumed상태가 될 수도 있고, 아예 종료될 수도 있습니다. 따라서 여기서는 유지되길 원하는 값이 있다면 저장해 주는 일을 하도록 합니다.

대부분의 앱에서 프레그먼트를 만들 때, 위의 세가지 콜백 메소드를 구현하지만, 그 외에도 콜백 메소드가 몇가지 더 있습니다. 자세한 내용은 프레그먼트의 생명주기 다루기에서 학습하실 수 있습니다.

위에서도 언급했듯이 프레그먼트를 만들 때는 기본적으로 Fragment클래스를 상속받을 수 있지만, 이미 만들어진 서브클래스를 상속 받을 수도 있는데 그 몇가지를 소개합니다:

DialogFragment
현재 화면 위에 뜨는 다이얼로그를 구현한 프레그먼트입니다. 이것은 기존에 액티비티에서 AlertDialog를 이용해 다이얼로그를 띄우던 방식을 대체할 수 있습니다. 액티비티에 의해 관리되는 백스택에 들어가기 때문에, 뒤로 버튼을 눌렀을 때, 다이얼로그의 바닥에 보이는 이전 화면으로 돌아갈 수 있습니다.

ListFragment
ListActivity처럼 어댑터에 의해 관리되는 리스트를 보여주는 프레그먼트입니다. onListItemClick()과 같은 리스트뷰 관련 콜백 메소드들을 제공합니다.

PreferenceFragment
PreferenceActivity처럼 Preference객체를 보여주는 프레그먼트입니다. 앱의 "설정" 화면을 만들 때 사용하 수 있습니다.


사용자 인터페이스(UI) 추가하기

프레그먼트는 보통 액티비티의 사용자 인터페이스의 일부로 사용되고, 독립적인 레이아웃을 갖습니다. 

프레그먼트의 레이아웃을 구현하기 위해서는, 안드로이드 시스템이 프레그먼트의 레이아웃을 화면에 그리려고 할때 호출되는 onCreateView() 콜백 메소드를 구현하여, 여기서 프레그먼트 레이아웃의 루트가 되는 View 객체를 리턴해주면 됩니다. 

메모: 만약 ListFragment를 상속받아 프레그먼트를 만들었다면, 기본적으로 onCreateView()에서 ListView객체를 리턴하고 있기 때문에, onCreateView()를 구현하지 않아도 됩니다.

onCreateView()에서 레이아웃을 리턴하기 위해서, XML로 정의된 레이아웃 리소스로부터 뷰객체를 만들 수 있습니다. 이를 위하여 onCreateView()에서는 LayoutInflater객체를 매개변수로 제공해 줍니다.

아래 예제에서는 리소스에 있는 example_fragment.xml파일을 이용하여 레이이웃을 만드는 것을 보여줍니다.

public static class ExampleFragment extends Fragment {
   
@Override
   
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             
Bundle savedInstanceState) {
       
// Inflate the layout for this fragment
       
return inflater.inflate(R.layout.example_fragment, container, false);
   
}
}

위 예제의 container 매개변수는, 프레그먼트의 레이아웃이 들어가게 되는 부모 ViewGroup객체(액티비티 레이이웃의 일부)를 가리킵니다. 그리고 savedInstanceState 매개변수는 프레그먼트가 resumed상태가 될 때 이전 상태를 복구하기 위해 제공되는 Bundle객체입니다. (자세한 내용은 프레그먼트 생명주기 다루기에서 학습하실 수 있습니다.)

inflate()메소드는 3개의 인자를 받습니다:

  • 만들고자 하는 레이아웃의 리소스 ID.
  • 프레그먼트 레이아웃의 부모 ViewGroup객체. 레이아웃을 자식으로 추가하거나, 레이아웃이 뭔지 미리 알려줌으로써 레이아웃에 LayoutParameter를 만들어 셋팅하기 위해 필요합니다.
  • 위의 두번째 매개변수로 받은 ViewGroup객체에 추가할지 여부(boolean). 위 예제에서는 상위 클래스의 onCreateView()에서, 리턴하는 뷰를 container에 추가하는 로직이 이미 구현되어 있기 때문에 이 값이 false입니다.

지금까지는 화면에 보여질 레이아웃을 갖는 프레그먼트를 어떻게 만드는지에 대해서 알아봤습니다. 다음으로 프레그먼트를 액티비티에 어떻게 추가하는지에 대해 알아보겠습니다.

레이아웃 생성: 위 예제에서 R.layout.example_fragment는 앱의 리소스로 저장된 example_fragment.xml파일을 가리킵니다. 레이아웃 xml파일을 만드는 방법에 대한 자세한 내용은 사용자 인터페이스에서 학습하실 수 있습니다. 


프레그먼트를 액티비티에 추가하기

프레그먼트는 액티비티의 뷰 계층구조의 일부로 추가되어, 액티비티 UI의 한 부분을 구성합니다. 액티비티 레이아웃에 추가되는 방법은 2가지가 있습니다.

● 액티비티의 레이아웃 파일에 프레그먼트 요소 선언하기

이 경우, 뷰그룹이나 뷰위젯을 추가할 때처럼 레이아웃 속성값을 지정할 수 있습니다. 아래 예제는 액티비티 레이아웃에 2개의 프레그먼트를 추가하는 것을 보여줍니다:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:orientation="horizontal"
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">
   
<fragment android:name="com.example.news.ArticleListFragment"
           
android:id="@+id/list"
           
android:layout_weight="1"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
   
<fragment android:name="com.example.news.ArticleReaderFragment"
           
android:id="@+id/viewer"
           
android:layout_weight="2"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
</LinearLayout>

위 예제에서 <fragment>의 android:name 속성에는 Fragment클래스의 패키지명 포함한 이름을 지정합니다.

시스템은 액티비티의 레이아웃을 생성할 때, 각 프레그먼트의 onCreateView()를 호출하여 리턴받은 뷰를 <fragment>요소 대신에 추가합니다.

메모: 각 프레그먼트는 유일한 식별자를 필요로 합니다. 이는 액티비티가 재시작될 때 시스템이 프레그먼트를 식별하기 위함이며, 내 액티비티에서 프레그먼트를 찾아 제거하거나 뭔가 작업을 수행하기 위해서이기도 합니다. 프레그먼트에 식별자를 제공하는 방법은 아래와 같이 3가지가 있습니다.

  • android:id에 유일한 ID를 지정합니다.
  • android:tag에 유일한 문자열을 지정합니다.
  • 위의 2가지를 모두 지정하지 않았다면, 시스템은 부모뷰의 ID를 사용합니다.

소스코드에서 ViewGroup객체에 프레그먼트 추가하기

액티비티가 실행되고 있는 동안, 프레그먼트를 액티비티의 레이아웃에 추가할 수 있습니다. 그러기 위해서는 프레그먼트가 들어갈 ViewGroup객체를 지정해야 합니다. 

프레그먼트를 액티비티에 추가하거나 제거하거나 다른 프레그먼트로 대체하거나 하려면, FragmentTransaction을 사용해야 합니다. FragmentTransaction객체는 액티비티에서 아래와 같이 얻을 수 있습니다.

FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

그리고 나서 아래와 같이 add()메소드를 사용하여 프레그먼트를 추가할 수 있습니다. 

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction
.add(R.id.fragment_container, fragment);
fragmentTransaction
.commit();

add()메소드의 첫번째 인자는 프레그먼트가 추가될 부모뷰(ViewGroup)의 리소스 ID이고, 두번째 인자는 추가할 프레그먼트입니다. 

FragmentTransaction객체에 뭔가 변화가 생겼을 때는, commit()메소드를 호출해야 그것이 실제로 반영됩니다.


UI 없이 프레그먼트 추가하기

위에서는, UI를 제공하는 프레그먼트를 어떻게 액티비티에 추가할 것인가를 학습했지만, UI 없이 액티비티에서 백그라운드 작업을 하기 위해 프레그먼트를 활용할 수도 있습니다.

UI 없는 프레그먼트를 추가하려면, add(Fragment, String) 메소드를 호출하면 됩니다. 여기서 두번째 인자인 String은 프레그먼트를 구분하기 위한 유일한 "태그"이며, 뷰의 리소스 ID는 인자로 들어가지 않습니다. 이 메소드를 호출함으로써 프레그먼트를 액티비티에 추가하기는 하지만, 뷰 레이아웃을 필요로 하지는 않기 때문에 프레그먼트의 onCreateView()가 호출되지 않습니다. 따라서 onCreateView() 콜백 메소드를 구현할 필요도 없습니다.

UI가 없는 프레그먼트를 추가할 때 태그를 반드시 넣어야만 하는 것은 아닙니다. 그리고 태그는 UI가 있는 프레그먼트에도 넣을 수 있습니다. 다만, UI가 없는 프레그먼트에서는 "태그"가 프레그먼트를 구분할 수 있는 유일한 방법이기 때문에 보통 태그를 넣습니다. 그리고 나서 프레그먼트를 찾을 때는 findFragmentByTag()메소드를 호출하면 됩니다.

프레그먼트로 백그라운드 작업을 하는 예는 FragmentRetainInstance.java 샘플에서 확인하실 수 있습니다.


프레그먼트 다루기 


액티비티에서 프레그먼트를 다루기 위해서는, FragmentManager객체가 필요하며, 이는 getFragmentManager()를 호출하여 얻을 수 있습니다.

FragmentManager객체로 할 수 있는 몇가지는 아래와 같습니다:

  • 액티비티에 있는 프레그먼트를 얻고자 할때는, findFragmentById()findFragmentByTag()를 사용합니다. findFragmentById()는 UI를 제공하는 프레그먼트의 경우 사용하고, findFragmentByTag()는 UI의 제공유무와 상관없이 사용할 수 있습니다.
  • 사용자가 뒤로버튼을 눌렀을 때와 같이 프레그먼트를 백스택에서 제거할 때는 popBackStack()을 호출합니다.
  • 백스택에 변화가 생기는 순간을 잡기 위해 리스너를 등록하는 메소드는 addOnBackStackChangedListener()입니다.

FragmentManager객체의 메소드들에 대한 더 자세한 내용은 FragmentManager 클래스 문서에서 학습하실 수 있습니다.

이전 섹션에서 학습한 바와 같이, 프레그먼트를 추가 및 제거하는 등의 작업을 할 때 필요한 FragmentTransactionFragmentManager에서 얻을 수 있습니다.


프레그먼트 트랜잭션 수행하기


액티비티에서 프레그먼트를 사용함으로써 얻을 수 있는 가장 큰 이점은, 하나의 액티비티 안에서 사용자와의 상호작용을 통해 프레그먼트들을 추가하거나 제거하거나 대체하거나 하는 등의 작업을 할 수 있는 것입니다. 이러한 액티비티의 변화를 트랜잭션이라 칭하며, FragmentTransaction의 API들을 호출함으로써 트랜잭션을 수행할 수 있습니다. 액티비티에 의해 관리되는 백스택에 해당 트랜잭션을 저장함으로써, 사용자가 뒤로버튼을 누르면 이전 액티비티가 보이게 하는 등의 처리를 할 수 있습니다. (이는 액티비티 백스택의 동작과 유사합니다.)

아래와 같은 방법으로 FragmentManager로부터 FragmentTransaction객체를 얻을 수 있습니다:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

한번의 트랜잭션은, 동시에 처리되기 위한 변화들의 집합을 의미합니다. FragmentTransaction객체의 add(), remove(), replace()와 같은 메소드들을 호출함으로써 원하는 변화들을 지정할 수 있고, commit() 메소드를 호출함으로써 그 변화들의 집합을 액티비티에 반영할 수 있습니다.

프레그먼트의 백스택에 트랜잭션을 적용하기 위해서는, commit()을 호출하기 전에 addToBackStack()을 호출해야 합니다. 프레그먼트의 백스택은 액티비티에 의해 관리되며, 사용자가 뒤로버튼을 눌렀을 때 이전 프레그먼트가 보이도록 해주는 등의 작업을 위해 필요합니다.

아래 예제는 이전 프레그먼트를 다음 프레그먼트로 대체하면서, 백스택을 이용해 이전 상태로 돌아갈 수 있도록 하는 방법을 보여줍니다:

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction
.replace(R.id.fragment_container, newFragment);
transaction
.addToBackStack(null);

// Commit the transaction
transaction
.commit();

위 예제에서 newFragment는, R.id.fragment_container가 가리키는 레이아웃 안에 있는 프레그먼트를 대체합니다. 그리고 addToBackStack()을 호출함으로써 트랜잭션을 프레그먼트 백스택에 저장하여, 사용자가 뒤로버튼을 눌렀을 때 이전 프레그먼트가 다시 보일 수 있게 됩니다.

FragmentTransaction변화를 추가하는 순서는 별로 중요하지 않지만 아래 2가지는 지켜져야 합니다.

  • commit() 메소드가 제일 나중에 호출되어야 합니다.
  • 동일한 부모뷰 안에 2개 이상의 프레그먼트를 추가하는 경우에는, 추가하는 순서에 따라 먼저 추가한 프레그먼트가 뷰 계층구조에서 상위에 위치하게 됩니다.

만약 프레그먼트를 제거하는 트랜잭션을 수행할 때 addToBackStack()을 호출하지 않고 commit()을 호출한다면, 해당 프레그먼트는 종료되며(destroyed) 다시 이전상태로 돌아갈 수 없습니다. 반면에 addToBackStack()을 호출했다면, 프레그먼트는 stopped상태가 되며 사용자에 의해 이전상태로 돌아갈 때 다시 resumed상태가 됩니다.

팁: 프레그먼트 트랜잭션에 의해 프레그먼트가 바뀔 때 애니메이션을 적용하기 위해 setTransition() 메소드를 사용할 수 있습니다.

commit() 메소드 호출 직후 해당 트랜잭션이 바로 적용되는 것은 아닙니다. 안드로이드 시스템은 UI의 변화를 액티비티의 UI 쓰레드("메인" 쓰레드)에서 수행하며, commit() 호출시 해당 트랜잭션에 의한 UI 변화를 메인 쓰레드의 작업 큐(queue)에 추가하기 때문에, 이미 다른 작업들이 대기하고 있다면 그 작업들이 먼저 수행됩니다. 하지만, 현재 UI 쓰레드에서 executePendingTransactions()를 호출함으로써 commit() 호출 직후 해당 트랜잭션이 적용되도록 할 수도 있습니다. 그러나 현재 쓰레드가 다른 쓰레드들과 의존적 상관관계가 있지 않다면, executePendingTransactions()는 별로 필요하지 않을 것입니다. 다시 말해서, 메인 쓰레드의 작업큐에 다른 UI 쓰레드에 의해 추가된 "UI의 변화"가 있을 경우, 반드시 이보다 먼저 프레그먼트 트랜잭션을 반영해야 한다면 executePendingTransactions()를 수행하면 됩니다. 하지만 아마도 이러한 경우는 거의 없을 것입니다.

주의사항: commit() 메소드는 액티비티의 상태가 저장되는 시점(사용자가 액티비티를 벗어날 때) 이전에만 호출될 수 있습니다. 만약 그 이후에 호출하면 예외가 발생됩니다. 왜냐하면, commit() 메소드 호출 후의 액티비티 상태가 저장될 수 없기 때문이며, 이를 무시하고서라도 commit을 하고 싶다면, commitAllowingStateLoss()를 호출하면 됩니다.


액티비티와 소통하기


비록 프레그먼트가 액티비티에 의존적이지 않은 독립적인 존재이고 여러 액티비티에 재사용될 수 있는 존재이긴 하더라도, 그를 포함하고 있는 액티비티와 직접적으로 소통할 수 있습니다.

특히, 프레그먼트는 getActivity()를 호출하여 액티비티 객체를 얻을 수 있고 그것을 통해 아래와 같이 액티비티의 뷰를 얻을 수도 있습니다:

View listView = getActivity().findViewById(R.id.list);

이와 마찬가지로, 액티비티도 FragmentManagerfindFragmentById()findFragmentByTag() 메소드를 이용하여 아래와 같이 프레그먼트 객체를 얻을 수 있습니다:

ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);


액티비티에 이벤트 콜백 생성하기

프레그먼트가 액티비티와 이벤트를 공유해야 하는 경우 좋은 방법은, 이벤트를 받을 프레그먼트에 콜백 인터페이스를 정의하고 액티비티에서 그것을 구현하도록 하는 것입니다. 액티비티가 구현한 콜백 메소드가 호출되면, 다른 프레그먼트에 관련 정보를 공유할 수도 있습니다.

예를 들어, 뉴스앱의 액티비티에 두 개의 프레그먼트를 보여줘야 하는데 하나(프레그먼트 A)는 뉴스 목록을 보여주고 다른 하나(프레그먼트 B)는 뉴스 내용을 보여줘야 한다면, 프레그먼트 A에서 선택된 뉴스 항목을 액티비티에게 알려줘야 하고, 액티비티는 그것을 프레그먼트 B에게 공유하여 그 뉴스의 내용을 보여줘야 할 것입니다. 아래 예제에서는 FragmentAOnArticleSelectedListener를 정의하고 있습니다:

public static class FragmentA extends ListFragment {
   
...
   
// Container Activity must implement this interface
   
public interface OnArticleSelectedListener {
       
public void onArticleSelected(Uri articleUri);
   
}
   
...
}

그리고 FragmentA객체를 포함하고 있는 액티비티는 FragmentA로부터 전달받은 이벤트 정보를 FragmentB로 전달하기 위해 OnArticleSelectedListeneronArticleSelected()를 오버라이드하여 구현합니다. 그리고나서 FragmentAonAttach() 콜백 메소드를 구현해야 하는데, 여기서 매개변수로 넘겨받은 액티비티를 OnArticleSelectedListener로 타입캐스팅하여 멤버변수로 저장합니다:

public static class FragmentA extends ListFragment {
   
OnArticleSelectedListener mListener;
   
...
   
@Override
   
public void onAttach(Activity activity) {
       
super.onAttach(activity);
       
try {
            mListener
= (OnArticleSelectedListener) activity;
       
} catch (ClassCastException e) {
           
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
       
}
   
}
   
...
}

만약 액티비티가 OnArticleSelectedListener를 구현하고 있지 않다면 ClassCastException 예외가 발생될 것이며, 제대로 구현했다면 mListener 멤버변수에 해당 인터페이스에 대한 구현 객체가 저장되어 이벤트 발생시 콜백 메소드를 호출함으로써 액티비티로 이벤트 정보를 전달할 수 있을 것입니다. 아래 예제에서는, ListFragment를 상속받아 FragmentA를 구현하고, 사용자가 리스트 아이템을 클릭했을 때 호출되는 onListItemClick()을 구현하여, 여기서 액티비티에게 이벤트 정보를 전달하기 위해 mListeneronArticleSelected()를 호출합니다:

public static class FragmentA extends ListFragment {
   
OnArticleSelectedListener mListener;
   
...
   
@Override
   
public void onListItemClick(ListView l, View v, int position, long id) {
       
// Append the clicked item's row ID with the content provider Uri
       
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
       
// Send the event and Uri to the host activity
        mListener
.onArticleSelected(noteUri);
   
}
   
...
}

위의 예제에서 onListItemClick()id 매개변수는, 리스트 정보를 앱의 컨텐트 프로바이더로부터 얻어오는 경우, 선택된 항목의 id 정보를 전달해줍니다. 

컨텐트 프로바이더를 사용하는 방법에 대한 더 자세한 내용은 컨텐트 프로바이더에 대하여에서 학습하실 수 있습니다.


액션바에 항목 추가하기

프레그먼트에서는 onCreateOptionsMenu() 콜백 메소드를 구현함으로써 액티비티의 옵션 메뉴에 들어갈 항목들을 지정할 수 있습니다(액션바의 옵션 메뉴 버튼을 클릭했을 때 보여지는 메뉴). 하지만 프레그먼트가 옵션 메뉴를 사용한다는 것을 시스템에게 알려주기 위해 프레그먼트의 onCreate()에서 setHasOptionsMenu()을 호출해야만, 사용자가 옵션 메뉴 버튼을 클릭했을 때 onCreateOptionsMenu() 콜백 메소드가 호출됩니다.

프레그먼트에 의해 추가된 옵션 메뉴들은 기존에 액티비티 또는 다른 프레그먼트에 의해 추가된 옵션 메뉴들 아래로 추가됩니다. 그리고 프레그먼트에서는 옵션 메뉴가 선택되었을 때 호출되는 onOptionsItemSelected() 콜백 메소드도 오버라이드하여 구현할 수 있습니다.

registerForContextMenu()를 호출함으로써 프레그먼트 내의 특정 뷰가 컨텍스트 메뉴를 제공하도록 할 수 있습니다(컨텍스트 메뉴는 뷰를 롱클릭했을 때 뜨는 리스트 다이얼로그입니다). 사용자가 컨텍스트 메뉴를 띄울 때, 프레그먼트의 onCreateContextMenu() 콜백 메소드가 호출됩니다. 그리고 사용자가 컨텍스트 메뉴의 항목을 선택할 때, onContextItemSelected()가 호출됩니다.

메모: 프레그먼트에서 옵션 메뉴나 컨텍스트 메뉴에서 항목 선택시 실행할 작업을 구현해 놨다 하더라도, 액티비티에서 구현한 작업이 먼저 실행됩니다. 선택된 메뉴 항목에 대한 작업을 액티비티에서 다루고 있지 않을 경우에, 해당 이벤트 정보가 프레그먼트의 콜백 메소드로 전달됩니다. 액티비티의 onOptionsItemSelected()onContextItemSelected()boolean을 리턴하도록 되어 있는데, 이 값이 true면 메뉴 항목에 대한 처리를 한 것이고, false면 하지 않은 것입니다.

메뉴와 관련하여 더 자세한 내용은 메뉴액션바 문서에서 학습하실 수 있습니다.


프레그먼트의 생명주기 다루기


프레그먼트의 생명주기를 관리하는 것은 액티비티의 생명주기를 관리하는 것과 매우 유사합니다. 액티비티처럼 프레그먼트도 3가지의 상태를 갖습니다.

Resumed
프레그먼트가 액티비티에서 보여지는 상태입니다.

Paused
다른 액티비티가 전면(foreground)에 나오며 포커스를 가져갔지만 (resumed상태가 되었지만), 일부가 투명하거나 전체화면을 덮지 않으면서 여전히 이전 액티비티가 보이는 상태가 된다면, 이전 액티비티에서 보여지고 있던 프레그먼트의 상태는 paused상태가 됩니다.

Stopped
프레그먼트가 액티비티에서 보여지지 않는 상태입니다. 액티비티가 stopped상태로 되었거나, 프레그먼트가 액티비티에서 제거되었으나 백스택에 추가된 상태입니다. stopped상태의 프레그먼트는 여전히 살아있으며 멤버 변수 및 상태정보들이 시스템에 의해 유지됩니다. 하지만 사용자에게 보여지지 않는 상태이며, 액티비티가 종료되면 같이 종료됩니다.

액티비티에서처럼, 프레그먼트도 Bundle객체를 이용하여 상태를 저장 및 복구할 수 있습니다. 액티비티의 프로세스가 종료되었다가 다시 생성될 때 프로세스의 상태를 복구할 수 있다는 것입니다. 프레그먼트의 onSaveInstanceState()을 구현함으로써 상태를 저장할 수 있고, onCreate()onCreateView()onActivityCreated()에서 상태를 복구할 수 있습니다. 상태를 저장 및 복구하는 방법에 대해서는 액티비티 상태 저장하기에서 학습하실 수 있습니다.

액티비티 생명주기와 프레그먼트 생명주기의 가장 큰 차이점은 각각의 백스택에 저장되는 방식입니다. 액티비티는 stopped상태로 될 때 시스템에 의해 관리되는 액티비티 백스택에 위치하게 되며, 사용자가 뒤로버튼을 눌렀을 때 이전 액티비티가 재시작됩니다(자세한 내용은 태스크와 백스택에서 학습하실 수 있습니다). 하지만, 프레그먼트는 액티비티로부터 제거되는 트랜잭션이 수행되는 중에 addToBackStack()이 호출되어야만, 액티비티에 의해 관리되는 백스택에 저장되어 이전 프레그먼트를 재실행할 수 있습니다.

이외에, 프레그먼트의 생명주기를 관리하는 것은 액티비티의 생명주기를 관리하는 것과 매우 유사합니다. 따라서, 액티비티의 생명주기 관리하기에서 학습한 내용들을 프레그먼트에도 유사하게 적용할 수 있습니다. 하지만, 액티비티의 생명주기 흐름이 프레그먼트의 생명주기 흐름에 어떠한 영향을 끼치는지에 대해서는 충분히 이해할 필요가 있습니다.

주의사항: 프레그먼트에서 Context객체를 얻을때는 getActivity()를 사용할 수 있는데, 프레그먼트가 액티비티에 포함되어 있을때만(attached) 사용할 수 있다는 점에 주의해야 합니다. 액티비티가 아직 포함되지 않았거나, 프레그먼트가 액티비티로부터 제거된 후에 getActivity()를 호출하면 null이 리턴됩니다.


그림 3. 액티비티의 생명주기가 프레그먼트의 생명주기에 미치는 영향


액티비티의 생명주기와 연계하여 구현하기


액티비티의 생명주기는 그 안에 있는 프레그먼트들의 생명주기에 직접적인 영향을 줍니다. 그래서 프레그먼트의 생명주기 콜백 메소드는 액티비티의 생명주기 콜백 메소드와 매우 유사합니다. 예를 들어, 액티비티의 onPause()가 호출될 때, 그 액티비티 안에 있는 프레그먼트들의 onPause()들이 호출됩니다.

하지만 프레그먼트는 UI를 생성하거나 제거하는 것과 관련하여 액티비티와의 상호작용을 위해 몇가지 액티비티의 생명주기에는 없는 몇가지 콜백 메소드들을 갖습니다:

onAttach()
프레그먼트가 액티비티 안에 소속될 때 호출됩니다(매개변수로 액티비티 객체를 받습니다).

onCreateView()
프레그먼트의 뷰를 생성하기 위해 호출됩니다.

onActivityCreated()
액티비티의 onCreate()가 호출된 직후에 호출됩니다.

onDestroyView()
프레그먼트의 뷰가 제거될 때 호출됩니다.

onDetach()
프레그먼트가 액티비티로부터 떨어져 나올 때 호출됩니다.

위의 그림3은 액티비티의 생명주기에 영향을 받는 프레그먼트의 생명주기를 보여줍니다. 여기서 액티비티의 각 상태에 따라 프레그먼트의 어떤 콜백 메소드들이 호출되는지를 볼 수 있습니다. 예를 들어, 액티비티의 onCreate() 콜백 메소드가 호출되어 created상태에 있다면, 프레그먼트는 onActivityCreated() 콜백 메소드까지만 호출이 됩니다. 

일단 액티비티가 resumed상태가 되기만 하면, 동적으로 생성되는 프레그먼트들이 액티비티에 자유롭게 추가 또는 제거될 수 있습니다. 다시 말해서, 액티비티가 resumed상태에 있는 동안에만 프레그먼트의 생명주기가 독립적으로 변할 수 있다는 것입니다.

하지만 액티비티가 resumed상태에서 벗어나면, 프레그먼트는 다시 액티비티에 의해 액티비티 생명주기의 영향권으로 들어가게 됩니다. 


예제


본 문서에서 학습한 모든 것들을 포함하는 예제 코드를 소개합니다. 여기서는 2개의 프레그먼트로 구성된 하나의 액티비티를 구현하는 과정을 볼 수 있습니다. 그 2개의 프레그먼트 중 하나는 셰익스피어의 작품 목록을 보여주고, 나머지 하나는 선택된 작품의 요약된 내용을 보여줍니다. 본 예제에서는 화면의 가로 또는 세로 모드에 대하여 각 프레그먼트가 대응한 방법들도 볼 수 있습니다.

메인 액티비티는 아래 예제에서 보이는 바와 같이 일반적인 방식으로 레이아웃을 적용합니다:

@Override
protected void onCreate(Bundle savedInstanceState) {
   
super.onCreate(savedInstanceState);

    setContentView
(R.layout.fragment_layout);
}

적용된 레이아웃인 fragment_layout.xml은 아래와 같습니다:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:orientation="horizontal"
   
android:layout_width="match_parent" android:layout_height="match_parent">

   
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
           
android:id="@+id/titles" android:layout_weight="1"
           
android:layout_width="0px" android:layout_height="match_parent" />

   
<FrameLayout android:id="@+id/details" android:layout_weight="1"
           
android:layout_width="0px" android:layout_height="match_parent"
           
android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

위의 레이아웃을 사용하면, 액티비티가 레이아웃을 로드할 때, 시스템은 작품 목록을 보여주는 TitlesFragment를 생성하고, 작품 내용이 들어갈 영역인 FrameLayout을 내용 없이 영역만 잡도록 생성합니다. 그리고 사용자가 작품 목록에서 작품을 선택하면 FrameLayout 안에 해당 프레그먼트가 채워질 것입니다.

하지만, 좌우로 2개의 프레그먼트를 모두 보여주는 것은 화면이 세로 모드일 때 상당히 부자연스러울 것입니다. 그래서 위의 레이아웃은 가로 모드에서만 적용되도록 하기 위해 res/layout-land/fragment_layout.xml로 저장합니다.

그에 따라 세로 모드의 레이아웃은 아래 예제와 같이 작성하여 res/layout/fragment_layout.xml로 저장하면 됩니다:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:layout_width="match_parent" android:layout_height="match_parent">
   
<fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
           
android:id="@+id/titles"
           
android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

위의 레이아웃은 TitlesFragment만 포함하고 있습니다. 이것은, 디바이스가 세로 모드일 때 작품 목록만 화면에 보여준다는 것을 의미합니다. 그리고 이 목록에서 사용자가 작품을 선택하면, 작품 내용을 보여주기 위해 프레그먼트를 생성하는 대신에 새 액티비티를 실행할 것입니다.

다음으로, 프레그먼트 클래스들이 어떻게 구현되어 있는지를 살펴볼 것입니다. 첫번째로 TitlesFragment는 셰익스피어 작품의 목록을 보여주는 프레그먼트로서, 리스트뷰의 역할을 수행하기 위해 ListFragment를 상속받습니다.

아래 코드를 살펴보면 사용자가 리스트의 항목을 클릭했을 때 상황에 따라 다음 동작이 2갈래로 분기되는 것을 알 수 있습니다. 화면에 2개의 레이아웃이 보여지고 있다면(FrameLayout이 있다면) 현재 액티비티에서 작품 내용도 보여주기 위해 프레그먼트를 생성하여 FrameLayout에 추가할 것이며, 그렇지 않다면(FrameLayout이 없다면) 새 액티비티를 실행하여 작품 내용을 보여주는 프레그먼트를 그 액티비티에 추가할 것입니다.

public static class TitlesFragment extends ListFragment {
   
boolean mDualPane;
   
int mCurCheckPosition = 0;

   
@Override
   
public void onActivityCreated(Bundle savedInstanceState) {
       
super.onActivityCreated(savedInstanceState);

       
// Populate list with our static array of titles.
        setListAdapter
(new ArrayAdapter<String>(getActivity(),
                android
.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

       
// Check to see if we have a frame in which to embed the details
       
// fragment directly in the containing UI.
       
View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane
= detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

       
if (savedInstanceState != null) {
           
// Restore last state for checked position.
            mCurCheckPosition
= savedInstanceState.getInt("curChoice", 0);
       
}

       
if (mDualPane) {
           
// In dual-pane mode, the list view highlights the selected item.
            getListView
().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
           
// Make sure our UI is in the correct state.
            showDetails
(mCurCheckPosition);
       
}
   
}

   
@Override
   
public void onSaveInstanceState(Bundle outState) {
       
super.onSaveInstanceState(outState);
        outState
.putInt("curChoice", mCurCheckPosition);
   
}

   
@Override
   
public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails
(position);
   
}

   
/**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */

   
void showDetails(int index) {
        mCurCheckPosition
= index;

       
if (mDualPane) {
           
// We can display everything in-place with fragments, so update
           
// the list to highlight the selected item and show the data.
            getListView
().setItemChecked(index, true);

           
// Check what fragment is currently shown, replace if needed.
           
DetailsFragment details = (DetailsFragment)
                    getFragmentManager
().findFragmentById(R.id.details);
           
if (details == null || details.getShownIndex() != index) {
               
// Make new fragment to show this selection.
                details
= DetailsFragment.newInstance(index);

               
// Execute a transaction, replacing any existing fragment
               
// with this one inside the frame.
               
FragmentTransaction ft = getFragmentManager().beginTransaction();
               
if (index == 0) {
                    ft
.replace(R.id.details, details);
               
} else {
                    ft
.replace(R.id.a_item, details);
               
}
                ft
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft
.commit();
           
}

       
} else {
           
// Otherwise we need to launch a new activity to display
           
// the dialog fragment with selected text.
           
Intent intent = new Intent();
            intent
.setClass(getActivity(), DetailsActivity.class);
            intent
.putExtra("index", index);
            startActivity
(intent);
       
}
   
}
}

두번째로 DetailsFragmentTitlesFragment에서 선택된 작품의 요약된 내용을 보여주는 프레그먼트입니다:

public static class DetailsFragment extends Fragment {
   
/**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */

   
public static DetailsFragment newInstance(int index) {
       
DetailsFragment f = new DetailsFragment();

       
// Supply index input as an argument.
       
Bundle args = new Bundle();
        args
.putInt("index", index);
        f
.setArguments(args);

       
return f;
   
}

   
public int getShownIndex() {
       
return getArguments().getInt("index", 0);
   
}

   
@Override
   
public View onCreateView(LayoutInflater inflater, ViewGroup container,
           
Bundle savedInstanceState) {
       
if (container == null) {
           
// We have different layouts, and in one of them this
           
// fragment's containing frame doesn't exist.  The fragment
           
// may still be created from its saved state, but there is
           
// no reason to try to create its view hierarchy because it
           
// won't be displayed.  Note this is not needed -- we could
           
// just run the code below, where we would create and return
           
// the view hierarchy; it would just never be used.
           
return null;
       
}

       
ScrollView scroller = new ScrollView(getActivity());
       
TextView text = new TextView(getActivity());
       
int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
               
4, getActivity().getResources().getDisplayMetrics());
        text
.setPadding(padding, padding, padding, padding);
        scroller
.addView(text);
        text
.setText(Shakespeare.DIALOGUE[getShownIndex()]);
       
return scroller;
   
}
}

TitlesFragment 코드를 다시 살펴보면, 사용자가 리스트의 항목을 클릭했을 때, (DetailsFragment가 들어갈 자리인) R.id.details가 가리키는 뷰가 없다면 DetailsActivity를 실행하라고 되어 있습니다.

아래 코드가 DetailsActivity이며, 여기서는 단순히 DetailsFragment를 포함하고 있으며 화면이 세로 모드일 때 작품의 요약된 내용을 보여주도록 되어 있습니다:

public static class DetailsActivity extends Activity {

   
@Override
   
protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);

       
if (getResources().getConfiguration().orientation
               
== Configuration.ORIENTATION_LANDSCAPE) {
           
// If the screen is now in landscape mode, we can show the
           
// dialog in-line with the list so we don't need this activity.
            finish
();
           
return;
       
}

       
if (savedInstanceState == null) {
           
// During initial setup, plug in the details fragment.
           
DetailsFragment details = new DetailsFragment();
            details
.setArguments(getIntent().getExtras());
            getFragmentManager
().beginTransaction().add(android.R.id.content, details).commit();
       
}
   
}
}

위의 액티비티에서 화면이 가로 모드이면(Configuration.ORIENTATION_LANDSCAPE) 액티비티를 종료하는 부분에 주목하시기 바랍니다. 화면이 가로 모드이면 메인 액티비티에서 TitlesFragment 옆에 DetailsFragment를 보여주도록 되어 있기 때문에 DetailsActivity는 종료시키는 것입니다. 메인 액티비티를 보여줄 때 이미 가로 모드였다면 DetailsActivity를 실행할 일도 없었을 것입니다. 이러한 경우는 메인 액티비티를 보여줄 때는 세로 모드였다가, 항목을 선택하여 DetailsActivity를 실행하는 과정에서 onCreate()가 호출되기 전에 화면이 가로 모드로 바뀌는 경우라고 볼 수 있습니다.

프레그먼트와 관련하여 더 많은 샘플 코드를 확인하기 위해서는, SDK에 포함되어 있는 샘플 중 ApiDemos 를 참고하시기 바랍니다. 


Posted by 개발자 김태우
,