Fragment는 동작 또는 Activity 내에서 사용자 인터페이스의 일부를 나타냅니다. 여러 개의 프래그먼트를 하나의 액티비티에 조합하여 창이 여러 개인 UI를 구축할 수 있으며, 하나의 프래그먼트를 여러 액티비티에서 재사용할 수 있습니다. 프래그먼트는 자체 수명 주기를 가지고, 자체 입력 이벤트를 받으며, 액티비티 실행 중에 추가 및 제거가 가능한 액티비티의 모듈식 섹션이라고 생각하면 됩니다(다른 액티비티에 재사용할 수 있는 "하위 액티비티"와 같은 개념).
프래그먼트는 항상 액티비티 내에 포함되어 있어야 하며 해당 프래그먼트의 수명 주기는 호스트 액티비티의 수명 주기에 직접적으로 영향을 받습니다. 예를 들어 액티비티가 일시정지되는 경우, 그 안의 모든 프래그먼트도 일시정지되며 액티비티가 소멸되면 모든 프래그먼트도 마찬가지로 소멸됩니다. 그러나 액티비티가 실행 중인 동안에는(재개됨 수명 주기 상태에 있을 때를 말합니다) 각 프래그먼트를 추가 또는 제거하는 등 개별적으로 조작할 수 있습니다. 그와 같은 프래그먼트 트랜잭션을 수행할 때에는 이를 액티비티가 관리하는 백 스택에도 추가할 수 있습니다. 각 백 스택 항목이 발생한 프래그먼트 트랜잭션의 기록이 됩니다. 이 백 스택을 사용하면 사용자가 프래그먼트 트랜잭션을 거꾸로 돌릴 수 있습니다(뒤로 이동). 이때 Back 버튼을 누르면 됩니다.
프래그먼트를 액티비티 레이아웃의 일부로 추가하는 경우, 이는 액티비티의 뷰 계층 내부의 ViewGroup 안에 살며, 해당 프래그먼트가 자신의 뷰 레이아웃을 정의합니다. 프래그먼트를 액티비티 레이아웃에 삽입하려면 해당 프래그먼트를 액티비티의 레이아웃 파일에서 <fragment> 요소로 선언하거나, 애플리케이션 코드에서 이를 기존의 ViewGroup에 추가하면 됩니다. 그러나 프래그먼트가 액티비티 레이아웃의 일부분이어야만 하는 것은 아닙니다. 나름의 UI가 없는 프래그먼트도 액티비티를 위한 보이지 않는 작업자로 사용할 수 있습니다.
이 문서에서는 프래그먼트를 사용하도록 애플리케이션을 구축하는 방법을 설명합니다. 그중에는 프래그먼트를 액티비티의 백 스택에 추가했을 때 프래그먼트가 자신의 상태를 유지하는 방법, 액티비티 및 액티비티 내의 다른 프래그먼트와 이벤트를 공유하는 방법과 액티비티의 작업 모음에 참가하는 법 등등 여러 가지가 포함됩니다.
디자인 철학
Android가 프래그먼트를 처음 도입한 것은 Android 3.0(API 레벨 11)부터입니다. 기본적으로 태블릿과 같은 큰 화면에서 보다 역동적이고 유연한 UI 디자인을 지원하는 것이 목적이었습니다. 태블릿의 화면은 핸드셋 화면보다 훨씬 크기 때문에 UI 구성 요소를 조합하고 상호 교환할 공간이 더 많습니다. 프래그먼트는 개발자가 뷰 계층에 복잡한 변경 내용을 관리하지 않아도 이러한 디자인을 사용할 수 있도록 해줍니다. 한 액티비티의 레이아웃을 여러 프래그먼트로 나누면 런타임에 액티비티의 외관을 수정할 수도 있고 그러한 변경 내용을 해당 액티비티가 관리하는 백 스택에 보존할 수도 있습니다.
예를 들어 뉴스 애플리케이션이라면 하나의 프래그먼트를 사용하여 왼쪽에 기사 목록을 표시하도록 하고 또 다른 프래그먼트로 오른쪽에 기사 내용을 표시하도록 할 수 있습니다. 두 프래그먼트 모두 한 액티비티에서 양쪽으로 나란히 나타나며, 각 프래그먼트에 나름의 수명 주기 콜백 메서드가 있고 각자 사용자 입력 이벤트를 따로 처리하게 됩니다. 따라서, 사용자는 기사를 선택하는 데 한 액티비티를 쓰고 기사를 읽는 데 또 다른 액티비티를 선택하는 대신에 같은 액티비티 안에서 기사를 선택하고 읽는 과정을 모두 끝낼 수 있습니다. 이것은 그림 1에 나타낸 태블릿 레이아웃과 같습니다.
프래그먼트를 디자인할 때에는 각 프래그먼트를 모듈식이며 재사용 가능한 액티비티 구성 요소로 만들어야 합니다. 다시 말해, 각 프래그먼트가 나름의 레이아웃을 따로 정의하고 자기만의 수명 주기 콜백으로 자기 나름의 동작을 정의하기 때문에 한 프래그먼트를 여러 액티비티에 포함시킬 수 있습니다. 그러므로 재사용을 염두에 두고 디자인하며 한 프래그먼트를 또 다른 프래그먼트로부터 직접 조작하는 것은 삼가야 합니다. 이것은 특히 모듈식 프래그먼트를 사용하면 프래그먼트 조합을 여러 가지 화면 크기에 맞춰 변경할 수 있도록 해주기 때문에 중요한 요점입니다. 태블릿과 핸드셋을 모두 지원하는 애플리케이션을 디자인하는 경우, 사용 가능한 화면 공간을 토대로 사용자 경험을 최적화하도록 프래그먼트를 여러 레이아웃 구성에 재사용할 수 있습니다. 예를 들어 핸드셋에서의 경우 프래그먼트를 분리해서 단일 창 UI를 제공하도록 해야할 수 있습니다. 같은 액티비티 안에 하나 이상이 들어가지 않을 수 있기 때문입니다.
그림 1. 프래그먼트가 정의한 두 가지 UI 모듈이 태블릿 디자인에서는 하나의 액티비티로 조합될 수 있는 반면 핸드셋 디자인에서는 분리될 수 있다는 것을 나타낸 예시.
예를 들어—뉴스 애플리케이션 예시를 계속 사용하겠습니다—이 애플리케이션을 태블릿 크기의 기기에서 실행하는 경우, 애플리케이션 내의 액티비티 A 안에 두 개의 프래그먼트를 포함시킬 수 있습니다. 그러나 핸드셋 크기의 화면에서라면 두 프래그먼트를 모두 쓸 만큼 공간이 충분치 않습니다. 따라서 액티비티 A에는 기사 목록에 해당되는 프래그먼트만 포함하고, 사용자가 기사를 하나 선택하면 이것이 액티비티 B를 시작합니다. 여기에 기사를 읽을 두 번째 프래그먼트가 포함되어 있습니다. 따라서 이 애플리케이션은 서로 다른 조합으로 프래그먼트를 재사용함으로써 태블릿과 핸드셋을 둘 다 지원합니다 (그림 1 참조).
여러 가지 화면 구성에 맞게 여러 가지 프래그먼트 조합으로 애플리케이션을 디자인하는 법에 대한 자세한 정보는 태블릿 및 핸드셋 지원에 대한 가이드를 참조하세요.
프래그먼트 생성
그림 2. 프래그먼트의 수명 주기( 소속 액티비티가 실행 중일 때).
프래그먼트를 생성하려면 Fragment의 서브클래스(또는 이의 기존 서브클래스)를 생성해야 합니다. Fragment 클래스에는Activity와 아주 유사해 보이는 코드가 있습니다. 여기에는 액티비티와 비슷한 콜백 메서드가 들어 있습니다. 예를 들어 onCreate(), onStart(), onPause() 및 onStop() 등입니다. 사실, 기존 Android 애플리케이션을 변환하여 프래그먼트를 사용하도록 하려면 그저 액티비티의 콜백 메서드에서 프래그먼트에 해당되는 콜백 메서드로 코드를 옮기기만 하면 될 수도 있습니다.
보통은 최소한 다음과 같은 수명 주기 메서드를 구현해야 합니다.
onCreate()시스템은 프래그먼트를 생성할 때 이것을 호출합니다. 구현 내에서 프래그먼트의 기본 구성 요소 중 프래그먼트가 일시정지되거나 중단되었다가 재개되었을 때 유지하고자 하는 것을 초기화해야 합니다.onCreateView()시스템은 프래그먼트가 자신의 사용자 인터페이스를 처음으로 그릴 시간이 되면 이것을 호출합니다. 프래그먼트에 맞는 UI를 그리려면 메서드에서 View를 반환해야 합니다. 이 메서드는 프래그먼트 레이아웃의 루트입니다. 프래그먼트가 UI를 제공하지 않는 경우 null을 반환하면 됩니다.onPause()시스템이 이 메서드를 호출하는 것은 사용자가 프래그먼트를 떠난다는 첫 번째 신호입니다(다만 이것이 항상 프래그먼트가 소멸 중이라는 뜻은 아닙니다). 현재 사용자 세션을 넘어서 지속되어야 하는 변경 사항을 커밋하려면 보통 이곳에서 해야 합니다(사용자가 돌아오지 않을 수 있기 때문입니다).
대부분의 애플리케이션은 각각의 프래그먼트에 이와 같은 메서드를 최소한 세 개씩 구현해야 하지만, 프래그먼트 수명 주기의 여러 단계를 처리하려면 사용해야 하는 다른 콜백 메서드도 많이 있습니다. 모든 수명 주기 콜백 메서드는 나중에 프래그먼트 수명 주기 처리 섹션에서 더욱 상세히 논의할 것입니다.
이외에도, 기본적인 Fragment 클래스 대신 확장하고자 하는 서브클래스도 몇 개 있을 수 있습니다.
DialogFragment부동 대화상자를 표시합니다. 이 클래스를 사용하여 대화상자를 생성하면 Activity 클래스의 대화상자 도우미 메서드를 사용하는 것의 좋은 대안책이 됩니다. 이렇게 하면 프래그먼트 대화상자를 액티비티가 관리하는 프래그먼트의 백 스택에 통합시킬 수 있어, 사용자가 닫힌 프래그먼트로 돌아갈 수 있도록 해주기 때문입니다.ListFragment어댑터가 관리하는 항목의 목록(예: SimpleCursorAdapter)을 표시하며, ListActivity와 비슷합니다. 이것은 목록 뷰를 관리하는 데 쓰는 몇 가지 메서드를 제공합니다. 예를 들어 onListItemClick() 콜백을 제공하여 클릭 이벤트를 처리하는 것 등입니다.PreferenceFragmentPreference 객체의 계층을 목록으로 표시하며, PreferenceActivity와 비슷합니다. 이것은 애플리케이션에 대한 "설정" 액티비티를 생성할 때 유용합니다.
사용자 인터페이스 추가
프래그먼트는 일반적으로 액티비티의 사용자 인터페이스의 일부로 사용되며 자체 레이아웃으로 액티비티에 기여합니다.
프래그먼트에 대해 레이아웃을 제공하려면 반드시 onCreateView() 콜백 메서드를 구현해야 합니다. 이것은 프래그먼트가 자신의 레이아웃을 그릴 때가 되면 Android 시스템이 호출하는 것입니다. 이 메서드의 구현은 반드시 View를 반환해야 합니다. 이것은 프래그먼트 레이아웃의 루트입니다.
참고: 프래그먼트가 ListFragment의 서브클래스인 경우, 기본 구현이 onCreateView()로부터 ListView를 반환하므로 이를 구현하지 않아도 됩니다.
onCreateView()로부터 레이아웃을 반환하려면 이를 XML에서 정의된 레이아웃 리소스로부터 팽창시키면 됩니다. 이를 돕기 위해 onCreateView()가 LayoutInflater 객체를 제공합니다.
예를 들어 다음은 Fragment의 서브클래스로, 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);
}
}
레이아웃 생성
위의 샘플에서 R.layout.example_fragment는 애플리케이션 리소스에 저장된 example_fragment.xml이라는 레이아웃 리소스에 대한 참조입니다. 레이아웃을 XML로 생성하는 방법에 대한 정보는 사용자 인터페이스 문서를 참조하세요.
onCreateView()로 전달된 container 매개변수가 상위 ViewGroup이며(액티비티의 레이아웃으로부터), 이 안에 프래그먼트 레이아웃이 삽입됩니다. savedInstanceState 매개변수는 Bundle이며, 프래그먼트가 재개되는 중에 프래그먼트의 이전 인스턴스에 대한 데이터를 제공합니다(상태 복원은 프래그먼트 수명 주기 처리에서 자세히 설명함).
inflate() 메서드는 다음과 같은 세 개의 인수를 취합니다.
- 팽창시키고자 하는 레이아웃의 리소스 ID.
- 팽창된 레이아웃의 상위가 될 ViewGroup. container를 전달해야 (실행 중인 상위 뷰에서 지정한 것과 같이) 시스템이 레이아웃 매개변수를 팽창된 레이아웃의 루트 뷰에 적용할 수 있으므로 중요한 과정입니다.
- 팽창된 레이아웃이 팽창 중에 ViewGroup(두 번째 매개변수)에 첨부되어야 하는지를 나타내는 부울 값. (이 경우, 이것은 거짓입니다. 시스템이 이미 팽창된 레이아웃을 container— 안에 삽입하고 있기 때문입니다. true를 전달하면 최종 레이아웃에 중복된 뷰 그룹을 생성하게 됩니다).
이제 레이아웃을 제공하는 프래그먼트 생성하는 방법을 알게 되셨습니다. 다음은 프래그먼트를 액티비티에 추가해야 합니다.
액티비티에 프래그먼트 추가
프래그먼트는 보통 UI의 일부분으로 호스트 액티비티에 참가합니다. 이는 해당 액티비티의 전반적인 뷰 계층의 일부분으로 포함되게 됩니다. 프래그먼트를 액티비티 레이아웃에 추가하는 데에는 두 가지 방법이 있습니다.
- 프래그먼트를 액티비티의 레이아웃 파일 안에서 선언합니다.
이 경우, 프래그먼트에 대한 레이아웃 속성을 마치 뷰인 것처럼 나타낼 수 있습니다. 예를 들어, 다음은 프래그먼트가 두 개 있는 액티비티에 대한 레이아웃 파일입니다.
<?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() 메서드를 호출하여 각 프래그먼트의 레이아웃을 검색합니다. 시스템은 프래그먼트가 반환한 View를 <fragment> 요소 자리에 곧바로 삽입합니다.
참고: 각 프래그먼트에는 액티비티가 재시작되는 경우 프래그먼트를 복구하기 위해 시스템이 사용할 수 있는 고유한 식별자가 필요합니다(그리고, 개발자는 이것을 사용하여 프래그먼트를 캡처해 이를 제거하는 등 여러 가지 트랜잭션을 수행할 수 있습니다). 프래그먼트에 ID를 제공하는 데에는 다음과 같은 세 가지 방법이 있습니다.
- 고유한 ID와 함께 android:id 특성을 제공합니다.
- 고유한 문자열과 함께 android:tag 특성을 제공합니다.
- 위의 두 가지 중 어느 것도 제공하지 않으면, 시스템은 컨테이너 뷰의 ID를 사용합니다.
- 또는, 프로그래밍 방식으로 프래그먼트를 기존의 ViewGroup에 추가합니다.
액티비티가 실행 중인 동안에는 언제든 액티비티 레이아웃에 프래그먼트를 추가할 수 있습니다. 그저 프래그먼트를 배치할 ViewGroup를 지정하기만 하면 됩니다.
액티비티 내에서 프래그먼트 트랜잭션을 수행하려면(프래그먼트 추가, 제거 또는 교체 등), FragmentTransaction에서 가져온 API를 사용해야 합니다. FragmentTransaction의 인스턴스를 Activity에서 가져오는 방법은 다음과 같습니다.
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)을 사용하여 추가합니다(이때, 프래그먼트에 대해 보기 ID가 아니라 고유한 문자열 "태그"를 제공합니다). 이렇게 하면 프래그먼트가 추가되지만, 이것은 액티비티 레이아웃 안에 있는 뷰와 연관되어 있지 않기 때문에 onCreateView()로의 호출은 받지 않습니다. 따라서 그 메서드는 구현하지 않아도 됩니다.
프래그먼트에 대해 문자열 태그를 제공하는 것은 엄밀히 말해 비 UI 프래그먼트에만 해당되는 것은 아닙니다. UI가 있는 프래그먼트에도 문자열 태그를 제공할 수 있습니다. 하지만 프래그먼트에 UI가 없는 경우라면 이를 식별할 방법은 문자열 태그뿐입니다. 액티비티에서 나중에 프래그먼트를 가져오고자 하는 경우, findFragmentByTag()를 사용해야 합니다.
예를 들어 어떤 액티비티에서 UI 없이 프래그먼트를 배경 작업자로 사용한다고 가정해봅시다. 이것의 예로 FragmentRetainInstance.java 샘플을 들 수 있으며 이는 SDK 샘플에 포함되어 있고(Android SDK Manager를 통해 이용 가능), 시스템에서<sdk_root>/APIDemos/app/src/main/java/com/example/android/apis/app/FragmentRetainInstance.java로 찾을 수 있습니다.
프래그먼트 관리
액티비티 내의 프래그먼트를 관리하려면 FragmentManager를 사용해야 합니다. 이것을 가져오려면 액티비티에서 getFragmentManager()를 호출하세요.
FragmentManager를 가지고 할 수 있는 여러 가지 일 중에 예를 들면 다음과 같습니다.
- 액티비티 내에 존재하는 프래그먼트를 findFragmentById()로 가져오기(액티비티 레이아웃 내에서 UI를 제공하는 프래그먼트의 경우) 또는 findFragmentByTag()로 가져오기(UI를 제공하거나 하지 않는 프래그먼트의 경우).
- popBackStack()을 사용하여 프래그먼트를 백 스택에서 꺼냅니다(사용자가 Back 명령을 시뮬레이션).
- 백 스택에 변경 내용이 있는지 알아보기 위해 addOnBackStackChangedListener()로 리스너를 등록합니다.
이와 같은 메서드와 그 외 다른 메서드에 대한 자세한 정보는 FragmentManager 클래스 관련 문서를 참조하세요.
이전 섹션에서 설명한 바와 같이 FragmentManager를 사용해서도 FragmentTransaction 을 열 수 있습니다. 이렇게 하면 프래그먼트 추가 및 제거 등 트랜잭션을 수행할 수 있게 해줍니다.
프래그먼트 트랜잭션 수행
액티비티에서 프래그먼트를 사용하는 경우 특히 유용한 점은 사용자 상호작용에 응답하여 추가, 제거, 교체 및 다른 작업을 수행할 수 있다는 것입니다. 액티비티에 커밋한 변경 내용의 집합을 트랜잭션이라고 하며, 이것을 수행하려면 FragmentTransaction 내의 API를 사용하면 됩니다. 해당 액티비티가 관리하는 백 스택에 행해진 각 트랜잭션을 저장할 수도 있습니다. 이렇게 하면 사용자가 프래그먼트 변경 내역을 거쳐 뒤로 탐색할 수 있습니다(액티비티를 통과해 뒤로 탐색하는 것과 비슷합니다).
FragmentTransaction의 인스턴스를 FragmentManager로부터 가져오는 방법은 다음과 같습니다.
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
각 트랜잭션은 동시에 수행하고자 하는 변경 집합입니다. 주어진 트랜잭션에 대해 수행하고자 하는 모든 변경 사항을 설정하려면 add(), remove(), 및 replace()와 같은 메서드를 사용하면 됩니다. 그런 다음, 트랜잭션을 액티비티에 적용하려면 반드시 commit()을 호출해야 합니다.
하지만 commit()을 호출하기 전에 먼저 호출해야 할 것이 있습니다. 바로 addToBackStack()입니다. 이렇게 해야 트랜잭션을 프래그먼트 트랜잭션의 백 스택에 추가할 수 있습니다. 이 백 스택을 액티비티가 관리하며, 이를 통해 사용자가 이전 프래그먼트 상태로 되돌아갈 수 있습니다. 이때 Back 버튼을 누르면 됩니다.
예를 들어 다음은 한 프래그먼트를 다른 것으로 교체하고 이전 상태를 백 스택에 보존하는 방법을 보여줍니다.
// 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 ID로 교체합니다. addToBackStack()를 호출함으로써 교체 트랜잭션이 백 스택에 저장되므로, 사용자가 Back 버튼을 눌러 트랜잭션을 되돌리고 이전 프래그먼트를 다시 가져올 수 있습니다.
트랜잭션에 여러 개의 변경을 추가하고(예: 또 다른 add() 또는 remove()), addToBackStack()을 호출하면, commit()을 호출하기 전에 적용된 모든 변경 내용이 백 스택에 하나의 트랜잭션으로 추가되며, Back 버튼을 누르면 모두 한꺼번에 되돌려집니다.
FragmentTransaction에 변경 내용을 추가하는 순서는 중요하지 않습니다. 다만 다음과 같은 예외가 있습니다.
- commit()을 마지막으로 호출해야 합니다.
- 같은 컨테이너에 여러 개의 프래그먼트를 추가하는 경우, 이를 추가하는 순서가 이들이 뷰 계층에 나타나는 순서를 결정 짓습니다.
프래그먼트를 제거하는 트랜잭션을 수행하면서 addToBackStack()을 호출하지 않는 경우, 해당 프래그먼트는 트랜잭션이 적용되면 소멸되고 사용자가 이를 되짚어 탐색할 수 없게 됩니다. 반면에 프래그먼트를 제거하면서 addToBackStack()을 호출하면, 해당 프래그먼트는 중단되고 사용자가 뒤로 탐색하면 재개됩니다.
팁: 각 프래그먼트 트랜잭션에 대해 전환 애니메이션을 적용하려면 커밋하기 전에 setTransition()을 호출하면 됩니다.
commit()을 호출해도 그 즉시 트랜잭션을 수행하지는 않습니다. 그보다는, 액티비티의 UI 스레드("주요" 스레드)를 스레드가 할 수 있는 한 빨리 이 트랜잭션을 수행하도록 일정을 예약하는 것에 가깝습니다. 하지만 필요한 경우 UI 스레드로부터 executePendingTransactions()를 호출하면 commit()이 제출한 트랜잭션을 즉시 실행할 수 있습니다. 트랜잭션이 다른 스레드의 작업에 대한 종속성이 아니라면 굳이 이렇게 해야만 하는 것은 아닙니다.
주의: 트랜잭션을 커밋할 때 commit()을 사용해도 되는 것은 (사용자가 액티비티를 떠날 때) 액티비티가 그 상태를 저장하기전뿐입니다. 그 시점 이후에 적용하려고 하면 예외가 발생합니다. 이것은 액티비티를 복원해야 하는 경우 적용 이후의 상태가 손실될 수 있기 때문입니다. 커밋이 손실되어도 괜찮은 상황이라면, commitAllowingStateLoss()를 사용하세요.
액티비티와의 통신
Fragment는 Activity로부터 독립적인 객체로 구현되었고 여러 개의 액티비티 안에서 사용할 수 있는 것이 사실이지만, 프래그먼트의 주어진 인스턴스는 그것을 포함하고 있는 액티비티에 직접적으로 연결되어 있습니다.
구체적으로 말하면, 이 프래그먼트는 getActivity()를 사용하여 Activity 인스턴스에 액세스하여 액티비티 레이아웃에서 뷰를 찾는 것과 같은 작업을 손쉽게 수행할 수 있습니다.
View listView = getActivity().findViewById(R.id.list);
이와 마찬가지로, 액티비티도 프래그먼트 안의 메서드를 호출할 수 있습니다. 그러려면 FragmentManager로부터의Fragment에 대한 참조를 가져와야 하며, 이때 findFragmentById() 또는 findFragmentByTag()를 사용합니다. 예:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
액티비티로의 이벤트 콜백 생성
어떤 경우에는 프래그먼트로 하여금 액티비티와 이벤트를 공유하게 해야 할 수 있습니다. 이렇게 하기 위한 한 가지 좋은 방법은 프래그먼트 내부의 콜백 인터페이스를 정의한 다음 해당 호스트 액티비티가 이를 구현하도록 하는 것입니다. 액티비티가 인터페이스를 통해 콜백을 수신하면, 필요에 따라 그 정보를 레이아웃 내의 다른 프래그먼트와 공유할 수 있습니다.
예를 들어 어떤 뉴스 애플리케이션에서 액티비티 하나에 프래그먼트가 두 개 있습니다. 하나는 기사 목록을 표시(프래그먼트 A)하고 다른 하나는 기사 하나를 표시(프래그먼트 B)하는 경우 목록 항목이 선택되면 프래그먼트 A가 액티비티에 알려야 프래그먼트 B에 해당 기사를 표시하라고 알릴 수 있습니다. 이 경우, OnArticleSelectedListener 인터페이스는 프래그먼트 A 내부에 선언됩니다.
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
그러면 프래그먼트를 호스팅하는 액티비티가 OnArticleSelectedListener 인터페이스를 구현하고 onArticleSelected()를 재정의하여 프래그먼트 A로부터 발생한 이벤트를 프래그먼트 B에 알립니다. 호스트 액티비티가 이 인터페이스를 구현하도록 하려면 프래그먼트 A의 onAttach() 콜백 메서드(프래그먼트를 액티비티에 추가할 때 시스템이 호출하는 메서드)가 OnArticleSelectedListener의 인스턴스를 생성해야 합니다. 이때 onAttach()로 전달되는 Activity를 형변환하는 방법을 씁니다.
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");
}
}
...
}
액티비티가 인터페이스를 구현하지 않은 경우, 프래그먼트가 ClassCastException을 발생시킵니다. 성공 시, mListener 멤버가 액티비티의 OnArticleSelectedListener 구현에 대한 참조를 보유하므로, 프래그먼트 A가 액티비티와 이벤트를 공유할 수 있습니다. 이때 OnArticleSelectedListener 인터페이스가 정의한 메서드를 호출하는 방법을 사용합니다. 예를 들어 프래그먼트 A가 ListFragment의 확장인 경우, 사용자가 목록 항목을 클릭할 때마다 시스템이 프래그먼트 안의 onListItemClick()을 호출하고, 그러면 이것이 onArticleSelected()를 호출하여 해당 이벤트를 액티비티와 공유하는 것입니다.
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이며, 액티비티(또는 다른 프래그먼트)가 이것을 사용해 애플리케이션의 ContentProvider에서 기사를 가져옵니다.
콘텐츠 제공자 사용법에 대한 자세한 정보는 콘텐츠 제공자 문서에서 이용하실 수 있습니다.
앱 바에 항목 추가
프래그먼트는 액티비티의 옵션 메뉴에(결과적으로 앱 바에도) 메뉴 항목을 추가할 수 있습니다. 이때onCreateOptionsMenu()를 구현하는 방법을 씁니다. 이 메서드가 호출을 수신하도록 하려면, setHasOptionsMenu()중에 onCreate()를 호출하여 프래그먼트가 옵션 메뉴에 항목을 추가하고자 한다는 것을 나타내야 합니다(그렇지 않으면 해당 프래그먼트가 onCreateOptionsMenu()로의 호출을 받지 못하게 됩니다).
그런 다음 프래그먼트로부터 옵션 메뉴에 추가하는 모든 항목은 기존의 메뉴 항목에 추가됩니다. 메뉴 항목을 선택하면 해당 프래그먼트는 onOptionsItemSelected() 콜백도 수신하게 됩니다.
또한 프래그먼트 레이아웃에 뷰를 등록하여 컨텍스트 메뉴를 제공하도록 할 수도 있습니다. 이때 registerForContextMenu()를 호출하면 됩니다. 사용자가 컨텍스트 메뉴를 열면, 해당 프래그먼트는 {@linkandroid.app.Fragment#onCreateContextMenu(ContextMenu,View,ContextMenu.ContextMenuInfo) onCreateContextMenu()} 호출을 수신합니다. 사용자가 항목을 선택하면, 해당 프래그먼트는 onContextItemSelected() 호출을 수신합니다.
참고: 프래그먼트는 추가한 각 메뉴 항목이 선택될 때 콜백을 받게 되지만, 사용자가 메뉴 항목을 선택할 때 그에 상응하는 콜백을 가장 처음 받는 것은 액티비티입니다. 액티비티가 구현한 항목 선택 시 콜백이 선택된 항목을 다루지 않는 경우, 해당 이벤트는 프래그먼트의 콜백으로 전달됩니다. 이는 옵션 메뉴와 컨텍스트 메뉴에 모두 해당됩니다.
메뉴에 대한 자세한 내용은, 메뉴 개발자 가이드와 앱 바 교육 과정을 참조하세요.
프래그먼트 수명 주기 처리
그림 3. 액티비티 수명 주기가 프래그먼트 수명 주기에 미치는 영향.
프래그먼트의 수명 주기를 관리하는 것은 액티비티의 수명 주기를 관리하는 것과 매우 비슷합니다. 액티비티와 마찬가지로 프래그먼트는 세 가지 상태로 존재할 수 있습니다.
재개됨(Resumed)프래그먼트가 실행 중인 액티비티에 표시됩니다.일시정지됨(Paused)또 다른 액티비티가 포그라운드에 있고 포커스를 갖고 있지만, 이 프래그먼트가 있는 액티비티도 여전히 표시됩니다(포그라운드의 액티비티가 부분적으로 투명하거나 전체 화면을 뒤덮지 않습니다).정지됨(Stopped)프래그먼트가 표시되지 않습니다. 호스트 액티비티가 정지되었거나 프래그먼트가 액티비티에서 제거되었지만 백 스택에 추가되었습니다. 정지된 프래그먼트도 여전히 표시는 됩니다(모든 상태 및 멤버 정보를 시스템이 보존합니다). 하지만, 사용자에게는 더 이상 표시되지 않으며 액티비티를 종료하면 이것도 종료됩니다.
이번에도 액티비티와 마찬가지로, 프래그먼트의 상태를 보존하려면 Bundle을 사용합니다. 이는 혹시나 액티비티의 프로세스가 종료되고 액티비티를 다시 만들 때 해당 프래그먼트의 상태를 복구해야 할 필요가 있을 때를 대비하는 것입니다. 프래그먼트의 onSaveInstanceState() 콜백 중에 상태를 저장할 수 있고,onCreate(), onCreateView() 또는 onActivityCreated() 중에 상태를 복구할 수 있습니다. 상태 저장에 관한 자세한 정보는 액티비티 문서를 참조하세요.
액티비티와 프래그먼트의 수명 주기에서 가장 중대한 차이점은 해당되는 백 스택에 저장되는 방법입니다. 액티비티는 기본적으로 중단되었을 때 시스템이 관리하는 액티비티 백 스택 안에 배치됩니다. (따라서 사용자가 Back 버튼을 사용하여 다시 이 액티비티로 돌아갈 수 있습니다. 이 내용은 작업 및 백 스택에서 설명하였습니다.) 하지만, 프래그먼트가 호스트 액티비티가 관리하는 백 스택 안에 배치되는 경우는 트랜잭션 동안 프래그먼트를 제거하는 addToBackStack()을 호출하여 해당 인스턴스를 저장하라고 명시적으로 요청하는 경우뿐입니다.
이것만 제외하면, 프래그먼트 수명 주기를 관리하는 것은 액티비티의 수명 주기를 관리하는 것과 아주 비슷합니다. 따라서, 액티비티 수명 주기 관리에 쓰이는 사례가 프래그먼트에도 똑같이 적용됩니다. 하지만 또 한 가지 이해해두어야 하는 것이 있습니다. 즉, 액티비티의 수명이 프래그먼트의 수명에 어떤 영향을 미치는지를 알아두어야 합니다.
주의: Fragment 내에서 Context 객체가 필요한 경우, getActivity()를 호출하면 됩니다. 그러나 getActivity()를 호출하는 것은 프래그먼트가 액티비티에 첨부되어 있는 경우뿐이니 유의하세요. 프래그먼트가 아직 첨부되지 않았거나 수명 주기가 끝날 무렵 분리된 경우, getActivity()가 null을 반환합니다.
액티비티 수명 주기와의 조화
프래그먼트가 있는 액티비티의 수명 주기는 해당 프래그먼트의 수명 주기에 직접적인 영향을 미칩니다. 따라서 액티비티에 대한 각 수명 주기 콜백이 각 프래그먼트에 대한 비슷한 콜백을 유발합니다. 예를 들어 액티비티가 onPause()를 받으면, 해당 액티비티 내의 각 프래그먼트가 onPause()를 받습니다.
하지만, 프래그먼트에는 프래그먼트의 UI를 구축하고 소멸시키는 것과 같은 작업을 하기 위해 액티비티와의 고유한 상호작용을 처리하는 몇 가지 수명 주기 콜백이 더 있습니다. 이러한 추가 콜백 메서드에는 다음과 같은 것들이 있습니다.
onAttach()프래그먼트가 액티비티와 연관되어 있었던 경우 호출됩니다(여기에서 Activity가 전달됩니다).onCreateView()프래그먼트와 연관된 뷰 계층을 생성하기 위해 호출됩니다.onActivityCreated()액티비티의 onCreate() 메서드가 반환되면 호출됩니다.onDestroyView()프래그먼트와 연관된 뷰 계층이 제거되는 중일 때 호출됩니다.onDetach()프래그먼트가 액티비티와 연결이 끊어지는 중일 때 호출됩니다.
호스트 액티비티의 영향을 받는 동안 프래그먼트 수명 주기의 흐름이 그림 3에 나타나 있습니다. 이 그림에서는 프래그먼트가 어떤 콜백 메서드를 수신할지 여부는 액티비티의 각 연속된 상태에 따라 결정된다는 것을 보여줍니다. 예를 들어 액티비티가 자신의 onCreate() 콜백을 받은 경우, 해당 액티비티 안에 있는 프래그먼트는 onActivityCreated() 콜백을 받을 뿐입니다.
액티비티가 재개된 상태에 도달하면 자유자재로 프래그먼트를 액티비티에 추가하거나 액티비티에서 제거해도 됩니다. 따라서, 액티비티가 재개된 상태에 있는 동안에만 프래그먼트의 수명 주기를 독립적으로 변경할 수 있습니다.
그러나 액티비티가 재개된 상태를 떠나면 액티비티는 다시 프래그먼트를 그 수명 주기 안으로 넣습니다.
예
이 문서에서 논의한 모든 것을 한 번에 모아 보기 위해, 다음은 두 개의 프래그먼트를 사용하여 창이 두 개인 레이아웃을 생성하는 액티비티를 예시로 나타낸 것입니다. 아래의 액티비티에 포함된 한 프래그먼트는 셰익스피어 희곡 제목 목록을 표시하고, 또 다른 하나는 목록에서 선택했을 때 해당 희곡의 요약을 표시합니다. 또한 화면 구성을 근거로 프래그먼트를 여러 가지로 구성하여 제공하는 방법도 보여줍니다.
참고: 이 액티비티에 대한 완전한 소스 코드는 FragmentLayout.java에 있습니다.
주요 액티비티는 onCreate() 중에 일반적인 방식으로 레이아웃을 적용합니다.
@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 안에 배치됩니다.
그러나 희곡 목록과 요약을 둘 다 나란히 표시할 만큼 너비가 넓지 않은 화면 구성도 있습니다. 따라서 위의 레이아웃은 가로 방향 화면 구성에만 사용되며, 이를 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를 확장하며, 목록 뷰 작업의 대부분을 처리하기 위해 여기에 의존합니다.
이 코드를 살펴보면서 사용자가 목록 항목을 클릭하면 일어날 수 있는 두 가지 동작이 있다는 점을 눈여겨 보세요. 두 레이아웃 중 어느 것이 활성화 상태인지에 따라 같은 액티비티 내에서 세부 사항을 표시하기 위해 새 프래그먼트를 생성하거나 표시할 수도 있고(프래그먼트를 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);
}
}
}
두 번째 프래그먼트인 DetailsFragment는 TitlesFragment에서 가져온 목록에서 선택한 항목에 대한 희곡 요약을 표시하는 것입니다.
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 액티비티를 시작하게 됩니다.
다음은 화면이 세로 방향으로 구성되어 있을 때 선택한 희곡의 요약을 표시하기 위해 단순히 DetailsFragment를 포함하는 DetailsActivity입니다.
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();
}
}
}
이 액티비티는 구성이 가로 방향인 경우 알아서 종료한다는 점을 눈여겨 보세요. 따라서 주요 액티비티가 작업을 인계 받아 DetailsFragment를 TitlesFragment와 함께 표시할 수 있는 것입니다. 이것은 사용자가 세로 방향 구성에서 DetailsActivity를 시작했지만 그런 다음 가로 방향으로 돌리는 경우(현재 액티비티를 다시 시작함) 일어날 수 있습니다.
프래그먼트 사용에 대한 더 많은 샘플(및 이 예시에 대한 완전한 소스 파일)을 보려면 ApiDemos에서 이용할 수 있는 API Demos 샘플 앱을 참조하세요(샘플 SDK 구성 요소에서 다운로드할 수 있습니다).
'개발 > Adroid' 카테고리의 다른 글
[android studio] ImageButton 배경 투명하게 만들기 (0) | 2019.08.17 |
---|---|
[android studio] RecyclerView (0) | 2019.08.16 |
[android studio] android.support.v4.app.CoreComponentFactory (0) | 2019.08.12 |
[android] Google Maps Android API 사용 방법 및 예제 (1) | 2019.08.09 |
android 타이틀 or 상태 바 없애기 / 전체화면 (0) | 2019.07.30 |
댓글